@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.cjs CHANGED
@@ -20,7 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ACTION_BODY_WRITE_EXCLUSIONS: () => ACTION_BODY_WRITE_EXCLUSIONS,
24
+ ACTION_BODY_WRITE_PATTERNS: () => ACTION_BODY_WRITE_PATTERNS,
25
+ ACTION_BODY_WRITE_PATTERN_IDS: () => ACTION_BODY_WRITE_PATTERN_IDS,
26
+ ACTION_BODY_WRITE_UNKNOWN_FIELD: () => ACTION_BODY_WRITE_UNKNOWN_FIELD,
23
27
  ACTION_NAME_UNDEFINED: () => ACTION_NAME_UNDEFINED,
28
+ ACTION_RECORD_WRITE_DISCARDED: () => ACTION_RECORD_WRITE_DISCARDED,
29
+ ACTION_RECORD_WRITE_PATTERNS: () => ACTION_RECORD_WRITE_PATTERNS,
30
+ ACTION_RECORD_WRITE_PATTERN_IDS: () => ACTION_RECORD_WRITE_PATTERN_IDS,
24
31
  AGENT_AUTHORING_WITHDRAWN: () => AGENT_AUTHORING_WITHDRAWN,
25
32
  AI_SKILL_SURFACE_MISMATCH: () => AI_SKILL_SURFACE_MISMATCH,
26
33
  AI_SKILL_TOOL_UNRESOLVED: () => AI_SKILL_TOOL_UNRESOLVED,
@@ -47,13 +54,20 @@ __export(index_exports, {
47
54
  FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
48
55
  FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
49
56
  FLOW_DRAFT_STATUS_AMBIGUOUS: () => FLOW_DRAFT_STATUS_AMBIGUOUS,
57
+ FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
50
58
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL: () => FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
51
59
  FLOW_TEMPLATE_UNKNOWN_FIELD: () => FLOW_TEMPLATE_UNKNOWN_FIELD,
52
60
  FLOW_TRIGGER_UNKNOWN_OBJECT: () => FLOW_TRIGGER_UNKNOWN_OBJECT,
53
61
  FLOW_UPDATE_READONLY_FIELD: () => FLOW_UPDATE_READONLY_FIELD,
54
62
  FLOW_UPDATE_READONLY_WHEN_FIELD: () => FLOW_UPDATE_READONLY_WHEN_FIELD,
63
+ FLOW_WRITE_NODE_TYPES: () => FLOW_WRITE_NODE_TYPES,
64
+ FLOW_WRITE_NODE_TYPES_DEFERRED: () => FLOW_WRITE_NODE_TYPES_DEFERRED,
55
65
  FORM_COLSPAN_ABSOLUTE: () => FORM_COLSPAN_ABSOLUTE,
56
66
  FORM_FIELD_UNKNOWN: () => FORM_FIELD_UNKNOWN,
67
+ HOOK_BODY_WRITE_EXCLUSIONS: () => HOOK_BODY_WRITE_EXCLUSIONS,
68
+ HOOK_BODY_WRITE_PATTERNS: () => HOOK_BODY_WRITE_PATTERNS,
69
+ HOOK_BODY_WRITE_PATTERN_IDS: () => HOOK_BODY_WRITE_PATTERN_IDS,
70
+ HOOK_BODY_WRITE_UNKNOWN_FIELD: () => HOOK_BODY_WRITE_UNKNOWN_FIELD,
57
71
  LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
58
72
  MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
59
73
  NAV_OBJECT_UNGRANTED: () => NAV_OBJECT_UNGRANTED,
@@ -67,6 +81,7 @@ __export(index_exports, {
67
81
  REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
68
82
  REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
69
83
  REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
84
+ SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
70
85
  SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
71
86
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
72
87
  SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
@@ -99,6 +114,9 @@ __export(index_exports, {
99
114
  WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
100
115
  buildAccessMatrix: () => buildAccessMatrix,
101
116
  diffAccessMatrix: () => diffAccessMatrix,
117
+ extractHookBodyWriteSet: () => extractHookBodyWriteSet,
118
+ extractHookBodyWrites: () => extractHookBodyWrites,
119
+ validateActionBodyWrites: () => validateActionBodyWrites,
102
120
  validateActionNameRefs: () => validateActionNameRefs,
103
121
  validateAiAgentAuthoring: () => validateAiAgentAuthoring,
104
122
  validateAiSurfaceAffinity: () => validateAiSurfaceAffinity,
@@ -108,9 +126,11 @@ __export(index_exports, {
108
126
  validateChartBindings: () => validateChartBindings,
109
127
  validateDashboardActionRefs: () => validateDashboardActionRefs,
110
128
  validateFilterTokens: () => validateFilterTokens,
129
+ validateFlowNodeWrites: () => validateFlowNodeWrites,
111
130
  validateFlowTemplatePaths: () => validateFlowTemplatePaths,
112
131
  validateFlowTriggerReadiness: () => validateFlowTriggerReadiness,
113
132
  validateFormLayout: () => validateFormLayout,
133
+ validateHookBodyWrites: () => validateHookBodyWrites,
114
134
  validateJsxPages: () => validateJsxPages,
115
135
  validateListViewMode: () => validateListViewMode,
116
136
  validateNavAccess: () => validateNavAccess,
@@ -124,6 +144,7 @@ __export(index_exports, {
124
144
  validateRecordTitle: () => validateRecordTitle,
125
145
  validateReferenceIntegrity: () => validateReferenceIntegrity,
126
146
  validateResponsiveStyles: () => validateResponsiveStyles,
147
+ validateSearchableFields: () => validateSearchableFields,
127
148
  validateSecurityPosture: () => validateSecurityPosture,
128
149
  validateSeedReplaySafety: () => validateSeedReplaySafety,
129
150
  validateSeedStateMachine: () => validateSeedStateMachine,
@@ -137,7 +158,18 @@ __export(index_exports, {
137
158
  module.exports = __toCommonJS(index_exports);
138
159
 
139
160
  // src/validate-widget-bindings.ts
161
+ var import_data2 = require("@objectstack/spec/data");
162
+ var import_ui = require("@objectstack/spec/ui");
163
+
164
+ // src/system-fields.ts
140
165
  var import_data = require("@objectstack/spec/data");
166
+ var import_system = require("@objectstack/spec/system");
167
+ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
168
+ ...import_data.FIELD_GROUP_SYSTEM_FIELDS,
169
+ ...Object.values(import_system.SystemFieldName)
170
+ ]);
171
+
172
+ // src/validate-widget-bindings.ts
141
173
  var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
142
174
  var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
143
175
  var WIDGET_MEASURE_UNKNOWN = "widget-measure-unknown";
@@ -168,20 +200,18 @@ function asArray(v) {
168
200
  function asStrings(v) {
169
201
  return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
170
202
  }
171
- var CHART_TYPES = /* @__PURE__ */ new Set([
172
- "bar",
173
- "horizontal-bar",
174
- "column",
175
- "line",
176
- "area",
177
- "pie",
178
- "donut",
179
- "funnel",
180
- "scatter",
181
- "treemap",
182
- "sankey",
183
- "radar"
203
+ var MEASURE_EXEMPT_CHART_TYPES = /* @__PURE__ */ new Set([
204
+ "gauge",
205
+ "solid-gauge",
206
+ "metric",
207
+ "kpi",
208
+ "bullet",
209
+ "table",
210
+ "pivot"
184
211
  ]);
212
+ var CHART_TYPES = new Set(
213
+ import_ui.ChartTypeSchema.options.filter((t) => !MEASURE_EXEMPT_CHART_TYPES.has(t))
214
+ );
185
215
  function levenshtein(a, b) {
186
216
  const m = a.length, n = b.length;
187
217
  let prev = Array.from({ length: n + 1 }, (_, j) => j);
@@ -227,18 +257,6 @@ function list(names) {
227
257
  }
228
258
  var DATE_RANGE_FILTER_NAME = "dateRange";
229
259
  var DATE_RANGE_DEFAULT_FIELD = "created_at";
230
- var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
231
- "id",
232
- "created_at",
233
- "created_by",
234
- "updated_at",
235
- "updated_by",
236
- "owner_id",
237
- "organization_id",
238
- "tenant_id",
239
- "user_id",
240
- "deleted_at"
241
- ]);
242
260
  function dashboardFilterDefs(dash) {
243
261
  const byName = /* @__PURE__ */ new Map();
244
262
  const dateRange = dash.dateRange;
@@ -293,7 +311,7 @@ function validateWidgetBindings(stack) {
293
311
  const aggregate = typeof m.aggregate === "string" ? m.aggregate : void 0;
294
312
  if (!field || !aggregate) continue;
295
313
  const ftype = fieldTypes.get(field);
296
- if (ftype && (0, import_data.isIncoherentAggregate)(aggregate, ftype)) {
314
+ if (ftype && (0, import_data2.isIncoherentAggregate)(aggregate, ftype)) {
297
315
  findings.push({
298
316
  severity: "warning",
299
317
  rule: MEASURE_AGGREGATE_INCOHERENT,
@@ -472,6 +490,7 @@ function validateWidgetBindings(stack) {
472
490
 
473
491
  // src/validate-expressions.ts
474
492
  var import_formula = require("@objectstack/formula");
493
+ var import_automation = require("@objectstack/spec/automation");
475
494
  function asArray2(v) {
476
495
  if (Array.isArray(v)) return v;
477
496
  if (v && typeof v === "object") {
@@ -532,37 +551,53 @@ function validateStackExpressions(stack) {
532
551
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
533
552
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
534
553
  };
554
+ const checkDeclaredPredicate = (where, raw) => {
555
+ if (raw == null) return;
556
+ const res = (0, import_formula.validateExpression)("predicate", raw);
557
+ for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
558
+ for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
559
+ };
535
560
  for (const flow of asArray2(stack.flows)) {
536
561
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
537
562
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
538
- const edges = Array.isArray(flow.edges) ? flow.edges : [];
539
563
  const startNode = nodes.find((n) => n.type === "start");
540
564
  const startCfg = startNode?.config ?? {};
541
565
  const objectName = typeof startCfg.objectName === "string" ? startCfg.objectName : void 0;
542
- for (const node of nodes) {
543
- const cfg = node.config ?? {};
544
- check(`flow '${flowName}' \xB7 node '${node.id}' (${node.type}) condition`, cfg.condition, objectName);
545
- if (node.type === "script") {
546
- const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
547
- const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
548
- const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
549
- if (!fn && !action && !inline) {
550
- issues.push({
551
- where: `flow '${flowName}' \xB7 node '${node.id}' (script) callable`,
552
- 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 })\`).`,
553
- source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
554
- });
555
- } else if (action === "invoke_function" && !fn) {
556
- issues.push({
557
- where: `flow '${flowName}' \xB7 node '${node.id}' (script) callable`,
558
- 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 })\`.`,
559
- source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
560
- });
566
+ for (const graph of (0, import_automation.collectFlowGraphs)(flow)) {
567
+ const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
568
+ for (const node of graph.nodes) {
569
+ const cfg = node.config ?? {};
570
+ check(`${at} \xB7 node '${node.id}' (${node.type}) condition`, cfg.condition, objectName);
571
+ const nodeType = typeof node.type === "string" ? node.type : "";
572
+ for (const found of (0, import_automation.resolveFlowNodeExpressions)(nodeType, cfg)) {
573
+ if (found.entry.role !== "predicate") continue;
574
+ checkDeclaredPredicate(
575
+ `${at} \xB7 node '${node.id}' (${nodeType}) ${found.entry.label} at config.${found.path}`,
576
+ found.value
577
+ );
578
+ }
579
+ if (node.type === "script") {
580
+ const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
581
+ const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
582
+ const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
583
+ if (!fn && !action && !inline) {
584
+ issues.push({
585
+ where: `${at} \xB7 node '${node.id}' (script) callable`,
586
+ 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 })\`).`,
587
+ source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
588
+ });
589
+ } else if (action === "invoke_function" && !fn) {
590
+ issues.push({
591
+ where: `${at} \xB7 node '${node.id}' (script) callable`,
592
+ 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 })\`.`,
593
+ source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
594
+ });
595
+ }
561
596
  }
562
597
  }
563
- }
564
- for (const edge of edges) {
565
- check(`flow '${flowName}' \xB7 edge '${edge.id}' (${edge.source}\u2192${edge.target}) condition`, edge.condition, objectName);
598
+ for (const edge of graph.edges) {
599
+ check(`${at} \xB7 edge '${edge.id}' (${edge.source}\u2192${edge.target}) condition`, edge.condition, objectName);
600
+ }
566
601
  }
567
602
  }
568
603
  for (const obj of objects) {
@@ -815,6 +850,85 @@ function validateFlowTriggerReadiness(stack) {
815
850
  return findings;
816
851
  }
817
852
 
853
+ // src/flow-walk.ts
854
+ var import_automation2 = require("@objectstack/spec/automation");
855
+ function isRec(v) {
856
+ return !!v && typeof v === "object" && !Array.isArray(v);
857
+ }
858
+ function strName(v) {
859
+ return typeof v === "string" && v.length > 0 ? v : void 0;
860
+ }
861
+ var REGION_SLOTS = new Map(
862
+ [...import_automation2.FLOW_REGION_SLOTS_BY_TYPE].map(([type, slots]) => [type, slots.map((s) => s.key)])
863
+ );
864
+ var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
865
+ var MAX_REGION_DEPTH = 16;
866
+ function flowNodeLabel(node, index) {
867
+ return strName(node.label) ?? strName(node.id) ?? `#${index}`;
868
+ }
869
+ function stripRegions(config) {
870
+ if (!isRec(config)) return void 0;
871
+ let out;
872
+ for (const key of Object.keys(config)) {
873
+ if (!REGION_CONFIG_KEYS.has(key)) continue;
874
+ out ?? (out = { ...config });
875
+ delete out[key];
876
+ }
877
+ return out ?? config;
878
+ }
879
+ function walkFlowNodes(flow, flowPath) {
880
+ const out = [];
881
+ if (!isRec(flow)) return out;
882
+ const visitList = (nodes, basePath, trail, depth) => {
883
+ if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
884
+ nodes.forEach((raw, index) => {
885
+ if (!isRec(raw)) return;
886
+ const path = `${basePath}[${index}]`;
887
+ out.push({
888
+ node: raw,
889
+ path,
890
+ localConfig: stripRegions(raw.config),
891
+ regionTrail: trail,
892
+ depth
893
+ });
894
+ const type = strName(raw.type);
895
+ const slots = type ? REGION_SLOTS.get(type) : void 0;
896
+ if (!slots || !isRec(raw.config)) return;
897
+ const config = raw.config;
898
+ const here = `${type} "${flowNodeLabel(raw, index)}"`;
899
+ for (const slot of slots) {
900
+ const value = config[slot];
901
+ if (slot === "branches") {
902
+ if (!Array.isArray(value)) continue;
903
+ value.forEach((branch, b) => {
904
+ if (!isRec(branch)) return;
905
+ const branchName = strName(branch.name) ?? `#${b}`;
906
+ visitList(
907
+ branch.nodes,
908
+ `${path}.config.branches[${b}].nodes`,
909
+ joinTrail(trail, `${here} \u203A branch ${branchName}`),
910
+ depth + 1
911
+ );
912
+ });
913
+ continue;
914
+ }
915
+ if (!isRec(value)) continue;
916
+ visitList(
917
+ value.nodes,
918
+ `${path}.config.${slot}.nodes`,
919
+ joinTrail(trail, `${here} \u203A ${slot}`),
920
+ depth + 1
921
+ );
922
+ }
923
+ });
924
+ };
925
+ visitList(flow.nodes, `${flowPath}.nodes`, "", 0);
926
+ return out;
927
+ }
928
+ function joinTrail(trail, segment) {
929
+ return trail ? `${trail} \u203A ${segment}` : segment;
930
+ }
931
+
818
932
  // src/validate-flow-template-paths.ts
819
933
  var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
820
934
  var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
@@ -828,19 +942,10 @@ function asArray5(v) {
828
942
  }
829
943
  return [];
830
944
  }
831
- var SYSTEM_FIELDS2 = /* @__PURE__ */ new Set([
832
- "id",
945
+ var IMPLICIT_HEADS = /* @__PURE__ */ new Set([
946
+ ...SYSTEM_FIELDS,
833
947
  "name",
834
948
  "owner",
835
- "owner_id",
836
- "created_at",
837
- "created_by",
838
- "updated_at",
839
- "updated_by",
840
- "organization_id",
841
- "tenant_id",
842
- "is_deleted",
843
- "deleted_at",
844
949
  "record_type"
845
950
  ]);
846
951
  var RELATION_TYPES = /* @__PURE__ */ new Set([
@@ -970,12 +1075,13 @@ function validateFlowTemplatePaths(stack) {
970
1075
  if (!obj) return;
971
1076
  const fieldTypes = fieldTypesOf(obj);
972
1077
  const expandSet = declaredExpandOf(flow);
973
- nodes.forEach((node, nodeIndex) => {
974
- if (typeof node !== "object" || !node) return;
975
- const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${nodeIndex}`;
1078
+ walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
1079
+ const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
1080
+ const where = regionTrail ? `flow "${flowName}" ${regionTrail} node "${nodeLabel}"` : `flow "${flowName}" node "${nodeLabel}"`;
976
1081
  const nodeType = typeof node.type === "string" ? node.type : "";
977
1082
  const guarded = FILTER_GUARDED_NODE_TYPES.has(nodeType);
978
- const leaves = collectNodeLeaves(node, guarded);
1083
+ const scanNode = localConfig !== void 0 && localConfig !== node.config ? { ...node, config: localConfig } : node;
1084
+ const leaves = collectNodeLeaves(scanNode, guarded);
979
1085
  if (leaves.length === 0) return;
980
1086
  const seenUnknown = /* @__PURE__ */ new Set();
981
1087
  const seenTraversal = /* @__PURE__ */ new Set();
@@ -985,15 +1091,15 @@ function validateFlowTemplatePaths(stack) {
985
1091
  const head = rest[0];
986
1092
  const hasSubPath = rest.length > 1;
987
1093
  const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
988
- const isKnown = fieldTypes.has(head) || SYSTEM_FIELDS2.has(head);
1094
+ const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
989
1095
  if (!isKnown) {
990
1096
  if (seenUnknown.has(head)) continue;
991
1097
  seenUnknown.add(head);
992
1098
  findings.push({
993
1099
  severity: inFilter ? "error" : "warning",
994
1100
  rule: FLOW_TEMPLATE_UNKNOWN_FIELD,
995
- where: `flow "${flowName}" node "${nodeLabel}"`,
996
- path: `flows[${flowIndex}].nodes[${nodeIndex}]`,
1101
+ where,
1102
+ path: nodePath,
997
1103
  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).`,
998
1104
  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.`
999
1105
  });
@@ -1008,8 +1114,8 @@ function validateFlowTemplatePaths(stack) {
1008
1114
  findings.push({
1009
1115
  severity: inFilter ? "error" : "warning",
1010
1116
  rule: FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
1011
- where: `flow "${flowName}" node "${nodeLabel}"`,
1012
- path: `flows[${flowIndex}].nodes[${nodeIndex}]`,
1117
+ where,
1118
+ path: nodePath,
1013
1119
  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).`,
1014
1120
  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}'.`
1015
1121
  });
@@ -1073,8 +1179,8 @@ function validateReadonlyFlowWrites(stack) {
1073
1179
  if (flow.runAs === "system") return;
1074
1180
  const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
1075
1181
  const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
1076
- const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
1077
- nodes.forEach((node, nodeIndex) => {
1182
+ const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
1183
+ walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
1078
1184
  if (node?.type !== "update_record") return;
1079
1185
  const config = node.config ?? {};
1080
1186
  const objectName = readLiteralObjectName(config);
@@ -1083,7 +1189,8 @@ function validateReadonlyFlowWrites(stack) {
1083
1189
  if (!fieldMap) return;
1084
1190
  const fields = config.fields;
1085
1191
  if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
1086
- const nodeName = typeof node.label === "string" && node.label ? node.label : typeof node.id === "string" && node.id ? node.id : `#${nodeIndex}`;
1192
+ const nodeName = flowNodeLabel(node, walkIndex);
1193
+ const where = regionTrail ? `flow "${flowName}" \u203A ${regionTrail} \u203A node "${nodeName}"` : `flow "${flowName}" \u203A node "${nodeName}"`;
1087
1194
  for (const fieldName of Object.keys(fields)) {
1088
1195
  const meta = fieldMap.get(fieldName);
1089
1196
  if (!meta) continue;
@@ -1091,8 +1198,8 @@ function validateReadonlyFlowWrites(stack) {
1091
1198
  findings.push({
1092
1199
  severity: "error",
1093
1200
  rule: FLOW_UPDATE_READONLY_FIELD,
1094
- where: `flow "${flowName}" \u203A node "${nodeName}"`,
1095
- path: `flows[${flowIndex}].nodes[${nodeIndex}].config.fields.${fieldName}`,
1201
+ where,
1202
+ path: `${nodePath}.config.fields.${fieldName}`,
1096
1203
  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.`,
1097
1204
  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.`
1098
1205
  });
@@ -1100,8 +1207,8 @@ function validateReadonlyFlowWrites(stack) {
1100
1207
  findings.push({
1101
1208
  severity: "warning",
1102
1209
  rule: FLOW_UPDATE_READONLY_WHEN_FIELD,
1103
- where: `flow "${flowName}" \u203A node "${nodeName}"`,
1104
- path: `flows[${flowIndex}].nodes[${nodeIndex}].config.fields.${fieldName}`,
1210
+ where,
1211
+ path: `${nodePath}.config.fields.${fieldName}`,
1105
1212
  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.`,
1106
1213
  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.`
1107
1214
  });
@@ -1553,208 +1660,586 @@ function validateReactPages(stack) {
1553
1660
 
1554
1661
  // src/validate-react-page-props.ts
1555
1662
  var import_node_module2 = require("module");
1556
- var import_ui = require("@objectstack/spec/ui");
1557
- var import_meta2 = {};
1558
- var cachedTs = null;
1559
- function loadTypeScript() {
1560
- if (cachedTs) return cachedTs;
1561
- const anchor = typeof import_meta2 !== "undefined" && import_meta2.url ? import_meta2.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
1562
- try {
1563
- cachedTs = (0, import_node_module2.createRequire)(anchor)("typescript");
1564
- } catch (err) {
1565
- throw new Error(
1566
- `@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.`
1567
- );
1663
+ var import_ui2 = require("@objectstack/spec/ui");
1664
+ var import_data3 = require("@objectstack/spec/data");
1665
+
1666
+ // src/validate-searchable-fields.ts
1667
+ var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1668
+ function asArray10(v) {
1669
+ if (Array.isArray(v)) return v;
1670
+ if (v && typeof v === "object") {
1671
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1568
1672
  }
1569
- return cachedTs;
1673
+ return [];
1570
1674
  }
1571
- var asArray10 = (v) => Array.isArray(v) ? v : [];
1572
- var BLOCKS = new Map(
1573
- import_ui.REACT_BLOCKS.map((b) => [
1574
- b.tag,
1575
- {
1576
- requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
1577
- knownProps: new Set(b.interactions.map((i) => i.name))
1578
- }
1579
- ])
1580
- );
1581
- function editDistance(a, b, cap = 2) {
1582
- if (Math.abs(a.length - b.length) > cap) return cap + 1;
1583
- const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
1584
- for (let j = 1; j <= b.length; j++) {
1585
- let prev = dp[0];
1586
- dp[0] = j;
1587
- for (let i = 1; i <= a.length; i++) {
1588
- const tmp = dp[i];
1589
- dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1590
- prev = tmp;
1591
- }
1592
- }
1593
- return dp[a.length];
1675
+ function isRec2(v) {
1676
+ return !!v && typeof v === "object" && !Array.isArray(v);
1594
1677
  }
1595
- function nearestKnown(prop, known) {
1596
- if (known.has(prop)) return null;
1597
- let best = null;
1598
- let bestD = 3;
1599
- for (const k of known) {
1600
- const d = editDistance(prop, k);
1601
- if (d < bestD) {
1602
- bestD = d;
1603
- best = k;
1604
- }
1678
+ function strName2(v) {
1679
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1680
+ }
1681
+ function declaredFieldNames(obj) {
1682
+ const fields = obj.fields;
1683
+ if (!fields || typeof fields !== "object") return null;
1684
+ const names = /* @__PURE__ */ new Set();
1685
+ for (const f of asArray10(fields)) {
1686
+ const n = strName2(f.name);
1687
+ if (n) names.add(n);
1605
1688
  }
1606
- return bestD <= 2 ? best : null;
1689
+ return names.size > 0 ? names : null;
1607
1690
  }
1608
- var NOT_STATIC = /* @__PURE__ */ Symbol("not-static");
1609
- function staticValue(tsc, sf, node) {
1610
- if (!node) return NOT_STATIC;
1611
- if (tsc.isParenthesizedExpression(node)) return staticValue(tsc, sf, node.expression);
1612
- if (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) return node.text;
1613
- if (tsc.isNumericLiteral(node)) return Number(node.text);
1614
- if (node.kind === tsc.SyntaxKind.TrueKeyword) return true;
1615
- if (node.kind === tsc.SyntaxKind.FalseKeyword) return false;
1616
- if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
1617
- if (tsc.isArrayLiteralExpression(node)) {
1618
- const out = [];
1619
- for (const el of node.elements) {
1620
- const v = staticValue(tsc, sf, el);
1621
- if (v === NOT_STATIC) return NOT_STATIC;
1622
- out.push(v);
1691
+ function suggest2(target, known) {
1692
+ let best;
1693
+ let bestScore = Infinity;
1694
+ for (const candidate of known) {
1695
+ const d = distance(target, candidate);
1696
+ if (d < bestScore) {
1697
+ bestScore = d;
1698
+ best = candidate;
1623
1699
  }
1624
- return out;
1625
1700
  }
1626
- if (tsc.isObjectLiteralExpression(node)) {
1627
- const out = {};
1628
- for (const p of node.properties) {
1629
- if (!tsc.isPropertyAssignment(p)) return NOT_STATIC;
1630
- const key = tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name) ? p.name.text : null;
1631
- if (key === null) return NOT_STATIC;
1632
- const v = staticValue(tsc, sf, p.initializer);
1633
- if (v === NOT_STATIC) return NOT_STATIC;
1634
- out[key] = v;
1701
+ const limit = Math.max(2, Math.floor(target.length / 3));
1702
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
1703
+ }
1704
+ function distance(a, b) {
1705
+ const m = a.length;
1706
+ const n = b.length;
1707
+ if (m === 0) return n;
1708
+ if (n === 0) return m;
1709
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
1710
+ for (let i = 1; i <= m; i++) {
1711
+ const curr = [i, ...new Array(n).fill(0)];
1712
+ for (let j = 1; j <= n; j++) {
1713
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1714
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
1635
1715
  }
1636
- return out;
1716
+ prev = curr;
1637
1717
  }
1638
- return NOT_STATIC;
1639
- }
1640
- function attrValue(tsc, sf, attr) {
1641
- const init = attr.initializer;
1642
- if (!init) return true;
1643
- if (tsc.isStringLiteral(init)) return init.text;
1644
- if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
1645
- return NOT_STATIC;
1718
+ return prev[n];
1646
1719
  }
1647
- var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
1648
- var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
1649
- var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
1650
- var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
1651
- var SYSTEM_FIELDS3 = /* @__PURE__ */ new Set([
1652
- "id",
1653
- "created_at",
1654
- "created_by",
1655
- "updated_at",
1656
- "updated_by",
1657
- "owner_id",
1658
- "organization_id",
1659
- "tenant_id",
1660
- "user_id",
1661
- "deleted_at"
1662
- ]);
1663
- function namedArray(v) {
1664
- if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
1665
- if (v && typeof v === "object") {
1666
- return Object.entries(v).map(([name, def]) => ({
1667
- name,
1668
- ...def && typeof def === "object" ? def : {}
1669
- }));
1720
+ function indexObjectSearchTargets(stack) {
1721
+ const fieldsByObject = /* @__PURE__ */ new Map();
1722
+ if (!isRec2(stack)) return fieldsByObject;
1723
+ for (const obj of asArray10(stack.objects)) {
1724
+ const name = strName2(obj.name);
1725
+ if (name) fieldsByObject.set(name, declaredFieldNames(obj));
1670
1726
  }
1671
- return [];
1727
+ return fieldsByObject;
1672
1728
  }
1673
- function indexObjectFields(stack) {
1674
- const out = /* @__PURE__ */ new Map();
1675
- for (const obj of namedArray(stack.objects)) {
1676
- const name = typeof obj.name === "string" ? obj.name : void 0;
1729
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
1730
+ const findings = [];
1731
+ if (!Array.isArray(declared) || declared.length === 0) return findings;
1732
+ if (!objectName) return findings;
1733
+ if (!fieldsByObject.has(objectName)) return findings;
1734
+ const known = fieldsByObject.get(objectName);
1735
+ if (!known) return findings;
1736
+ for (let i = 0; i < declared.length; i++) {
1737
+ const entry = declared[i];
1738
+ const name = strName2(entry);
1677
1739
  if (!name) continue;
1678
- const names = /* @__PURE__ */ new Set();
1679
- for (const f of namedArray(obj.fields)) {
1680
- if (typeof f.name === "string" && f.name) names.add(f.name);
1681
- }
1682
- out.set(name, names);
1740
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) continue;
1741
+ const dotted = name.includes(".");
1742
+ findings.push({
1743
+ severity: "error",
1744
+ rule: SEARCHABLE_FIELD_UNKNOWN,
1745
+ where,
1746
+ path: `${path}[${i}]`,
1747
+ 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)),
1748
+ 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(", ")}.` : "")
1749
+ });
1683
1750
  }
1684
- return out;
1751
+ return findings;
1685
1752
  }
1686
- var isRec = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1687
- var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
1688
- function checkObjectChart(attrs, objectFields, findings) {
1689
- const { values, where, path } = attrs;
1690
- const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
1691
- if (values.has("data")) return;
1692
- const aggregate = values.get("aggregate");
1693
- if (aggregate === void 0 || aggregate === NOT_STATIC) return;
1694
- if (!isRec(aggregate)) return;
1695
- const fn = strOf(aggregate.function);
1696
- const field = strOf(aggregate.field);
1697
- const groupBy = aggregate.groupBy;
1698
- const groupByField = strOf(groupBy) ?? (isRec(groupBy) ? strOf(groupBy.field) : void 0);
1699
- if (fn && !CHART_FUNCTIONS.includes(fn)) {
1700
- push(
1701
- "error",
1702
- REACT_CHART_AGGREGATE_INVALID,
1703
- `aggregate.function "${fn}" is not an aggregation this chart can run.`,
1704
- `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
1753
+ function validateSearchableFields(stack) {
1754
+ const findings = [];
1755
+ if (!isRec2(stack)) return findings;
1756
+ const objects = asArray10(stack.objects);
1757
+ const fieldsByObject = indexObjectSearchTargets(stack);
1758
+ const check = (declared, objectName, where, path, subject) => {
1759
+ findings.push(
1760
+ ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
1705
1761
  );
1706
- } else if (fn && fn !== "count" && !field) {
1707
- push(
1708
- "error",
1709
- REACT_CHART_AGGREGATE_INVALID,
1710
- `aggregate.function "${fn}" has no "field" to aggregate.`,
1711
- 'Add aggregate.field, or use function "count" (the only one that may omit it).'
1762
+ };
1763
+ for (let oi = 0; oi < objects.length; oi++) {
1764
+ const obj = objects[oi];
1765
+ if (!isRec2(obj)) continue;
1766
+ const objName = strName2(obj.name);
1767
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
1768
+ check(
1769
+ obj.searchableFields,
1770
+ objName,
1771
+ label2,
1772
+ `objects[${oi}].searchableFields`,
1773
+ "searchableFields"
1712
1774
  );
1775
+ if (isRec2(obj.listViews)) {
1776
+ for (const [key, lv] of Object.entries(obj.listViews)) {
1777
+ if (!isRec2(lv)) continue;
1778
+ check(
1779
+ lv.searchableFields,
1780
+ // A built-in list view belongs to its object; an inline `data.object`
1781
+ // may still retarget it (ADR-0047 allows the explicit binding).
1782
+ listViewObject(lv) ?? objName,
1783
+ `${label2} \u203A listViews.${key}`,
1784
+ `objects[${oi}].listViews.${key}.searchableFields`,
1785
+ "list-view searchableFields"
1786
+ );
1787
+ }
1788
+ }
1713
1789
  }
1714
- const objectName = strOf(values.get("objectName"));
1715
- const known = objectName ? objectFields.get(objectName) : void 0;
1716
- if (objectName && known) {
1717
- const fieldRef = (name, prop) => {
1718
- if (!name) return;
1719
- if (name.includes(".")) return;
1720
- if (known.has(name) || SYSTEM_FIELDS3.has(name)) return;
1721
- push(
1722
- "error",
1723
- REACT_CHART_FIELD_UNKNOWN,
1724
- `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.`,
1725
- `Fix the field name, or add "${name}" to ${objectName}.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1790
+ const views = asArray10(stack.views);
1791
+ for (let vi = 0; vi < views.length; vi++) {
1792
+ const view = views[vi];
1793
+ if (!isRec2(view)) continue;
1794
+ const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
1795
+ const viewObject = strName2(view.objectName) ?? strName2(view.object);
1796
+ if (isRec2(view.list)) {
1797
+ check(
1798
+ view.list.searchableFields,
1799
+ listViewObject(view.list) ?? viewObject,
1800
+ `view "${viewLabel}" \u203A list`,
1801
+ `views[${vi}].list.searchableFields`,
1802
+ "list-view searchableFields"
1726
1803
  );
1727
- };
1728
- fieldRef(field, "field");
1729
- fieldRef(groupByField, "groupBy");
1804
+ }
1805
+ if (isRec2(view.listViews)) {
1806
+ for (const [key, lv] of Object.entries(view.listViews)) {
1807
+ if (!isRec2(lv)) continue;
1808
+ check(
1809
+ lv.searchableFields,
1810
+ listViewObject(lv) ?? viewObject,
1811
+ `view "${viewLabel}" \u203A listViews.${key}`,
1812
+ `views[${vi}].listViews.${key}.searchableFields`,
1813
+ "list-view searchableFields"
1814
+ );
1815
+ }
1816
+ }
1730
1817
  }
1731
- const keys = (0, import_ui.chartAggregateResultKeys)({ field, function: fn, groupBy });
1732
- const columns = [keys.category, keys.value].filter((k) => !!k);
1733
- if (columns.length === 0) return;
1734
- const axisRef = (name, prop) => {
1735
- if (!name) return;
1736
- if (columns.includes(name)) return;
1737
- if (keys.comparison && name === keys.comparison) return;
1738
- push(
1739
- "error",
1740
- REACT_CHART_AXIS_UNKNOWN,
1818
+ return findings;
1819
+ }
1820
+ function listViewObject(listView) {
1821
+ const data = listView.data;
1822
+ return isRec2(data) ? strName2(data.object) : void 0;
1823
+ }
1824
+
1825
+ // src/page-walk.ts
1826
+ function isRec3(v) {
1827
+ return !!v && typeof v === "object" && !Array.isArray(v);
1828
+ }
1829
+ function strName3(v) {
1830
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1831
+ }
1832
+ var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
1833
+ function isSourceAuthoredPage(page) {
1834
+ const kind = strName3(page.kind);
1835
+ return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
1836
+ }
1837
+ function walkPageComponents(page, pagePath) {
1838
+ const out = [];
1839
+ if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
1840
+ const pageObject = strName3(page.object);
1841
+ const visit = (node, path, inheritedObject) => {
1842
+ if (!isRec3(node)) return;
1843
+ const props = isRec3(node.properties) ? node.properties : void 0;
1844
+ const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
1845
+ const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
1846
+ out.push({ component: node, path, objectName });
1847
+ if (!props) return;
1848
+ if (Array.isArray(props.items)) {
1849
+ for (let i = 0; i < props.items.length; i++) {
1850
+ const item = props.items[i];
1851
+ if (!isRec3(item) || !Array.isArray(item.children)) continue;
1852
+ for (let c = 0; c < item.children.length; c++) {
1853
+ visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
1854
+ }
1855
+ }
1856
+ }
1857
+ if (Array.isArray(props.children)) {
1858
+ for (let i = 0; i < props.children.length; i++) {
1859
+ visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
1860
+ }
1861
+ }
1862
+ for (const key of ["body", "footer"]) {
1863
+ const slotList = props[key];
1864
+ if (!Array.isArray(slotList)) continue;
1865
+ for (let i = 0; i < slotList.length; i++) {
1866
+ visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
1867
+ }
1868
+ }
1869
+ };
1870
+ const regions = Array.isArray(page.regions) ? page.regions : [];
1871
+ for (let r = 0; r < regions.length; r++) {
1872
+ const region = regions[r];
1873
+ if (!isRec3(region) || !Array.isArray(region.components)) continue;
1874
+ for (let c = 0; c < region.components.length; c++) {
1875
+ visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
1876
+ }
1877
+ }
1878
+ const slots = isRec3(page.slots) ? page.slots : void 0;
1879
+ if (slots) {
1880
+ for (const [slot, value] of Object.entries(slots)) {
1881
+ const list3 = Array.isArray(value) ? value : [value];
1882
+ const indexed = Array.isArray(value);
1883
+ for (let i = 0; i < list3.length; i++) {
1884
+ visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
1885
+ }
1886
+ }
1887
+ }
1888
+ return out;
1889
+ }
1890
+
1891
+ // src/validate-page-field-bindings.ts
1892
+ var PAGE_FIELD_UNKNOWN = "page-field-unknown";
1893
+ function asArray11(v) {
1894
+ if (Array.isArray(v)) return v;
1895
+ if (v && typeof v === "object") {
1896
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1897
+ }
1898
+ return [];
1899
+ }
1900
+ function strName4(v) {
1901
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1902
+ }
1903
+ function isRec4(v) {
1904
+ return !!v && typeof v === "object" && !Array.isArray(v);
1905
+ }
1906
+ function fieldRefsFrom(value, basePath) {
1907
+ const out = [];
1908
+ const one = (v, path) => {
1909
+ const bare = strName4(v);
1910
+ if (bare) {
1911
+ out.push({ name: bare, path });
1912
+ return;
1913
+ }
1914
+ if (!isRec4(v)) return;
1915
+ const named = strName4(v.field) ?? strName4(v.name);
1916
+ if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
1917
+ };
1918
+ if (Array.isArray(value)) {
1919
+ for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
1920
+ } else {
1921
+ one(value, basePath);
1922
+ }
1923
+ return out;
1924
+ }
1925
+ function sortFieldRefs(value, basePath) {
1926
+ if (typeof value === "string") {
1927
+ const head = value.trim().split(/\s+/)[0];
1928
+ return head ? [{ name: head, path: basePath }] : [];
1929
+ }
1930
+ return fieldRefsFrom(value, basePath);
1931
+ }
1932
+ var COMPONENT_FIELD_SPECS = {
1933
+ "record:highlights": { props: ["fields"] },
1934
+ // `sections`/`hideFields` are not in RecordDetailsProps, but every real page
1935
+ // authors them (they survive because `properties` is unvalidated).
1936
+ "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
1937
+ "record:path": { props: ["statusField"] },
1938
+ "element:number": { props: ["field"] },
1939
+ "element:filter": { props: ["fields"] },
1940
+ "element:form": { props: ["fields"] },
1941
+ // The schema says `displayField`; real pages author `labelField`. Accept both.
1942
+ "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
1943
+ };
1944
+ var RELATED_LIST_TYPE = "record:related_list";
1945
+ function componentFieldRefs(type, props, basePath, sep = ".") {
1946
+ const spec = COMPONENT_FIELD_SPECS[type];
1947
+ if (!spec) return null;
1948
+ const refs = [];
1949
+ for (const key of spec.props ?? []) {
1950
+ refs.push(...fieldRefsFrom(props[key], `${basePath}${sep}${key}`));
1951
+ }
1952
+ for (const key of spec.nestedSections ?? []) {
1953
+ const sections = Array.isArray(props[key]) ? props[key] : [];
1954
+ for (let si = 0; si < sections.length; si++) {
1955
+ const section = sections[si];
1956
+ if (!isRec4(section)) continue;
1957
+ refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
1958
+ }
1959
+ }
1960
+ return refs;
1961
+ }
1962
+ function relatedListFieldRefs(props, basePath, sep = ".") {
1963
+ const add = isRec4(props.add) ? props.add : void 0;
1964
+ const picker = add && isRec4(add.picker) ? add.picker : void 0;
1965
+ const at = (key) => `${basePath}${sep}${key}`;
1966
+ return {
1967
+ relatedObject: strName4(props.objectName),
1968
+ related: [
1969
+ ...fieldRefsFrom(props.columns, at("columns")),
1970
+ ...sortFieldRefs(props.sort, at("sort")),
1971
+ ...fieldRefsFrom(props.filter, at("filter")),
1972
+ ...fieldRefsFrom(props.relationshipField, at("relationshipField")),
1973
+ ...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
1974
+ ],
1975
+ parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
1976
+ pickerObject: picker ? strName4(picker.object) : void 0,
1977
+ picker: picker ? [
1978
+ ...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
1979
+ ...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
1980
+ ] : []
1981
+ };
1982
+ }
1983
+ function indexObjectFields(stack) {
1984
+ const objectFields = /* @__PURE__ */ new Map();
1985
+ if (!isRec4(stack)) return objectFields;
1986
+ for (const obj of asArray11(stack.objects)) {
1987
+ const name = strName4(obj.name);
1988
+ if (!name) continue;
1989
+ const names = /* @__PURE__ */ new Set();
1990
+ for (const f of asArray11(obj.fields)) {
1991
+ const fn = strName4(f.name);
1992
+ if (fn) names.add(fn);
1993
+ }
1994
+ objectFields.set(name, names);
1995
+ }
1996
+ return objectFields;
1997
+ }
1998
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence = "skipped") {
1999
+ const findings = [];
2000
+ if (!objectName) return findings;
2001
+ const known = objectFields.get(objectName);
2002
+ if (!known) return findings;
2003
+ for (const ref of refs) {
2004
+ if (ref.name.includes(".")) continue;
2005
+ if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
2006
+ findings.push({
2007
+ severity: consequence === "queried" ? "error" : "warning",
2008
+ rule: PAGE_FIELD_UNKNOWN,
2009
+ where,
2010
+ path: ref.path,
2011
+ 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."),
2012
+ 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(", ")}.` : "")
2013
+ });
2014
+ }
2015
+ return findings;
2016
+ }
2017
+ function validatePageFieldBindings(stack) {
2018
+ const findings = [];
2019
+ if (!stack || typeof stack !== "object") return findings;
2020
+ const objectFields = indexObjectFields(stack);
2021
+ const pages = asArray11(stack.pages);
2022
+ for (let pi = 0; pi < pages.length; pi++) {
2023
+ const page = pages[pi];
2024
+ if (!page || typeof page !== "object") continue;
2025
+ const pageName = strName4(page.name) ?? `#${pi}`;
2026
+ const checkRefs = (refs, objectName, where) => {
2027
+ findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
2028
+ };
2029
+ for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2030
+ const type = strName4(component.type);
2031
+ const props = isRec4(component.properties) ? component.properties : void 0;
2032
+ if (!type || !props) continue;
2033
+ const where = `page "${pageName}" \xB7 ${type}`;
2034
+ const base = `${path}.properties`;
2035
+ if (type === RELATED_LIST_TYPE) {
2036
+ const split = relatedListFieldRefs(props, base);
2037
+ checkRefs(split.related, split.relatedObject, where);
2038
+ checkRefs(split.parent, objectName, where);
2039
+ checkRefs(split.picker, split.pickerObject, where);
2040
+ continue;
2041
+ }
2042
+ const refs = componentFieldRefs(type, props, base);
2043
+ if (!refs) continue;
2044
+ checkRefs(refs, objectName, where);
2045
+ }
2046
+ const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
2047
+ if (cfg) {
2048
+ const cfgObject = strName4(cfg.source) ?? strName4(page.object);
2049
+ const base = `pages[${pi}].interfaceConfig`;
2050
+ const refs = [
2051
+ ...fieldRefsFrom(cfg.columns, `${base}.columns`),
2052
+ ...sortFieldRefs(cfg.sort, `${base}.sort`),
2053
+ ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2054
+ ];
2055
+ const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
2056
+ if (userFilters) {
2057
+ refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2058
+ }
2059
+ checkRefs(refs, cfgObject, `page "${pageName}" \xB7 interfaceConfig`);
2060
+ }
2061
+ }
2062
+ return findings;
2063
+ }
2064
+
2065
+ // src/validate-react-page-props.ts
2066
+ var import_meta2 = {};
2067
+ var cachedTs = null;
2068
+ function loadTypeScript() {
2069
+ if (cachedTs) return cachedTs;
2070
+ const anchor = typeof import_meta2 !== "undefined" && import_meta2.url ? import_meta2.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
2071
+ try {
2072
+ cachedTs = (0, import_node_module2.createRequire)(anchor)("typescript");
2073
+ } catch (err) {
2074
+ throw new Error(
2075
+ `@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.`
2076
+ );
2077
+ }
2078
+ return cachedTs;
2079
+ }
2080
+ var asArray12 = (v) => Array.isArray(v) ? v : [];
2081
+ var BLOCKS = new Map(
2082
+ import_ui2.REACT_BLOCKS.map((b) => [
2083
+ b.tag,
2084
+ {
2085
+ requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
2086
+ knownProps: new Set(b.interactions.map((i) => i.name))
2087
+ }
2088
+ ])
2089
+ );
2090
+ function editDistance(a, b, cap = 2) {
2091
+ if (Math.abs(a.length - b.length) > cap) return cap + 1;
2092
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
2093
+ for (let j = 1; j <= b.length; j++) {
2094
+ let prev = dp[0];
2095
+ dp[0] = j;
2096
+ for (let i = 1; i <= a.length; i++) {
2097
+ const tmp = dp[i];
2098
+ dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
2099
+ prev = tmp;
2100
+ }
2101
+ }
2102
+ return dp[a.length];
2103
+ }
2104
+ function nearestKnown(prop, known) {
2105
+ if (known.has(prop)) return null;
2106
+ let best = null;
2107
+ let bestD = 3;
2108
+ for (const k of known) {
2109
+ const d = editDistance(prop, k);
2110
+ if (d < bestD) {
2111
+ bestD = d;
2112
+ best = k;
2113
+ }
2114
+ }
2115
+ return bestD <= 2 ? best : null;
2116
+ }
2117
+ var NOT_STATIC = /* @__PURE__ */ Symbol("not-static");
2118
+ function staticValue(tsc, sf, node) {
2119
+ if (!node) return NOT_STATIC;
2120
+ if (tsc.isParenthesizedExpression(node)) return staticValue(tsc, sf, node.expression);
2121
+ if (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) return node.text;
2122
+ if (tsc.isNumericLiteral(node)) return Number(node.text);
2123
+ if (node.kind === tsc.SyntaxKind.TrueKeyword) return true;
2124
+ if (node.kind === tsc.SyntaxKind.FalseKeyword) return false;
2125
+ if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
2126
+ if (tsc.isArrayLiteralExpression(node)) {
2127
+ const out = [];
2128
+ for (const el of node.elements) {
2129
+ const v = staticValue(tsc, sf, el);
2130
+ if (v === NOT_STATIC) return NOT_STATIC;
2131
+ out.push(v);
2132
+ }
2133
+ return out;
2134
+ }
2135
+ if (tsc.isObjectLiteralExpression(node)) {
2136
+ const out = {};
2137
+ for (const p of node.properties) {
2138
+ if (!tsc.isPropertyAssignment(p)) return NOT_STATIC;
2139
+ const key = tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name) ? p.name.text : null;
2140
+ if (key === null) return NOT_STATIC;
2141
+ const v = staticValue(tsc, sf, p.initializer);
2142
+ if (v === NOT_STATIC) return NOT_STATIC;
2143
+ out[key] = v;
2144
+ }
2145
+ return out;
2146
+ }
2147
+ return NOT_STATIC;
2148
+ }
2149
+ function attrValue(tsc, sf, attr) {
2150
+ const init = attr.initializer;
2151
+ if (!init) return true;
2152
+ if (tsc.isStringLiteral(init)) return init.text;
2153
+ if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
2154
+ return NOT_STATIC;
2155
+ }
2156
+ function filterAttrValue(tsc, sf, attr) {
2157
+ const init = attr.initializer;
2158
+ if (!init || !tsc.isJsxExpression(init)) return NOT_STATIC;
2159
+ const perPosition = (node) => {
2160
+ if (!node) return NOT_STATIC;
2161
+ if (tsc.isParenthesizedExpression(node)) return perPosition(node.expression);
2162
+ if (tsc.isArrayLiteralExpression(node)) return node.elements.map((el) => perPosition(el));
2163
+ return staticValue(tsc, sf, node);
2164
+ };
2165
+ return perPosition(init.expression);
2166
+ }
2167
+ var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
2168
+ var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
2169
+ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
2170
+ var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
2171
+ var isRec5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2172
+ var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
2173
+ function checkObjectChart(attrs, objectFields, findings) {
2174
+ const { values, where, path } = attrs;
2175
+ const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2176
+ if (values.has("data")) return;
2177
+ const aggregate = values.get("aggregate");
2178
+ if (aggregate === void 0 || aggregate === NOT_STATIC) return;
2179
+ if (!isRec5(aggregate)) return;
2180
+ const fn = strOf(aggregate.function);
2181
+ const field = strOf(aggregate.field);
2182
+ const groupBy = aggregate.groupBy;
2183
+ const groupByField = strOf(groupBy) ?? (isRec5(groupBy) ? strOf(groupBy.field) : void 0);
2184
+ if (fn && !CHART_FUNCTIONS.includes(fn)) {
2185
+ push(
2186
+ "error",
2187
+ REACT_CHART_AGGREGATE_INVALID,
2188
+ `aggregate.function "${fn}" is not an aggregation this chart can run.`,
2189
+ `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
2190
+ );
2191
+ } else if (fn && fn !== "count" && !field) {
2192
+ push(
2193
+ "error",
2194
+ REACT_CHART_AGGREGATE_INVALID,
2195
+ `aggregate.function "${fn}" has no "field" to aggregate.`,
2196
+ 'Add aggregate.field, or use function "count" (the only one that may omit it).'
2197
+ );
2198
+ }
2199
+ const objectName = strOf(values.get("objectName"));
2200
+ const known = objectName ? objectFields.get(objectName) : void 0;
2201
+ if (objectName && known) {
2202
+ const fieldRef = (name, prop) => {
2203
+ if (!name) return;
2204
+ if (name.includes(".")) return;
2205
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
2206
+ push(
2207
+ "error",
2208
+ REACT_CHART_FIELD_UNKNOWN,
2209
+ `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.`,
2210
+ `Fix the field name, or add "${name}" to ${objectName}.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
2211
+ );
2212
+ };
2213
+ fieldRef(field, "field");
2214
+ fieldRef(groupByField, "groupBy");
2215
+ }
2216
+ const keys = (0, import_ui2.chartAggregateResultKeys)({ field, function: fn, groupBy });
2217
+ const columns = [keys.category, keys.value].filter((k) => !!k);
2218
+ if (columns.length === 0) return;
2219
+ const axisRef = (name, prop) => {
2220
+ if (!name) return;
2221
+ if (columns.includes(name)) return;
2222
+ if (keys.comparison && name === keys.comparison) return;
2223
+ push(
2224
+ "error",
2225
+ REACT_CHART_AXIS_UNKNOWN,
1741
2226
  `"${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).`,
1742
2227
  `Result columns: ${columns.join(", ")}` + (keys.comparison ? ` (plus "${keys.comparison}" with a comparison overlay)` : "") + `. Bind ${prop} to one of them.`
1743
2228
  );
1744
2229
  };
1745
2230
  const xAxisRaw = values.get("xAxis");
1746
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2231
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec5(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
1747
2232
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
1748
2233
  axisRef(categoryAxis, categoryProp);
1749
2234
  const yAxisRaw = values.get("yAxis");
1750
2235
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
1751
2236
  for (const a of yAxisList) {
1752
- axisRef(strOf(a) ?? (isRec(a) ? strOf(a.field) : void 0), "yAxis[].field");
2237
+ axisRef(strOf(a) ?? (isRec5(a) ? strOf(a.field) : void 0), "yAxis[].field");
1753
2238
  }
1754
2239
  const series = values.get("series");
1755
2240
  if (Array.isArray(series)) {
1756
2241
  for (const s of series) {
1757
- if (!isRec(s)) continue;
2242
+ if (!isRec5(s)) continue;
1758
2243
  const dataKey = strOf(s.dataKey);
1759
2244
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
1760
2245
  }
@@ -1768,10 +2253,156 @@ function checkObjectChart(attrs, objectFields, findings) {
1768
2253
  );
1769
2254
  }
1770
2255
  }
2256
+ var REACT_FIELD_SPECS = {
2257
+ ListView: {
2258
+ // `fields` is the React overlay's "limit/order the columns"; `columns` the
2259
+ // spec ListView prop. Both name columns on the bound object, and a page may
2260
+ // write either. `hiddenFields`/`fieldOrder`/`filterableFields` are schema
2261
+ // props outside the curated contract — unadvertised but honored by the
2262
+ // renderer, so a stale name there is drift just the same.
2263
+ fields: ["fields", "columns", "hiddenFields", "fieldOrder", "filterableFields"],
2264
+ sorts: ["sort"],
2265
+ nestedFields: ["userFilters", "grouping"],
2266
+ filterArrays: ["filters"]
2267
+ },
2268
+ ObjectForm: {
2269
+ fields: ["fields"],
2270
+ keyedByField: ["initialValues"],
2271
+ // `groups` is FormViewSchema's legacy alias for `sections`.
2272
+ sections: ["sections", "groups"]
2273
+ },
2274
+ ObjectChart: {
2275
+ // The axes are result columns (checkObjectChart owns them); `filter` is an
2276
+ // ordinary ObjectQL predicate over the bound object, like ListView's.
2277
+ filterArrays: ["filter"]
2278
+ }
2279
+ };
2280
+ var PATH_SEP = " \u203A ";
2281
+ var SCHEMA_TYPE_BY_TAG = new Map(
2282
+ import_ui2.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
2283
+ );
2284
+ var FILTER_PROPS = new Set(
2285
+ Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
2286
+ );
2287
+ function readableProps(values) {
2288
+ const out = {};
2289
+ for (const [k, v] of values) if (v !== NOT_STATIC) out[k] = v;
2290
+ return out;
2291
+ }
2292
+ function subformFieldRefs(value, basePath) {
2293
+ const child = [];
2294
+ const parent = [];
2295
+ if (!Array.isArray(value)) return { child, parent };
2296
+ for (let i = 0; i < value.length; i++) {
2297
+ const sub = value[i];
2298
+ if (!isRec5(sub)) continue;
2299
+ const at = (key) => `${basePath}[${i}].${key}`;
2300
+ child.push({
2301
+ objectName: strOf(sub.childObject),
2302
+ refs: [
2303
+ ...fieldRefsFrom(sub.columns, at("columns")),
2304
+ ...fieldRefsFrom(sub.relationshipField, at("relationshipField")),
2305
+ ...fieldRefsFrom(sub.amountField, at("amountField"))
2306
+ ]
2307
+ });
2308
+ parent.push(...fieldRefsFrom(sub.totalField, at("totalField")));
2309
+ }
2310
+ return { child, parent };
2311
+ }
2312
+ function filterFieldRefs(node, basePath, out) {
2313
+ if (!Array.isArray(node) || node.length === 0) return;
2314
+ const head = node[0];
2315
+ if (typeof head === "string" && (head.toLowerCase() === "and" || head.toLowerCase() === "or")) {
2316
+ for (let i = 1; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
2317
+ return;
2318
+ }
2319
+ if (Array.isArray(head)) {
2320
+ for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
2321
+ return;
2322
+ }
2323
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data3.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
2324
+ out.push({ name: head, path: `${basePath}[0]` });
2325
+ }
2326
+ }
2327
+ function reactFieldRefs(spec, values, basePath) {
2328
+ const own = [];
2329
+ const queried = [];
2330
+ const readable = (key) => {
2331
+ const v = values.get(key);
2332
+ return v === NOT_STATIC ? void 0 : v;
2333
+ };
2334
+ const at = (key) => `${basePath}${PATH_SEP}${key}`;
2335
+ for (const key of spec.fields ?? []) {
2336
+ own.push(...fieldRefsFrom(readable(key), at(key)));
2337
+ }
2338
+ for (const key of spec.sorts ?? []) {
2339
+ own.push(...sortFieldRefs(readable(key), at(key)));
2340
+ }
2341
+ for (const key of spec.nestedFields ?? []) {
2342
+ const v = readable(key);
2343
+ if (isRec5(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2344
+ }
2345
+ for (const key of spec.sections ?? []) {
2346
+ const v = readable(key);
2347
+ if (!Array.isArray(v)) continue;
2348
+ for (let i = 0; i < v.length; i++) {
2349
+ const section = v[i];
2350
+ if (!isRec5(section)) continue;
2351
+ own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
2352
+ }
2353
+ }
2354
+ for (const key of spec.keyedByField ?? []) {
2355
+ const v = readable(key);
2356
+ if (!isRec5(v)) continue;
2357
+ for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
2358
+ }
2359
+ for (const key of spec.filterArrays ?? []) {
2360
+ filterFieldRefs(values.get(key), at(key), queried);
2361
+ }
2362
+ return { own, queried };
2363
+ }
2364
+ function checkBlockFieldProps(tag, values, objectFields, where, path) {
2365
+ const objectName = strOf(values.get("objectName"));
2366
+ const out = [];
2367
+ const spec = REACT_FIELD_SPECS[tag];
2368
+ if (spec) {
2369
+ const { own, queried } = reactFieldRefs(spec, values, path);
2370
+ out.push(...checkFieldRefs(own, objectName, objectFields, where));
2371
+ out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
2372
+ }
2373
+ if (tag === "ObjectForm") {
2374
+ const raw = values.get("subforms");
2375
+ const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
2376
+ for (const sub of subs.child) {
2377
+ out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
2378
+ }
2379
+ out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
2380
+ }
2381
+ const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
2382
+ if (schemaType) {
2383
+ const props = readableProps(values);
2384
+ if (schemaType === RELATED_LIST_TYPE) {
2385
+ const split = relatedListFieldRefs(props, path, PATH_SEP);
2386
+ out.push(...checkFieldRefs(split.related, split.relatedObject, objectFields, where));
2387
+ out.push(...checkFieldRefs(split.picker, split.pickerObject, objectFields, where));
2388
+ } else if (COMPONENT_FIELD_SPECS[schemaType]) {
2389
+ out.push(
2390
+ ...checkFieldRefs(
2391
+ componentFieldRefs(schemaType, props, path, PATH_SEP) ?? [],
2392
+ objectName,
2393
+ objectFields,
2394
+ where
2395
+ )
2396
+ );
2397
+ }
2398
+ }
2399
+ return out;
2400
+ }
1771
2401
  function validateReactPageProps(stack) {
1772
2402
  const findings = [];
1773
2403
  const objectFields = indexObjectFields(stack);
1774
- const pages = asArray10(stack.pages);
2404
+ const searchTargets = indexObjectSearchTargets(stack);
2405
+ const pages = asArray12(stack.pages);
1775
2406
  for (let p = 0; p < pages.length; p++) {
1776
2407
  const page = pages[p];
1777
2408
  if (!page || page.kind !== "react") continue;
@@ -1801,7 +2432,10 @@ function validateReactPageProps(stack) {
1801
2432
  if (tsc.isJsxAttribute(a)) {
1802
2433
  const propName = a.name.getText(sf);
1803
2434
  used.add(propName);
1804
- values.set(propName, attrValue(tsc, sf, a));
2435
+ values.set(
2436
+ propName,
2437
+ FILTER_PROPS.has(propName) ? filterAttrValue(tsc, sf, a) : attrValue(tsc, sf, a)
2438
+ );
1805
2439
  }
1806
2440
  }
1807
2441
  const where = `page "${name}" \u203A <${tag}>`;
@@ -1836,6 +2470,23 @@ function validateReactPageProps(stack) {
1836
2470
  if (tag === "ObjectChart" && !hasSpread) {
1837
2471
  checkObjectChart({ values, where, path }, objectFields, findings);
1838
2472
  }
2473
+ if (tag === "ListView" && !hasSpread) {
2474
+ findings.push(
2475
+ ...checkSearchableFieldList(
2476
+ values.get("searchableFields"),
2477
+ strOf(values.get("objectName")),
2478
+ searchTargets,
2479
+ where,
2480
+ `${path} \u203A searchableFields`,
2481
+ "searchableFields"
2482
+ )
2483
+ );
2484
+ }
2485
+ if (!hasSpread) {
2486
+ findings.push(
2487
+ ...checkBlockFieldProps(tag, values, objectFields, where, path)
2488
+ );
2489
+ }
1839
2490
  }
1840
2491
  }
1841
2492
  tsc.forEachChild(node, visit);
@@ -1847,11 +2498,11 @@ function validateReactPageProps(stack) {
1847
2498
 
1848
2499
  // src/validate-page-source-styling.ts
1849
2500
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
1850
- var asArray11 = (v) => Array.isArray(v) ? v : [];
2501
+ var asArray13 = (v) => Array.isArray(v) ? v : [];
1851
2502
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
1852
2503
  function validatePageSourceStyling(stack) {
1853
2504
  const findings = [];
1854
- const pages = asArray11(stack.pages);
2505
+ const pages = asArray13(stack.pages);
1855
2506
  for (let p = 0; p < pages.length; p++) {
1856
2507
  const page = pages[p];
1857
2508
  if (!page) continue;
@@ -1877,10 +2528,10 @@ function validatePageSourceStyling(stack) {
1877
2528
  }
1878
2529
 
1879
2530
  // src/validate-record-title.ts
1880
- var import_data2 = require("@objectstack/spec/data");
2531
+ var import_data4 = require("@objectstack/spec/data");
1881
2532
  var TITLE_FORMAT_RETIRED = "title-format-retired";
1882
2533
  var TITLE_UNRESOLVABLE = "title-unresolvable";
1883
- function asArray12(v) {
2534
+ function asArray14(v) {
1884
2535
  if (Array.isArray(v)) return v;
1885
2536
  if (v && typeof v === "object") {
1886
2537
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1889,7 +2540,7 @@ function asArray12(v) {
1889
2540
  }
1890
2541
  function validateRecordTitle(stack) {
1891
2542
  const findings = [];
1892
- const objects = asArray12(stack.objects);
2543
+ const objects = asArray14(stack.objects);
1893
2544
  for (let i = 0; i < objects.length; i++) {
1894
2545
  const obj = objects[i];
1895
2546
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -1905,7 +2556,7 @@ function validateRecordTitle(stack) {
1905
2556
  hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
1906
2557
  });
1907
2558
  }
1908
- const completeness = (0, import_data2.objectTitleCompleteness)(obj);
2559
+ const completeness = (0, import_data4.objectTitleCompleteness)(obj);
1909
2560
  if (completeness.status === "none") {
1910
2561
  findings.push({
1911
2562
  severity: "warning",
@@ -1925,7 +2576,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
1925
2576
  var FIELD_GROUP_EMPTY = "field-group-empty";
1926
2577
  var FIELD_GROUP_SHADOWED = "field-group-shadowed";
1927
2578
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
1928
- function asArray13(v) {
2579
+ function asArray15(v) {
1929
2580
  if (Array.isArray(v)) return v;
1930
2581
  if (v && typeof v === "object") {
1931
2582
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1934,7 +2585,7 @@ function asArray13(v) {
1934
2585
  }
1935
2586
  function validateSemanticRoles(stack) {
1936
2587
  const findings = [];
1937
- const objects = asArray13(stack.objects);
2588
+ const objects = asArray15(stack.objects);
1938
2589
  for (let i = 0; i < objects.length; i++) {
1939
2590
  const obj = objects[i];
1940
2591
  if (!obj || typeof obj !== "object") continue;
@@ -2029,7 +2680,7 @@ function validateSemanticRoles(stack) {
2029
2680
  // src/validate-form-layout.ts
2030
2681
  var FORM_FIELD_UNKNOWN = "form-field-unknown";
2031
2682
  var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
2032
- function asArray14(v) {
2683
+ function asArray16(v) {
2033
2684
  if (Array.isArray(v)) return v;
2034
2685
  if (v && typeof v === "object") {
2035
2686
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2054,13 +2705,13 @@ function boundObject(view) {
2054
2705
  function validateFormLayout(stack) {
2055
2706
  const findings = [];
2056
2707
  const objectFields = /* @__PURE__ */ new Map();
2057
- for (const obj of asArray14(stack.objects)) {
2708
+ for (const obj of asArray16(stack.objects)) {
2058
2709
  const name = typeof obj.name === "string" ? obj.name : void 0;
2059
2710
  if (!name) continue;
2060
2711
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
2061
2712
  objectFields.set(name, new Set(fields));
2062
2713
  }
2063
- const views = asArray14(stack.views);
2714
+ const views = asArray16(stack.views);
2064
2715
  for (let i = 0; i < views.length; i++) {
2065
2716
  const view = views[i];
2066
2717
  if (!view || typeof view !== "object") continue;
@@ -2110,7 +2761,7 @@ var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
2110
2761
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
2111
2762
  var CANONICAL = "visibleWhen";
2112
2763
  var ALIASES = ["visibleOn", "visibility"];
2113
- function asArray15(v) {
2764
+ function asArray17(v) {
2114
2765
  if (Array.isArray(v)) return v;
2115
2766
  if (v && typeof v === "object") {
2116
2767
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2172,7 +2823,7 @@ function isFieldObject(entry) {
2172
2823
  function validateVisibilityPredicates(stack, opts = {}) {
2173
2824
  const layer = opts.layer ?? "runtime";
2174
2825
  const findings = [];
2175
- const views = asArray15(stack.views);
2826
+ const views = asArray17(stack.views);
2176
2827
  for (let i = 0; i < views.length; i++) {
2177
2828
  const view = views[i];
2178
2829
  if (!view || typeof view !== "object") continue;
@@ -2195,7 +2846,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
2195
2846
  }
2196
2847
  }
2197
2848
  }
2198
- const pages = asArray15(stack.pages);
2849
+ const pages = asArray17(stack.pages);
2199
2850
  for (let i = 0; i < pages.length; i++) {
2200
2851
  const page = pages[i];
2201
2852
  if (!page || typeof page !== "object") continue;
@@ -2219,7 +2870,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
2219
2870
  // src/validate-capability-references.ts
2220
2871
  var import_security = require("@objectstack/spec/security");
2221
2872
  var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
2222
- function asArray16(v) {
2873
+ function asArray18(v) {
2223
2874
  if (Array.isArray(v)) return v;
2224
2875
  if (v && typeof v === "object") {
2225
2876
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2244,13 +2895,13 @@ function validateCapabilityReferences(stack) {
2244
2895
  const findings = [];
2245
2896
  if (!stack || typeof stack !== "object") return findings;
2246
2897
  const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
2247
- for (const cap of asArray16(stack.capabilities)) {
2898
+ for (const cap of asArray18(stack.capabilities)) {
2248
2899
  if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
2249
2900
  }
2250
- for (const ps of asArray16(stack.permissions)) {
2901
+ for (const ps of asArray18(stack.permissions)) {
2251
2902
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
2252
2903
  }
2253
- for (const seed of asArray16(stack.data)) {
2904
+ for (const seed of asArray18(stack.data)) {
2254
2905
  if (seed.object !== "sys_capability") continue;
2255
2906
  for (const rec of Array.isArray(seed.records) ? seed.records : []) {
2256
2907
  const name = rec?.name;
@@ -2269,7 +2920,7 @@ function validateCapabilityReferences(stack) {
2269
2920
  hint
2270
2921
  });
2271
2922
  };
2272
- const objects = asArray16(stack.objects);
2923
+ const objects = asArray18(stack.objects);
2273
2924
  for (let i = 0; i < objects.length; i++) {
2274
2925
  const obj = objects[i];
2275
2926
  if (!obj || typeof obj !== "object") continue;
@@ -2278,27 +2929,27 @@ function validateCapabilityReferences(stack) {
2278
2929
  for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
2279
2930
  flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
2280
2931
  }
2281
- const fields = asArray16(obj.fields);
2932
+ const fields = asArray18(obj.fields);
2282
2933
  for (const f of fields) {
2283
2934
  const fname = typeof f.name === "string" ? f.name : "(field)";
2284
2935
  for (const cap of asCapArray(f.requiredPermissions)) {
2285
2936
  flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
2286
2937
  }
2287
2938
  }
2288
- for (const [ai, action] of asArray16(obj.actions).entries()) {
2939
+ for (const [ai, action] of asArray18(obj.actions).entries()) {
2289
2940
  const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
2290
2941
  for (const cap of asCapArray(action.requiredPermissions)) {
2291
2942
  flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
2292
2943
  }
2293
2944
  }
2294
2945
  }
2295
- for (const [i, action] of asArray16(stack.actions).entries()) {
2946
+ for (const [i, action] of asArray18(stack.actions).entries()) {
2296
2947
  const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
2297
2948
  for (const cap of asCapArray(action.requiredPermissions)) {
2298
2949
  flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
2299
2950
  }
2300
2951
  }
2301
- const apps = asArray16(stack.apps);
2952
+ const apps = asArray18(stack.apps);
2302
2953
  for (let i = 0; i < apps.length; i++) {
2303
2954
  const app = apps[i];
2304
2955
  if (!app || typeof app !== "object") continue;
@@ -2325,7 +2976,7 @@ function validateCapabilityReferences(stack) {
2325
2976
  }
2326
2977
 
2327
2978
  // src/validate-approval-approvers.ts
2328
- var import_automation = require("@objectstack/spec/automation");
2979
+ var import_automation3 = require("@objectstack/spec/automation");
2329
2980
  var import_spec = require("@objectstack/spec");
2330
2981
  var import_formula2 = require("@objectstack/formula");
2331
2982
  var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
@@ -2347,7 +2998,7 @@ var TYPE_FIX = {
2347
2998
  business_unit: "department",
2348
2999
  bu: "department"
2349
3000
  };
2350
- function asArray17(v) {
3001
+ function asArray19(v) {
2351
3002
  if (Array.isArray(v)) return v;
2352
3003
  if (v && typeof v === "object") {
2353
3004
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2357,16 +3008,16 @@ function asArray17(v) {
2357
3008
  function validateApprovalApprovers(stack) {
2358
3009
  const findings = [];
2359
3010
  if (!stack || typeof stack !== "object") return findings;
2360
- const flows = asArray17(stack.flows);
2361
- const validTypes = new Set(import_automation.ApproverType.options);
3011
+ const flows = asArray19(stack.flows);
3012
+ const validTypes = new Set(import_automation3.ApproverType.options);
2362
3013
  for (let fi = 0; fi < flows.length; fi++) {
2363
3014
  const flow = flows[fi];
2364
3015
  if (!flow || typeof flow !== "object") continue;
2365
3016
  const flowName = typeof flow.name === "string" ? flow.name : `(flow ${fi})`;
2366
- const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
2367
- for (let ni = 0; ni < nodes.length; ni++) {
2368
- const node = nodes[ni];
2369
- if (!node || node.type !== import_automation.APPROVAL_NODE_TYPE) continue;
3017
+ const walked = walkFlowNodes(flow, `flows[${fi}]`);
3018
+ for (let ni = 0; ni < walked.length; ni++) {
3019
+ const { node, path: nodePath } = walked[ni];
3020
+ if (!node || node.type !== import_automation3.APPROVAL_NODE_TYPE) continue;
2370
3021
  const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
2371
3022
  const cfg = node.config ?? {};
2372
3023
  const approvers = Array.isArray(cfg.approvers) ? cfg.approvers : [];
@@ -2376,7 +3027,7 @@ function validateApprovalApprovers(stack) {
2376
3027
  if (!a || typeof a !== "object") continue;
2377
3028
  const type = typeof a.type === "string" ? a.type : "";
2378
3029
  const value = typeof a.value === "string" ? a.value : "";
2379
- const path = `flows[${fi}].nodes[${ni}].config.approvers[${ai}]`;
3030
+ const path = `${nodePath}.config.approvers[${ai}]`;
2380
3031
  if (type && !validTypes.has(type)) {
2381
3032
  const fix = TYPE_FIX[type];
2382
3033
  findings.push({
@@ -2384,12 +3035,12 @@ function validateApprovalApprovers(stack) {
2384
3035
  rule: APPROVAL_APPROVER_TYPE_UNKNOWN,
2385
3036
  where,
2386
3037
  path: `${path}.type`,
2387
- message: `approver type '${type}' is not an ApproverType (${import_automation.ApproverType.options.join(" | ")}).`,
3038
+ message: `approver type '${type}' is not an ApproverType (${import_automation3.ApproverType.options.join(" | ")}).`,
2388
3039
  hint: fix ? `Use the spec value: { type: '${fix}', value: '${value}' }.` : `Pick one of the spec values; unmapped types degrade to an inert '${type}:${value}' literal at runtime.`
2389
3040
  });
2390
3041
  continue;
2391
3042
  }
2392
- const canonical = (0, import_automation.canonicalApproverType)(type);
3043
+ const canonical = (0, import_automation3.canonicalApproverType)(type);
2393
3044
  if (canonical === "expression") {
2394
3045
  const source = value.trim();
2395
3046
  if (!source) {
@@ -2446,8 +3097,8 @@ function validateApprovalApprovers(stack) {
2446
3097
  message: `approver { type: '${type}', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: ${MEMBERSHIP_TIER_LIST}) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
2447
3098
  hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type 'org_membership_level' only for membership tiers (${MEMBERSHIP_TIER_LIST}) \u2014 the vocabulary is closed (ADR-0108), so a business role is always a position.`
2448
3099
  });
2449
- } else if (type in import_automation.DEPRECATED_APPROVER_TYPES) {
2450
- const fix = (0, import_automation.canonicalApproverType)(type);
3100
+ } else if (type in import_automation3.DEPRECATED_APPROVER_TYPES) {
3101
+ const fix = (0, import_automation3.canonicalApproverType)(type);
2451
3102
  findings.push({
2452
3103
  severity: "warning",
2453
3104
  rule: APPROVAL_APPROVER_TYPE_DEPRECATED,
@@ -2456,7 +3107,7 @@ function validateApprovalApprovers(stack) {
2456
3107
  message: `approver type '${type}' is the deprecated spelling of '${fix}' (ADR-0090 D3) and is removed in the next major.`,
2457
3108
  hint: `Author { type: '${fix}', value: '${value}' }. It resolves identically today.`
2458
3109
  });
2459
- } else if (import_automation.APPROVER_VALUE_BINDINGS[canonical]?.source === "unsupported") {
3110
+ } else if (import_automation3.APPROVER_VALUE_BINDINGS[canonical]?.source === "unsupported") {
2460
3111
  findings.push({
2461
3112
  severity: "warning",
2462
3113
  rule: APPROVAL_APPROVER_TYPE_UNSUPPORTED,
@@ -2467,7 +3118,7 @@ function validateApprovalApprovers(stack) {
2467
3118
  });
2468
3119
  }
2469
3120
  const declaredOrg = a.organization;
2470
- if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" && import_automation.ApproverType.options.includes(canonical) && !(0, import_automation.approverTypeIsOrgScoped)(canonical)) {
3121
+ if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" && import_automation3.ApproverType.options.includes(canonical) && !(0, import_automation3.approverTypeIsOrgScoped)(canonical)) {
2471
3122
  findings.push({
2472
3123
  severity: "error",
2473
3124
  rule: APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED,
@@ -2481,38 +3132,38 @@ function validateApprovalApprovers(stack) {
2481
3132
  const routable = approvers.filter(
2482
3133
  (a) => a && typeof a === "object" && typeof a.type === "string"
2483
3134
  );
2484
- if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0, import_automation.canonicalApproverType)(String(a.type))))) {
3135
+ if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0, import_automation3.canonicalApproverType)(String(a.type))))) {
2485
3136
  const locks = cfg.lockRecord !== false;
2486
3137
  findings.push({
2487
3138
  severity: "info",
2488
3139
  rule: APPROVAL_APPROVERS_MAY_RESOLVE_EMPTY,
2489
3140
  where,
2490
- path: `flows[${fi}].nodes[${ni}].config.approvers`,
3141
+ path: `${nodePath}.config.approvers`,
2491
3142
  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.` : `.`),
2492
3143
  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).`
2493
3144
  });
2494
3145
  }
2495
3146
  const hasExpression = approvers.some(
2496
- (a) => a && typeof a === "object" && (0, import_automation.canonicalApproverType)(String(a.type ?? "")) === "expression"
3147
+ (a) => a && typeof a === "object" && (0, import_automation3.canonicalApproverType)(String(a.type ?? "")) === "expression"
2497
3148
  );
2498
3149
  if (hasExpression && cfg.onEmptyApprovers == null) {
2499
3150
  findings.push({
2500
3151
  severity: "info",
2501
3152
  rule: APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
2502
3153
  where,
2503
- path: `flows[${fi}].nodes[${ni}].config`,
3154
+ path: `${nodePath}.config`,
2504
3155
  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).`,
2505
3156
  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).`
2506
3157
  });
2507
3158
  }
2508
- const declaredOutputs = (0, import_automation.normalizeDecisionOutputs)(cfg.decisionOutputs).map((d) => d.key);
3159
+ const declaredOutputs = (0, import_automation3.normalizeDecisionOutputs)(cfg.decisionOutputs).map((d) => d.key);
2509
3160
  const reserved = declaredOutputs.filter((k) => RESERVED_OUTPUT_KEYS.has(k));
2510
3161
  if (reserved.length) {
2511
3162
  findings.push({
2512
3163
  severity: "error",
2513
3164
  rule: APPROVAL_DECISION_OUTPUTS_RESERVED,
2514
3165
  where,
2515
- path: `flows[${fi}].nodes[${ni}].config.decisionOutputs`,
3166
+ path: `${nodePath}.config.decisionOutputs`,
2516
3167
  message: `decisionOutputs declares reserved key(s) \`${reserved.join("`, `")}\` \u2014 the resume envelope owns them, so every decide carrying them is rejected.`,
2517
3168
  hint: `Rename the output key(s); any name other than 'decision'/'requestId' works.`
2518
3169
  });
@@ -2525,7 +3176,7 @@ function validateApprovalApprovers(stack) {
2525
3176
  severity: "warning",
2526
3177
  rule: APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
2527
3178
  where,
2528
- path: `flows[${fi}].nodes[${ni}].config.escalation.escalateTo`,
3179
+ path: `${nodePath}.config.escalation.escalateTo`,
2529
3180
  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.`,
2530
3181
  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'.`
2531
3182
  });
@@ -2652,7 +3303,7 @@ var OWD_WIDTH = {
2652
3303
  public_read: 1,
2653
3304
  public_read_write: 2
2654
3305
  };
2655
- function asArray18(v) {
3306
+ function asArray20(v) {
2656
3307
  if (Array.isArray(v)) return v;
2657
3308
  if (v && typeof v === "object") {
2658
3309
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2678,7 +3329,7 @@ function refOf(def) {
2678
3329
  return typeof r === "string" && r ? r : void 0;
2679
3330
  }
2680
3331
  function firstMasterDetailField(obj) {
2681
- for (const f of asArray18(obj.fields)) {
3332
+ for (const f of asArray20(obj.fields)) {
2682
3333
  if (f.type === "master_detail") {
2683
3334
  return { name: String(f.name ?? "?"), parent: refOf(f) };
2684
3335
  }
@@ -2691,8 +3342,8 @@ function grantsObjectAccess(p) {
2691
3342
  function validateSecurityPosture(stack, opts) {
2692
3343
  const findings = [];
2693
3344
  if (!stack || typeof stack !== "object") return findings;
2694
- const objects = asArray18(stack.objects);
2695
- const permissionSets = asArray18(stack.permissions);
3345
+ const objects = asArray20(stack.objects);
3346
+ const permissionSets = asArray20(stack.permissions);
2696
3347
  for (let i = 0; i < objects.length; i++) {
2697
3348
  const obj = objects[i];
2698
3349
  if (!obj || typeof obj !== "object") continue;
@@ -2821,10 +3472,10 @@ function validateSecurityPosture(stack, opts) {
2821
3472
  if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
2822
3473
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
2823
3474
  flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
2824
- for (const f of asArray18(obj.fields)) {
3475
+ for (const f of asArray20(obj.fields)) {
2825
3476
  flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
2826
3477
  }
2827
- for (const [ai, action] of asArray18(obj.actions).entries()) {
3478
+ for (const [ai, action] of asArray20(obj.actions).entries()) {
2828
3479
  flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
2829
3480
  }
2830
3481
  }
@@ -2833,19 +3484,19 @@ function validateSecurityPosture(stack, opts) {
2833
3484
  if (!ps || typeof ps !== "object") continue;
2834
3485
  flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
2835
3486
  }
2836
- for (const [i, pos] of asArray18(stack.positions).entries()) {
3487
+ for (const [i, pos] of asArray20(stack.positions).entries()) {
2837
3488
  flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
2838
3489
  }
2839
- for (const [i, app] of asArray18(stack.apps).entries()) {
3490
+ for (const [i, app] of asArray20(stack.apps).entries()) {
2840
3491
  flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
2841
3492
  }
2842
- for (const [i, book] of asArray18(stack.books).entries()) {
3493
+ for (const [i, book] of asArray20(stack.books).entries()) {
2843
3494
  flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
2844
3495
  }
2845
3496
  const stackSetNames = new Set(
2846
3497
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
2847
3498
  );
2848
- for (const [i, book] of asArray18(stack.books).entries()) {
3499
+ for (const [i, book] of asArray20(stack.books).entries()) {
2849
3500
  const audience = book.audience;
2850
3501
  if (!audience || typeof audience !== "object") continue;
2851
3502
  const setName = audience.permissionSet;
@@ -2923,7 +3574,7 @@ function validateSecurityPosture(stack, opts) {
2923
3574
  }
2924
3575
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
2925
3576
  const nowMs = opts?.nowMs ?? Date.now();
2926
- for (const [i, seed] of asArray18(stack.data).entries()) {
3577
+ for (const [i, seed] of asArray20(stack.data).entries()) {
2927
3578
  const seedObject = typeof seed.object === "string" ? seed.object : "";
2928
3579
  if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
2929
3580
  const records = Array.isArray(seed.records) ? seed.records : [];
@@ -2967,7 +3618,7 @@ function validateSecurityPosture(stack, opts) {
2967
3618
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
2968
3619
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
2969
3620
  var ORG_PARENT_FIELD = "parent_organization_id";
2970
- function asArray19(v) {
3621
+ function asArray21(v) {
2971
3622
  if (Array.isArray(v)) return v;
2972
3623
  if (v && typeof v === "object") {
2973
3624
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -2988,9 +3639,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
2988
3639
  function validateOrgAxisRedLines(stack) {
2989
3640
  const findings = [];
2990
3641
  const cfg = stack ?? {};
2991
- const permissionSets = asArray19(cfg.permissions ?? cfg.permissionSets);
3642
+ const permissionSets = asArray21(cfg.permissions ?? cfg.permissionSets);
2992
3643
  permissionSets.forEach((ps, psIndex) => {
2993
- asArray19(ps.rowLevelSecurity).forEach((policy, pIndex) => {
3644
+ asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
2994
3645
  for (const clause of ["using", "check"]) {
2995
3646
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
2996
3647
  findings.push({
@@ -3004,10 +3655,10 @@ function validateOrgAxisRedLines(stack) {
3004
3655
  }
3005
3656
  });
3006
3657
  });
3007
- const objects = asArray19(cfg.objects);
3658
+ const objects = asArray21(cfg.objects);
3008
3659
  objects.forEach((object, oIndex) => {
3009
3660
  const objectName = str(object.name) || String(oIndex);
3010
- asArray19(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
3661
+ asArray21(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
3011
3662
  for (const clause of ["using", "check"]) {
3012
3663
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
3013
3664
  findings.push({
@@ -3021,7 +3672,7 @@ function validateOrgAxisRedLines(stack) {
3021
3672
  }
3022
3673
  });
3023
3674
  });
3024
- asArray19(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
3675
+ asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
3025
3676
  const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
3026
3677
  const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
3027
3678
  if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
@@ -3038,7 +3689,7 @@ function validateOrgAxisRedLines(stack) {
3038
3689
  const tenancyDisabledObjects = new Set(
3039
3690
  objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
3040
3691
  );
3041
- asArray19(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
3692
+ asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
3042
3693
  const target = str(rule.object ?? rule.objectName);
3043
3694
  if (!target || !tenancyDisabledObjects.has(target)) return;
3044
3695
  const sharedTo = rule.sharedTo ?? rule.recipient;
@@ -3059,14 +3710,14 @@ function validateOrgAxisRedLines(stack) {
3059
3710
  // src/validate-dashboard-action-refs.ts
3060
3711
  var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
3061
3712
  var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
3062
- function asArray20(v) {
3713
+ function asArray22(v) {
3063
3714
  if (Array.isArray(v)) return v;
3064
3715
  if (v && typeof v === "object") {
3065
3716
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3066
3717
  }
3067
3718
  return [];
3068
3719
  }
3069
- function strName(v) {
3720
+ function strName5(v) {
3070
3721
  return typeof v === "string" && v.length > 0 ? v : void 0;
3071
3722
  }
3072
3723
  var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
@@ -3083,7 +3734,7 @@ var URL_COLLECTION_TO_STACK_KEY = {
3083
3734
  views: "views"
3084
3735
  };
3085
3736
  function viewContainerName(item) {
3086
- 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);
3737
+ 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);
3087
3738
  }
3088
3739
  function collectKnownTargets(stack) {
3089
3740
  const actions = /* @__PURE__ */ new Set();
@@ -3093,22 +3744,22 @@ function collectKnownTargets(stack) {
3093
3744
  const pages = /* @__PURE__ */ new Set();
3094
3745
  const views = /* @__PURE__ */ new Set();
3095
3746
  const collectNames = (v, into, name) => {
3096
- for (const item of asArray20(v)) {
3747
+ for (const item of asArray22(v)) {
3097
3748
  if (!item || typeof item !== "object") continue;
3098
3749
  const n = name(item);
3099
3750
  if (n) into.add(n);
3100
3751
  }
3101
3752
  };
3102
- collectNames(stack.actions, actions, (a) => strName(a.name));
3103
- for (const obj of asArray20(stack.objects)) {
3753
+ collectNames(stack.actions, actions, (a) => strName5(a.name));
3754
+ for (const obj of asArray22(stack.objects)) {
3104
3755
  if (!obj || typeof obj !== "object") continue;
3105
- const n = strName(obj.name);
3756
+ const n = strName5(obj.name);
3106
3757
  if (n) objects.add(n);
3107
- collectNames(obj.actions, actions, (a) => strName(a.name));
3758
+ collectNames(obj.actions, actions, (a) => strName5(a.name));
3108
3759
  }
3109
- collectNames(stack.reports, reports, (r) => strName(r.name));
3110
- collectNames(stack.dashboards, dashboards, (d) => strName(d.name));
3111
- collectNames(stack.pages, pages, (p) => strName(p.name));
3760
+ collectNames(stack.reports, reports, (r) => strName5(r.name));
3761
+ collectNames(stack.dashboards, dashboards, (d) => strName5(d.name));
3762
+ collectNames(stack.pages, pages, (p) => strName5(p.name));
3112
3763
  collectNames(stack.views, views, viewContainerName);
3113
3764
  for (const o of objects) views.add(o);
3114
3765
  return { actions, objects, reports, dashboards, pages, views };
@@ -3140,14 +3791,14 @@ function resolveUrlRoute(target, known) {
3140
3791
  function validateDashboardActionRefs(stack) {
3141
3792
  const findings = [];
3142
3793
  if (!stack || typeof stack !== "object") return findings;
3143
- const dashboards = asArray20(stack.dashboards);
3794
+ const dashboards = asArray22(stack.dashboards);
3144
3795
  if (dashboards.length === 0) return findings;
3145
3796
  const known = collectKnownTargets(stack);
3146
3797
  const checkOne = (action, where, path) => {
3147
- const target = strName(action.actionUrl);
3798
+ const target = strName5(action.actionUrl);
3148
3799
  if (!target) return;
3149
3800
  if (target.includes("${")) return;
3150
- const actionType = strName(action.actionType) ?? "url";
3801
+ const actionType = strName5(action.actionType) ?? "url";
3151
3802
  if (actionType === "script" || actionType === "modal") {
3152
3803
  if (resolveActionTarget(actionType, target, known)) return;
3153
3804
  const kindWord = actionType === "script" ? "script" : "modal";
@@ -3178,25 +3829,25 @@ function validateDashboardActionRefs(stack) {
3178
3829
  for (let di = 0; di < dashboards.length; di++) {
3179
3830
  const dash = dashboards[di];
3180
3831
  if (!dash || typeof dash !== "object") continue;
3181
- const dashName = strName(dash.name) ?? `(dashboard ${di})`;
3832
+ const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
3182
3833
  const dashPath = `dashboards[${di}]`;
3183
- const headerActions = asArray20(dash.header?.actions);
3834
+ const headerActions = asArray22(dash.header?.actions);
3184
3835
  for (let ai = 0; ai < headerActions.length; ai++) {
3185
3836
  const action = headerActions[ai];
3186
3837
  if (!action || typeof action !== "object") continue;
3187
- const label2 = strName(action.label) ?? strName(action.actionUrl) ?? `#${ai}`;
3838
+ const label2 = strName5(action.label) ?? strName5(action.actionUrl) ?? `#${ai}`;
3188
3839
  checkOne(
3189
3840
  action,
3190
3841
  `dashboard "${dashName}" \xB7 header action "${label2}"`,
3191
3842
  `${dashPath}.header.actions[${ai}].actionUrl`
3192
3843
  );
3193
3844
  }
3194
- const widgets = asArray20(dash.widgets);
3845
+ const widgets = asArray22(dash.widgets);
3195
3846
  for (let wi = 0; wi < widgets.length; wi++) {
3196
3847
  const widget = widgets[wi];
3197
3848
  if (!widget || typeof widget !== "object") continue;
3198
- if (!strName(widget.actionUrl)) continue;
3199
- const widgetId = strName(widget.id) ?? `#${wi}`;
3849
+ if (!strName5(widget.actionUrl)) continue;
3850
+ const widgetId = strName5(widget.id) ?? `#${wi}`;
3200
3851
  checkOne(
3201
3852
  { actionType: widget.actionType, actionUrl: widget.actionUrl },
3202
3853
  `dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
@@ -3208,10 +3859,10 @@ function validateDashboardActionRefs(stack) {
3208
3859
  }
3209
3860
 
3210
3861
  // src/validate-filter-tokens.ts
3211
- var import_data3 = require("@objectstack/spec/data");
3862
+ var import_data5 = require("@objectstack/spec/data");
3212
3863
  var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
3213
3864
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
3214
- function asArray21(v) {
3865
+ function asArray23(v) {
3215
3866
  if (Array.isArray(v)) return v;
3216
3867
  if (v && typeof v === "object") {
3217
3868
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -3221,11 +3872,11 @@ function asArray21(v) {
3221
3872
  function label(v, fallback) {
3222
3873
  return typeof v === "string" && v.length > 0 ? v : fallback;
3223
3874
  }
3224
- var KNOWN_LIST = import_data3.CONTEXT_TOKENS.join("}, {");
3875
+ var KNOWN_LIST = import_data5.CONTEXT_TOKENS.join("}, {");
3225
3876
  function walkFilterValues(node, path, where, out, seen) {
3226
3877
  if (node === null || node === void 0) return;
3227
3878
  if (typeof node === "string") {
3228
- const cls = (0, import_data3.classifyFilterToken)(node);
3879
+ const cls = (0, import_data5.classifyFilterToken)(node);
3229
3880
  if (cls?.kind === "unknown") {
3230
3881
  const suggestion = cls.suggestion;
3231
3882
  out.push({
@@ -3280,7 +3931,7 @@ function validateFilterTokens(stack) {
3280
3931
  ["apps", "app"]
3281
3932
  ];
3282
3933
  for (const [key, kind] of surfaces) {
3283
- const items = asArray21(stack[key]);
3934
+ const items = asArray23(stack[key]);
3284
3935
  items.forEach((item, i) => {
3285
3936
  const name = label(item.name ?? item.id, `#${i}`);
3286
3937
  if (kind === "dashboard") {
@@ -3306,18 +3957,18 @@ function validateFilterTokens(stack) {
3306
3957
  }
3307
3958
 
3308
3959
  // src/validate-object-references.ts
3309
- var import_system = require("@objectstack/spec/system");
3310
- var PLATFORM_NAMES = [...import_system.PLATFORM_PROVIDED_OBJECT_NAMES];
3960
+ var import_system2 = require("@objectstack/spec/system");
3961
+ var PLATFORM_NAMES = [...import_system2.PLATFORM_PROVIDED_OBJECT_NAMES];
3311
3962
  var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
3312
3963
  var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
3313
- function asArray22(v) {
3964
+ function asArray24(v) {
3314
3965
  if (Array.isArray(v)) return v;
3315
3966
  if (v && typeof v === "object") {
3316
3967
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3317
3968
  }
3318
3969
  return [];
3319
3970
  }
3320
- function strName2(v) {
3971
+ function strName6(v) {
3321
3972
  return typeof v === "string" && v.length > 0 ? v : void 0;
3322
3973
  }
3323
3974
  function isInterpolated(target) {
@@ -3325,11 +3976,11 @@ function isInterpolated(target) {
3325
3976
  const open = target.indexOf("{");
3326
3977
  return open !== -1 && target.indexOf("}", open + 2) !== -1;
3327
3978
  }
3328
- function suggest2(target, known) {
3979
+ function suggest3(target, known) {
3329
3980
  let best;
3330
3981
  let bestScore = Infinity;
3331
3982
  for (const candidate of known) {
3332
- const d = distance(target, candidate);
3983
+ const d = distance2(target, candidate);
3333
3984
  if (d < bestScore) {
3334
3985
  bestScore = d;
3335
3986
  best = candidate;
@@ -3338,7 +3989,7 @@ function suggest2(target, known) {
3338
3989
  const limit = Math.max(2, Math.floor(target.length / 3));
3339
3990
  return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
3340
3991
  }
3341
- function distance(a, b) {
3992
+ function distance2(a, b) {
3342
3993
  const m = a.length;
3343
3994
  const n = b.length;
3344
3995
  if (m === 0) return n;
@@ -3357,25 +4008,25 @@ function distance(a, b) {
3357
4008
  function validateObjectReferences(stack) {
3358
4009
  const findings = [];
3359
4010
  if (!stack || typeof stack !== "object") return findings;
3360
- const objects = asArray22(stack.objects);
4011
+ const objects = asArray24(stack.objects);
3361
4012
  const ownObjects = /* @__PURE__ */ new Set();
3362
4013
  for (const obj of objects) {
3363
- const n = strName2(obj.name);
4014
+ const n = strName6(obj.name);
3364
4015
  if (n) ownObjects.add(n);
3365
4016
  }
3366
4017
  const check = (target, where, path, subject, fix) => {
3367
- const name = strName2(target);
4018
+ const name = strName6(target);
3368
4019
  if (!name) return;
3369
4020
  if (isInterpolated(name)) return;
3370
4021
  if (ownObjects.has(name)) return;
3371
- if ((0, import_system.isPlatformProvidedObjectName)(name)) return;
3372
- if ((0, import_system.hasPlatformObjectPrefix)(name)) {
4022
+ if ((0, import_system2.isPlatformProvidedObjectName)(name)) return;
4023
+ if ((0, import_system2.hasPlatformObjectPrefix)(name)) {
3373
4024
  findings.push({
3374
4025
  severity: "warning",
3375
4026
  rule: OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
3376
4027
  where,
3377
4028
  path,
3378
- 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),
4029
+ 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),
3379
4030
  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}`
3380
4031
  });
3381
4032
  return;
@@ -3385,26 +4036,26 @@ function validateObjectReferences(stack) {
3385
4036
  rule: OBJECT_REFERENCE_UNKNOWN,
3386
4037
  where,
3387
4038
  path,
3388
- 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),
4039
+ 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),
3389
4040
  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(", ")}.` : "")
3390
4041
  });
3391
4042
  };
3392
4043
  const checkActionParams2 = (action, actionPath, actionLabel) => {
3393
- const params = asArray22(action.params);
4044
+ const params = asArray24(action.params);
3394
4045
  for (let pi = 0; pi < params.length; pi++) {
3395
4046
  const param = params[pi];
3396
4047
  if (!param || typeof param !== "object") continue;
3397
- const paramLabel = strName2(param.name) ?? strName2(param.field) ?? `#${pi}`;
4048
+ const paramLabel = strName6(param.name) ?? strName6(param.field) ?? `#${pi}`;
3398
4049
  const where = `${actionLabel} \xB7 param "${paramLabel}"`;
3399
4050
  check(
3400
- strName2(param.reference),
4051
+ strName6(param.reference),
3401
4052
  where,
3402
4053
  `${actionPath}.params[${pi}].reference`,
3403
4054
  "record-picker target",
3404
4055
  "Without a resolvable target the picker degrades to a raw record-id text input."
3405
4056
  );
3406
4057
  check(
3407
- strName2(param.objectOverride),
4058
+ strName6(param.objectOverride),
3408
4059
  where,
3409
4060
  `${actionPath}.params[${pi}].objectOverride`,
3410
4061
  "field-backed param object",
@@ -3412,70 +4063,70 @@ function validateObjectReferences(stack) {
3412
4063
  );
3413
4064
  }
3414
4065
  };
3415
- const globalActions = asArray22(stack.actions);
4066
+ const globalActions = asArray24(stack.actions);
3416
4067
  for (let ai = 0; ai < globalActions.length; ai++) {
3417
4068
  const action = globalActions[ai];
3418
4069
  if (!action || typeof action !== "object") continue;
3419
- checkActionParams2(action, `actions[${ai}]`, `action "${strName2(action.name) ?? `#${ai}`}"`);
4070
+ checkActionParams2(action, `actions[${ai}]`, `action "${strName6(action.name) ?? `#${ai}`}"`);
3420
4071
  }
3421
4072
  for (let oi = 0; oi < objects.length; oi++) {
3422
4073
  const obj = objects[oi];
3423
4074
  if (!obj || typeof obj !== "object") continue;
3424
- const objName = strName2(obj.name) ?? `#${oi}`;
3425
- const objActions = asArray22(obj.actions);
4075
+ const objName = strName6(obj.name) ?? `#${oi}`;
4076
+ const objActions = asArray24(obj.actions);
3426
4077
  for (let ai = 0; ai < objActions.length; ai++) {
3427
4078
  const action = objActions[ai];
3428
4079
  if (!action || typeof action !== "object") continue;
3429
4080
  checkActionParams2(
3430
4081
  action,
3431
4082
  `objects[${oi}].actions[${ai}]`,
3432
- `object "${objName}" \xB7 action "${strName2(action.name) ?? `#${ai}`}"`
4083
+ `object "${objName}" \xB7 action "${strName6(action.name) ?? `#${ai}`}"`
3433
4084
  );
3434
4085
  }
3435
4086
  }
3436
- const dashboards = asArray22(stack.dashboards);
4087
+ const dashboards = asArray24(stack.dashboards);
3437
4088
  for (let di = 0; di < dashboards.length; di++) {
3438
4089
  const dash = dashboards[di];
3439
4090
  if (!dash || typeof dash !== "object") continue;
3440
- const dashName = strName2(dash.name) ?? `#${di}`;
3441
- const filters = asArray22(dash.globalFilters);
4091
+ const dashName = strName6(dash.name) ?? `#${di}`;
4092
+ const filters = asArray24(dash.globalFilters);
3442
4093
  for (let fi = 0; fi < filters.length; fi++) {
3443
4094
  const filter = filters[fi];
3444
4095
  if (!filter || typeof filter !== "object") continue;
3445
4096
  const optionsFrom = filter.optionsFrom;
3446
4097
  if (!optionsFrom || typeof optionsFrom !== "object") continue;
3447
4098
  check(
3448
- strName2(optionsFrom.object),
3449
- `dashboard "${dashName}" \xB7 filter "${strName2(filter.name) ?? `#${fi}`}"`,
4099
+ strName6(optionsFrom.object),
4100
+ `dashboard "${dashName}" \xB7 filter "${strName6(filter.name) ?? `#${fi}`}"`,
3450
4101
  `dashboards[${di}].globalFilters[${fi}].optionsFrom.object`,
3451
4102
  "filter options source",
3452
4103
  "The dropdown fetches its options from this object; an unknown one renders an always-empty filter."
3453
4104
  );
3454
4105
  }
3455
4106
  }
3456
- const apps = asArray22(stack.apps);
4107
+ const apps = asArray24(stack.apps);
3457
4108
  for (let ai = 0; ai < apps.length; ai++) {
3458
4109
  const app = apps[ai];
3459
4110
  if (!app || typeof app !== "object") continue;
3460
- const appName = strName2(app.name) ?? `#${ai}`;
4111
+ const appName = strName6(app.name) ?? `#${ai}`;
3461
4112
  const walkNav = (items, basePath) => {
3462
- const navItems = asArray22(items);
4113
+ const navItems = asArray24(items);
3463
4114
  for (let ni = 0; ni < navItems.length; ni++) {
3464
4115
  const nav = navItems[ni];
3465
4116
  if (!nav || typeof nav !== "object") continue;
3466
- const navId = strName2(nav.id) ?? `#${ni}`;
4117
+ const navId = strName6(nav.id) ?? `#${ni}`;
3467
4118
  const where = `app "${appName}" \xB7 nav "${navId}"`;
3468
4119
  const navPath = `${basePath}[${ni}]`;
3469
4120
  check(
3470
- strName2(nav.requiresObject),
4121
+ strName6(nav.requiresObject),
3471
4122
  where,
3472
4123
  `${navPath}.requiresObject`,
3473
4124
  "capability gate object",
3474
4125
  "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."
3475
4126
  );
3476
- if (nav.requiresObject && strName2(nav.objectName)) {
4127
+ if (nav.requiresObject && strName6(nav.objectName)) {
3477
4128
  check(
3478
- strName2(nav.objectName),
4129
+ strName6(nav.objectName),
3479
4130
  where,
3480
4131
  `${navPath}.objectName`,
3481
4132
  "navigation target",
@@ -3486,7 +4137,7 @@ function validateObjectReferences(stack) {
3486
4137
  }
3487
4138
  };
3488
4139
  walkNav(app.navigation, `apps[${ai}].navigation`);
3489
- const areas = asArray22(app.areas);
4140
+ const areas = asArray24(app.areas);
3490
4141
  for (let ri = 0; ri < areas.length; ri++) {
3491
4142
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
3492
4143
  }
@@ -3494,88 +4145,22 @@ function validateObjectReferences(stack) {
3494
4145
  return findings;
3495
4146
  }
3496
4147
 
3497
- // src/page-walk.ts
3498
- function isRec2(v) {
3499
- return !!v && typeof v === "object" && !Array.isArray(v);
3500
- }
3501
- function strName3(v) {
3502
- return typeof v === "string" && v.length > 0 ? v : void 0;
3503
- }
3504
- var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
3505
- function isSourceAuthoredPage(page) {
3506
- const kind = strName3(page.kind);
3507
- return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
3508
- }
3509
- function walkPageComponents(page, pagePath) {
3510
- const out = [];
3511
- if (!isRec2(page) || isSourceAuthoredPage(page)) return out;
3512
- const pageObject = strName3(page.object);
3513
- const visit = (node, path, inheritedObject) => {
3514
- if (!isRec2(node)) return;
3515
- const props = isRec2(node.properties) ? node.properties : void 0;
3516
- const dataSource = isRec2(node.dataSource) ? node.dataSource : void 0;
3517
- const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
3518
- out.push({ component: node, path, objectName });
3519
- if (!props) return;
3520
- if (Array.isArray(props.items)) {
3521
- for (let i = 0; i < props.items.length; i++) {
3522
- const item = props.items[i];
3523
- if (!isRec2(item) || !Array.isArray(item.children)) continue;
3524
- for (let c = 0; c < item.children.length; c++) {
3525
- visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
3526
- }
3527
- }
3528
- }
3529
- if (Array.isArray(props.children)) {
3530
- for (let i = 0; i < props.children.length; i++) {
3531
- visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
3532
- }
3533
- }
3534
- for (const key of ["body", "footer"]) {
3535
- const slotList = props[key];
3536
- if (!Array.isArray(slotList)) continue;
3537
- for (let i = 0; i < slotList.length; i++) {
3538
- visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
3539
- }
3540
- }
3541
- };
3542
- const regions = Array.isArray(page.regions) ? page.regions : [];
3543
- for (let r = 0; r < regions.length; r++) {
3544
- const region = regions[r];
3545
- if (!isRec2(region) || !Array.isArray(region.components)) continue;
3546
- for (let c = 0; c < region.components.length; c++) {
3547
- visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
3548
- }
3549
- }
3550
- const slots = isRec2(page.slots) ? page.slots : void 0;
3551
- if (slots) {
3552
- for (const [slot, value] of Object.entries(slots)) {
3553
- const list3 = Array.isArray(value) ? value : [value];
3554
- const indexed = Array.isArray(value);
3555
- for (let i = 0; i < list3.length; i++) {
3556
- visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
3557
- }
3558
- }
3559
- }
3560
- return out;
3561
- }
3562
-
3563
4148
  // src/validate-action-name-refs.ts
3564
4149
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
3565
- function asArray23(v) {
4150
+ function asArray25(v) {
3566
4151
  if (Array.isArray(v)) return v;
3567
4152
  if (v && typeof v === "object") {
3568
4153
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3569
4154
  }
3570
4155
  return [];
3571
4156
  }
3572
- function strName4(v) {
4157
+ function strName7(v) {
3573
4158
  return typeof v === "string" && v.length > 0 ? v : void 0;
3574
4159
  }
3575
4160
  function strList(v) {
3576
4161
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
3577
4162
  }
3578
- function distance2(a, b) {
4163
+ function distance3(a, b) {
3579
4164
  const m = a.length;
3580
4165
  const n = b.length;
3581
4166
  if (m === 0) return n;
@@ -3591,11 +4176,11 @@ function distance2(a, b) {
3591
4176
  }
3592
4177
  return prev[n];
3593
4178
  }
3594
- function suggest3(target, known) {
4179
+ function suggest4(target, known) {
3595
4180
  let best;
3596
4181
  let bestScore = Infinity;
3597
4182
  for (const candidate of known) {
3598
- const d = distance2(target, candidate);
4183
+ const d = distance3(target, candidate);
3599
4184
  if (d < bestScore) {
3600
4185
  bestScore = d;
3601
4186
  best = candidate;
@@ -3606,14 +4191,14 @@ function suggest3(target, known) {
3606
4191
  }
3607
4192
  function collectActionNames(stack) {
3608
4193
  const names = /* @__PURE__ */ new Set();
3609
- for (const action of asArray23(stack.actions)) {
3610
- const n = strName4(action?.name);
4194
+ for (const action of asArray25(stack.actions)) {
4195
+ const n = strName7(action?.name);
3611
4196
  if (n) names.add(n);
3612
4197
  }
3613
- for (const obj of asArray23(stack.objects)) {
4198
+ for (const obj of asArray25(stack.objects)) {
3614
4199
  if (!obj || typeof obj !== "object") continue;
3615
- for (const action of asArray23(obj.actions)) {
3616
- const n = strName4(action?.name);
4200
+ for (const action of asArray25(obj.actions)) {
4201
+ const n = strName7(action?.name);
3617
4202
  if (n) names.add(n);
3618
4203
  }
3619
4204
  }
@@ -3630,15 +4215,15 @@ function validateActionNameRefs(stack) {
3630
4215
  rule: ACTION_NAME_UNDEFINED,
3631
4216
  where,
3632
4217
  path,
3633
- 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),
4218
+ 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),
3634
4219
  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(", ")}.` : "")
3635
4220
  });
3636
4221
  };
3637
- const views = asArray23(stack.views);
4222
+ const views = asArray25(stack.views);
3638
4223
  for (let vi = 0; vi < views.length; vi++) {
3639
4224
  const view = views[vi];
3640
4225
  if (!view || typeof view !== "object") continue;
3641
- const viewName = strName4(view.name) ?? strName4(view.object) ?? `#${vi}`;
4226
+ const viewName = strName7(view.name) ?? strName7(view.object) ?? `#${vi}`;
3642
4227
  const checkListContainer = (container, label2, path) => {
3643
4228
  if (!container || typeof container !== "object") return;
3644
4229
  const list3 = container;
@@ -3649,238 +4234,66 @@ function validateActionNameRefs(stack) {
3649
4234
  names[ai],
3650
4235
  `view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
3651
4236
  `${path}.${key}[${ai}]`,
3652
- key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
3653
- );
3654
- }
3655
- }
3656
- };
3657
- checkListContainer(view.list, "list", `views[${vi}].list`);
3658
- const listViews = view.listViews;
3659
- if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
3660
- for (const [key, lv] of Object.entries(listViews)) {
3661
- checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
3662
- }
3663
- }
3664
- }
3665
- const pages = asArray23(stack.pages);
3666
- for (let pi = 0; pi < pages.length; pi++) {
3667
- const page = pages[pi];
3668
- if (!page || typeof page !== "object") continue;
3669
- const pageName = strName4(page.name) ?? `#${pi}`;
3670
- for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
3671
- const props = component.properties;
3672
- if (!props || typeof props !== "object") continue;
3673
- const names = strList(props.actionNames);
3674
- for (let ai = 0; ai < names.length; ai++) {
3675
- check(
3676
- names[ai],
3677
- `page "${pageName}" \xB7 component "${strName4(component.type) ?? "?"}"`,
3678
- `${path}.properties.actionNames[${ai}]`,
3679
- "Quick-actions bar"
3680
- );
3681
- }
3682
- }
3683
- }
3684
- const apps = asArray23(stack.apps);
3685
- for (let ai = 0; ai < apps.length; ai++) {
3686
- const app = apps[ai];
3687
- if (!app || typeof app !== "object") continue;
3688
- const appName = strName4(app.name) ?? `#${ai}`;
3689
- const walkNav = (items, basePath) => {
3690
- const navItems = asArray23(items);
3691
- for (let ni = 0; ni < navItems.length; ni++) {
3692
- const nav = navItems[ni];
3693
- if (!nav || typeof nav !== "object") continue;
3694
- const navPath = `${basePath}[${ni}]`;
3695
- const actionDef = nav.actionDef;
3696
- const actionName = strName4(actionDef?.actionName);
3697
- if (nav.type === "action" && actionName) {
3698
- check(
3699
- actionName,
3700
- `app "${appName}" \xB7 nav "${strName4(nav.id) ?? `#${ni}`}"`,
3701
- `${navPath}.actionDef.actionName`,
3702
- "Navigation action item"
3703
- );
3704
- }
3705
- if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
3706
- }
3707
- };
3708
- walkNav(app.navigation, `apps[${ai}].navigation`);
3709
- const areas = asArray23(app.areas);
3710
- for (let ri = 0; ri < areas.length; ri++) {
3711
- walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
3712
- }
3713
- }
3714
- return findings;
3715
- }
3716
-
3717
- // src/validate-page-field-bindings.ts
3718
- var PAGE_FIELD_UNKNOWN = "page-field-unknown";
3719
- var SYSTEM_FIELDS4 = /* @__PURE__ */ new Set([
3720
- "id",
3721
- "created_at",
3722
- "created_by",
3723
- "updated_at",
3724
- "updated_by",
3725
- "owner_id",
3726
- "organization_id",
3727
- "tenant_id",
3728
- "user_id",
3729
- "deleted_at"
3730
- ]);
3731
- function asArray24(v) {
3732
- if (Array.isArray(v)) return v;
3733
- if (v && typeof v === "object") {
3734
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3735
- }
3736
- return [];
3737
- }
3738
- function strName5(v) {
3739
- return typeof v === "string" && v.length > 0 ? v : void 0;
3740
- }
3741
- function isRec3(v) {
3742
- return !!v && typeof v === "object" && !Array.isArray(v);
3743
- }
3744
- function fieldRefsFrom(value, basePath) {
3745
- const out = [];
3746
- const one = (v, path) => {
3747
- const bare = strName5(v);
3748
- if (bare) {
3749
- out.push({ name: bare, path });
3750
- return;
3751
- }
3752
- if (!isRec3(v)) return;
3753
- const named = strName5(v.field) ?? strName5(v.name);
3754
- if (named) out.push({ name: named, path: `${path}.${strName5(v.field) ? "field" : "name"}` });
3755
- };
3756
- if (Array.isArray(value)) {
3757
- for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
3758
- } else {
3759
- one(value, basePath);
3760
- }
3761
- return out;
3762
- }
3763
- var COMPONENT_FIELD_SPECS = {
3764
- "record:highlights": { props: ["fields"] },
3765
- // `sections`/`hideFields` are not in RecordDetailsProps, but every real page
3766
- // authors them (they survive because `properties` is unvalidated).
3767
- "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
3768
- "record:path": { props: ["statusField"] },
3769
- "element:number": { props: ["field"] },
3770
- "element:filter": { props: ["fields"] },
3771
- "element:form": { props: ["fields"] },
3772
- // The schema says `displayField`; real pages author `labelField`. Accept both.
3773
- "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
3774
- };
3775
- var RELATED_LIST_TYPE = "record:related_list";
3776
- function validatePageFieldBindings(stack) {
3777
- const findings = [];
3778
- if (!stack || typeof stack !== "object") return findings;
3779
- const objectFields = /* @__PURE__ */ new Map();
3780
- for (const obj of asArray24(stack.objects)) {
3781
- const name = strName5(obj.name);
3782
- if (!name) continue;
3783
- const names = /* @__PURE__ */ new Set();
3784
- for (const f of asArray24(obj.fields)) {
3785
- const fn = strName5(f.name);
3786
- if (fn) names.add(fn);
3787
- }
3788
- objectFields.set(name, names);
3789
- }
3790
- const pages = asArray24(stack.pages);
3791
- for (let pi = 0; pi < pages.length; pi++) {
3792
- const page = pages[pi];
3793
- if (!page || typeof page !== "object") continue;
3794
- const pageName = strName5(page.name) ?? `#${pi}`;
3795
- const checkRefs = (refs, objectName, where) => {
3796
- if (!objectName) return;
3797
- const known = objectFields.get(objectName);
3798
- if (!known) return;
3799
- for (const ref of refs) {
3800
- if (ref.name.includes(".")) continue;
3801
- if (known.has(ref.name) || SYSTEM_FIELDS4.has(ref.name)) continue;
3802
- findings.push({
3803
- severity: "warning",
3804
- rule: PAGE_FIELD_UNKNOWN,
3805
- where,
3806
- path: ref.path,
3807
- message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 the component silently skips it, so it never renders.`,
3808
- 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(", ")}.` : "")
3809
- });
3810
- }
3811
- };
3812
- for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
3813
- const type = strName5(component.type);
3814
- const props = isRec3(component.properties) ? component.properties : void 0;
3815
- if (!type || !props) continue;
3816
- const where = `page "${pageName}" \xB7 ${type}`;
3817
- if (type === RELATED_LIST_TYPE) {
3818
- const relatedObject = strName5(props.objectName);
3819
- const relatedRefs = [
3820
- ...fieldRefsFrom(props.columns, `${path}.properties.columns`),
3821
- ...fieldRefsFrom(props.sort, `${path}.properties.sort`),
3822
- ...fieldRefsFrom(props.filter, `${path}.properties.filter`),
3823
- ...fieldRefsFrom(props.relationshipField, `${path}.properties.relationshipField`)
3824
- ];
3825
- checkRefs(relatedRefs, relatedObject, where);
3826
- checkRefs(
3827
- fieldRefsFrom(props.relationshipValueField, `${path}.properties.relationshipValueField`),
3828
- objectName,
3829
- where
3830
- );
3831
- const add = isRec3(props.add) ? props.add : void 0;
3832
- const picker = add && isRec3(add.picker) ? add.picker : void 0;
3833
- if (picker) {
3834
- checkRefs(
3835
- [
3836
- ...fieldRefsFrom(picker.valueField, `${path}.properties.add.picker.valueField`),
3837
- ...fieldRefsFrom(picker.labelField, `${path}.properties.add.picker.labelField`)
3838
- ],
3839
- strName5(picker.object),
3840
- where
3841
- );
3842
- }
3843
- if (add) {
3844
- checkRefs(
3845
- fieldRefsFrom(add.linkField, `${path}.properties.add.linkField`),
3846
- relatedObject,
3847
- where
4237
+ key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
3848
4238
  );
3849
4239
  }
3850
- continue;
3851
4240
  }
3852
- const spec = COMPONENT_FIELD_SPECS[type];
3853
- if (!spec) continue;
3854
- const refs = [];
3855
- for (const key of spec.props ?? []) {
3856
- refs.push(...fieldRefsFrom(props[key], `${path}.properties.${key}`));
4241
+ };
4242
+ checkListContainer(view.list, "list", `views[${vi}].list`);
4243
+ const listViews = view.listViews;
4244
+ if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
4245
+ for (const [key, lv] of Object.entries(listViews)) {
4246
+ checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
3857
4247
  }
3858
- for (const key of spec.nestedSections ?? []) {
3859
- const sections = Array.isArray(props[key]) ? props[key] : [];
3860
- for (let si = 0; si < sections.length; si++) {
3861
- const section = sections[si];
3862
- if (!isRec3(section)) continue;
3863
- refs.push(
3864
- ...fieldRefsFrom(section.fields, `${path}.properties.${key}[${si}].fields`)
3865
- );
3866
- }
4248
+ }
4249
+ }
4250
+ const pages = asArray25(stack.pages);
4251
+ for (let pi = 0; pi < pages.length; pi++) {
4252
+ const page = pages[pi];
4253
+ if (!page || typeof page !== "object") continue;
4254
+ const pageName = strName7(page.name) ?? `#${pi}`;
4255
+ for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4256
+ const props = component.properties;
4257
+ if (!props || typeof props !== "object") continue;
4258
+ const names = strList(props.actionNames);
4259
+ for (let ai = 0; ai < names.length; ai++) {
4260
+ check(
4261
+ names[ai],
4262
+ `page "${pageName}" \xB7 component "${strName7(component.type) ?? "?"}"`,
4263
+ `${path}.properties.actionNames[${ai}]`,
4264
+ "Quick-actions bar"
4265
+ );
3867
4266
  }
3868
- checkRefs(refs, objectName, where);
3869
4267
  }
3870
- const cfg = isRec3(page.interfaceConfig) ? page.interfaceConfig : void 0;
3871
- if (cfg) {
3872
- const cfgObject = strName5(cfg.source) ?? strName5(page.object);
3873
- const base = `pages[${pi}].interfaceConfig`;
3874
- const refs = [
3875
- ...fieldRefsFrom(cfg.columns, `${base}.columns`),
3876
- ...fieldRefsFrom(cfg.sort, `${base}.sort`),
3877
- ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
3878
- ];
3879
- const userFilters = isRec3(cfg.userFilters) ? cfg.userFilters : void 0;
3880
- if (userFilters) {
3881
- refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
4268
+ }
4269
+ const apps = asArray25(stack.apps);
4270
+ for (let ai = 0; ai < apps.length; ai++) {
4271
+ const app = apps[ai];
4272
+ if (!app || typeof app !== "object") continue;
4273
+ const appName = strName7(app.name) ?? `#${ai}`;
4274
+ const walkNav = (items, basePath) => {
4275
+ const navItems = asArray25(items);
4276
+ for (let ni = 0; ni < navItems.length; ni++) {
4277
+ const nav = navItems[ni];
4278
+ if (!nav || typeof nav !== "object") continue;
4279
+ const navPath = `${basePath}[${ni}]`;
4280
+ const actionDef = nav.actionDef;
4281
+ const actionName = strName7(actionDef?.actionName);
4282
+ if (nav.type === "action" && actionName) {
4283
+ check(
4284
+ actionName,
4285
+ `app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
4286
+ `${navPath}.actionDef.actionName`,
4287
+ "Navigation action item"
4288
+ );
4289
+ }
4290
+ if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
3882
4291
  }
3883
- checkRefs(refs, cfgObject, `page "${pageName}" \xB7 interfaceConfig`);
4292
+ };
4293
+ walkNav(app.navigation, `apps[${ai}].navigation`);
4294
+ const areas = asArray25(app.areas);
4295
+ for (let ri = 0; ri < areas.length; ri++) {
4296
+ walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
3884
4297
  }
3885
4298
  }
3886
4299
  return findings;
@@ -3891,23 +4304,23 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
3891
4304
  var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
3892
4305
  var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
3893
4306
  var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
3894
- function asArray25(v) {
4307
+ function asArray26(v) {
3895
4308
  if (Array.isArray(v)) return v;
3896
4309
  if (v && typeof v === "object") {
3897
4310
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3898
4311
  }
3899
4312
  return [];
3900
4313
  }
3901
- function strName6(v) {
4314
+ function strName8(v) {
3902
4315
  return typeof v === "string" && v.length > 0 ? v : void 0;
3903
4316
  }
3904
4317
  function strList2(v) {
3905
4318
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
3906
4319
  }
3907
- function isRec4(v) {
4320
+ function isRec6(v) {
3908
4321
  return !!v && typeof v === "object" && !Array.isArray(v);
3909
4322
  }
3910
- function distance3(a, b) {
4323
+ function distance4(a, b) {
3911
4324
  const m = a.length;
3912
4325
  const n = b.length;
3913
4326
  if (m === 0) return n;
@@ -3923,11 +4336,11 @@ function distance3(a, b) {
3923
4336
  }
3924
4337
  return prev[n];
3925
4338
  }
3926
- function suggest4(target, known) {
4339
+ function suggest5(target, known) {
3927
4340
  let best;
3928
4341
  let bestScore = Infinity;
3929
4342
  for (const c of known) {
3930
- const d = distance3(target, c);
4343
+ const d = distance4(target, c);
3931
4344
  if (d < bestScore) {
3932
4345
  bestScore = d;
3933
4346
  best = c;
@@ -3942,17 +4355,17 @@ function list2(names) {
3942
4355
  }
3943
4356
  function indexDatasets(stack) {
3944
4357
  const out = /* @__PURE__ */ new Map();
3945
- for (const ds of asArray25(stack.datasets)) {
3946
- const name = strName6(ds.name);
4358
+ for (const ds of asArray26(stack.datasets)) {
4359
+ const name = strName8(ds.name);
3947
4360
  if (!name) continue;
3948
4361
  const dimensions = /* @__PURE__ */ new Set();
3949
- for (const d of asArray25(ds.dimensions)) {
3950
- const n = strName6(d.name);
4362
+ for (const d of asArray26(ds.dimensions)) {
4363
+ const n = strName8(d.name);
3951
4364
  if (n) dimensions.add(n);
3952
4365
  }
3953
4366
  const measures = /* @__PURE__ */ new Set();
3954
- for (const m of asArray25(ds.measures)) {
3955
- const n = strName6(m.name);
4367
+ for (const m of asArray26(ds.measures)) {
4368
+ const n = strName8(m.name);
3956
4369
  if (n) measures.add(n);
3957
4370
  }
3958
4371
  out.set(name, { dimensions, measures });
@@ -3975,7 +4388,7 @@ function validateChartBindings(stack) {
3975
4388
  where: binding.where,
3976
4389
  path: `${binding.path}.dataset`,
3977
4390
  message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
3978
- hint: `Declared datasets: ${list2(datasets.keys())}.${suggest4(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
4391
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
3979
4392
  });
3980
4393
  return;
3981
4394
  }
@@ -3987,7 +4400,7 @@ function validateChartBindings(stack) {
3987
4400
  where: binding.where,
3988
4401
  path,
3989
4402
  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.`,
3990
- hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest4(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
4403
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
3991
4404
  });
3992
4405
  };
3993
4406
  const measureRef = (name, path, selected2) => {
@@ -3998,7 +4411,7 @@ function validateChartBindings(stack) {
3998
4411
  where: binding.where,
3999
4412
  path,
4000
4413
  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.`,
4001
- hint: `Dataset measures: ${list2(ds.measures)}.${suggest4(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
4414
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
4002
4415
  });
4003
4416
  return;
4004
4417
  }
@@ -4030,29 +4443,29 @@ function validateChartBindings(stack) {
4030
4443
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
4031
4444
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
4032
4445
  };
4033
- const reports = asArray25(stack.reports);
4446
+ const reports = asArray26(stack.reports);
4034
4447
  for (let ri = 0; ri < reports.length; ri++) {
4035
4448
  const report = reports[ri];
4036
- if (!isRec4(report)) continue;
4037
- const reportName = strName6(report.name) ?? `#${ri}`;
4449
+ if (!isRec6(report)) continue;
4450
+ const reportName = strName8(report.name) ?? `#${ri}`;
4038
4451
  const checkReportChart = (chart, dataset, values, where, path) => {
4039
- if (!isRec4(chart)) return;
4452
+ if (!isRec6(chart)) return;
4040
4453
  check({
4041
4454
  dataset,
4042
4455
  // `values` is the report's measure SELECTION, not a chart ref; feeding
4043
4456
  // it in lets the yAxis "declared but not selected" check work without
4044
4457
  // reporting the selection itself twice.
4045
4458
  values: { names: values, path: `${path}.values` },
4046
- xAxis: strName6(chart.xAxis) ? { name: strName6(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
4047
- yAxis: strName6(chart.yAxis) ? { name: strName6(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
4048
- series: asArray25(chart.series).map((s, si) => ({ name: strName6(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
4459
+ xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
4460
+ yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
4461
+ series: asArray26(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
4049
4462
  where,
4050
4463
  path: `${path}.chart`
4051
4464
  });
4052
4465
  };
4053
4466
  checkReportChart(
4054
4467
  report.chart,
4055
- strName6(report.dataset),
4468
+ strName8(report.dataset),
4056
4469
  strList2(report.values),
4057
4470
  `report "${reportName}" \xB7 chart`,
4058
4471
  `reports[${ri}]`
@@ -4060,45 +4473,45 @@ function validateChartBindings(stack) {
4060
4473
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
4061
4474
  for (let bi = 0; bi < blocks.length; bi++) {
4062
4475
  const block = blocks[bi];
4063
- if (!isRec4(block)) continue;
4476
+ if (!isRec6(block)) continue;
4064
4477
  checkReportChart(
4065
4478
  block.chart,
4066
- strName6(block.dataset),
4479
+ strName8(block.dataset),
4067
4480
  strList2(block.values),
4068
- `report "${reportName}" \xB7 block "${strName6(block.name) ?? `#${bi}`}" chart`,
4481
+ `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
4069
4482
  `reports[${ri}].blocks[${bi}]`
4070
4483
  );
4071
4484
  }
4072
4485
  }
4073
4486
  const checkListChart = (container, where, path) => {
4074
- if (!isRec4(container)) return;
4487
+ if (!isRec6(container)) return;
4075
4488
  const chart = container.chart;
4076
- if (!isRec4(chart)) return;
4489
+ if (!isRec6(chart)) return;
4077
4490
  check({
4078
- dataset: strName6(chart.dataset),
4491
+ dataset: strName8(chart.dataset),
4079
4492
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
4080
4493
  values: { names: strList2(chart.values), path: `${path}.chart.values` },
4081
4494
  where,
4082
4495
  path: `${path}.chart`
4083
4496
  });
4084
4497
  };
4085
- const views = asArray25(stack.views);
4498
+ const views = asArray26(stack.views);
4086
4499
  for (let vi = 0; vi < views.length; vi++) {
4087
4500
  const view = views[vi];
4088
- if (!isRec4(view)) continue;
4089
- const viewName = strName6(view.name) ?? strName6(view.objectName) ?? `#${vi}`;
4501
+ if (!isRec6(view)) continue;
4502
+ const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
4090
4503
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
4091
- if (isRec4(view.listViews)) {
4504
+ if (isRec6(view.listViews)) {
4092
4505
  for (const [key, lv] of Object.entries(view.listViews)) {
4093
4506
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
4094
4507
  }
4095
4508
  }
4096
4509
  }
4097
- const objects = asArray25(stack.objects);
4510
+ const objects = asArray26(stack.objects);
4098
4511
  for (let oi = 0; oi < objects.length; oi++) {
4099
4512
  const obj = objects[oi];
4100
- if (!isRec4(obj) || !isRec4(obj.listViews)) continue;
4101
- const objName = strName6(obj.name) ?? `#${oi}`;
4513
+ if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
4514
+ const objName = strName8(obj.name) ?? `#${oi}`;
4102
4515
  for (const [key, lv] of Object.entries(obj.listViews)) {
4103
4516
  checkListChart(
4104
4517
  lv,
@@ -4107,22 +4520,22 @@ function validateChartBindings(stack) {
4107
4520
  );
4108
4521
  }
4109
4522
  }
4110
- const pages = asArray25(stack.pages);
4523
+ const pages = asArray26(stack.pages);
4111
4524
  for (let pi = 0; pi < pages.length; pi++) {
4112
4525
  const page = pages[pi];
4113
- if (!isRec4(page)) continue;
4114
- const pageName = strName6(page.name) ?? `#${pi}`;
4526
+ if (!isRec6(page)) continue;
4527
+ const pageName = strName8(page.name) ?? `#${pi}`;
4115
4528
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4116
- const props = isRec4(component.properties) ? component.properties : void 0;
4117
- if (!props || !strName6(props.dataset)) continue;
4118
- const axisRefs = asArray25(props.yAxis).map((a, ai) => ({ name: strName6(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
4119
- const seriesRefs = asArray25(props.series).map((s, si) => ({ name: strName6(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
4529
+ const props = isRec6(component.properties) ? component.properties : void 0;
4530
+ if (!props || !strName8(props.dataset)) continue;
4531
+ const axisRefs = asArray26(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
4532
+ const seriesRefs = asArray26(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
4120
4533
  check({
4121
- dataset: strName6(props.dataset),
4534
+ dataset: strName8(props.dataset),
4122
4535
  dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
4123
4536
  values: { names: strList2(props.values), path: `${path}.properties.values` },
4124
4537
  series: [...axisRefs, ...seriesRefs],
4125
- where: `page "${pageName}" \xB7 ${strName6(component.type) ?? "chart"}`,
4538
+ where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
4126
4539
  path: `${path}.properties`
4127
4540
  });
4128
4541
  }
@@ -4131,10 +4544,10 @@ function validateChartBindings(stack) {
4131
4544
  }
4132
4545
 
4133
4546
  // src/validate-nav-access.ts
4134
- var import_system2 = require("@objectstack/spec/system");
4547
+ var import_system3 = require("@objectstack/spec/system");
4135
4548
 
4136
4549
  // src/build-access-matrix.ts
4137
- function asArray26(v) {
4550
+ function asArray27(v) {
4138
4551
  if (Array.isArray(v)) return v;
4139
4552
  if (v && typeof v === "object") {
4140
4553
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4145,13 +4558,13 @@ function buildAccessMatrix(stack) {
4145
4558
  const entries = [];
4146
4559
  if (!stack || typeof stack !== "object") return { version: 1, entries };
4147
4560
  const owdByObject = /* @__PURE__ */ new Map();
4148
- for (const obj of asArray26(stack.objects)) {
4561
+ for (const obj of asArray27(stack.objects)) {
4149
4562
  const name = typeof obj.name === "string" ? obj.name : "";
4150
4563
  if (!name) continue;
4151
4564
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
4152
4565
  if (typeof owd === "string") owdByObject.set(name, owd);
4153
4566
  }
4154
- for (const ps of asArray26(stack.permissions)) {
4567
+ for (const ps of asArray27(stack.permissions)) {
4155
4568
  const psName = typeof ps.name === "string" ? ps.name : "";
4156
4569
  if (!psName) continue;
4157
4570
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -4224,34 +4637,34 @@ function diffAccessMatrix(before, after) {
4224
4637
 
4225
4638
  // src/validate-nav-access.ts
4226
4639
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
4227
- function asArray27(v) {
4640
+ function asArray28(v) {
4228
4641
  if (Array.isArray(v)) return v;
4229
4642
  if (v && typeof v === "object") {
4230
4643
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4231
4644
  }
4232
4645
  return [];
4233
4646
  }
4234
- function strName7(v) {
4647
+ function strName9(v) {
4235
4648
  return typeof v === "string" && v.length > 0 ? v : void 0;
4236
4649
  }
4237
4650
  function collectNavExposures(stack) {
4238
4651
  const out = [];
4239
- const apps = asArray27(stack.apps);
4652
+ const apps = asArray28(stack.apps);
4240
4653
  for (let ai = 0; ai < apps.length; ai++) {
4241
4654
  const app = apps[ai];
4242
4655
  if (!app || typeof app !== "object") continue;
4243
- const appName = strName7(app.name) ?? `#${ai}`;
4656
+ const appName = strName9(app.name) ?? `#${ai}`;
4244
4657
  const walk = (items, basePath) => {
4245
- const navItems = asArray27(items);
4658
+ const navItems = asArray28(items);
4246
4659
  for (let ni = 0; ni < navItems.length; ni++) {
4247
4660
  const nav = navItems[ni];
4248
4661
  if (!nav || typeof nav !== "object") continue;
4249
4662
  const navPath = `${basePath}[${ni}]`;
4250
- const objectName = strName7(nav.objectName);
4663
+ const objectName = strName9(nav.objectName);
4251
4664
  if (nav.type === "object" && objectName) {
4252
4665
  out.push({
4253
4666
  objectName,
4254
- where: `app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
4667
+ where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
4255
4668
  path: `${navPath}.objectName`
4256
4669
  });
4257
4670
  }
@@ -4259,7 +4672,7 @@ function collectNavExposures(stack) {
4259
4672
  }
4260
4673
  };
4261
4674
  walk(app.navigation, `apps[${ai}].navigation`);
4262
- const areas = asArray27(app.areas);
4675
+ const areas = asArray28(app.areas);
4263
4676
  for (let ri = 0; ri < areas.length; ri++) {
4264
4677
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4265
4678
  }
@@ -4269,13 +4682,13 @@ function collectNavExposures(stack) {
4269
4682
  function validateNavAccess(stack) {
4270
4683
  const findings = [];
4271
4684
  if (!stack || typeof stack !== "object") return findings;
4272
- const permissionSets = asArray27(stack.permissions);
4685
+ const permissionSets = asArray28(stack.permissions);
4273
4686
  if (permissionSets.length === 0) return findings;
4274
4687
  const exposures = collectNavExposures(stack);
4275
4688
  if (exposures.length === 0) return findings;
4276
4689
  const ownObjects = /* @__PURE__ */ new Set();
4277
- for (const obj of asArray27(stack.objects)) {
4278
- const n = strName7(obj.name);
4690
+ for (const obj of asArray28(stack.objects)) {
4691
+ const n = strName9(obj.name);
4279
4692
  if (n) ownObjects.add(n);
4280
4693
  }
4281
4694
  const readable = /* @__PURE__ */ new Set();
@@ -4287,7 +4700,7 @@ function validateNavAccess(stack) {
4287
4700
  for (const exposure of exposures) {
4288
4701
  const { objectName } = exposure;
4289
4702
  if (reported.has(objectName)) continue;
4290
- if ((0, import_system2.isPlatformProvidedObjectName)(objectName)) continue;
4703
+ if ((0, import_system3.isPlatformProvidedObjectName)(objectName)) continue;
4291
4704
  if (!ownObjects.has(objectName)) continue;
4292
4705
  if (readable.has(objectName)) continue;
4293
4706
  reported.add(objectName);
@@ -4304,21 +4717,21 @@ function validateNavAccess(stack) {
4304
4717
  }
4305
4718
 
4306
4719
  // src/validate-translation-references.ts
4307
- var import_system3 = require("@objectstack/spec/system");
4720
+ var import_system4 = require("@objectstack/spec/system");
4308
4721
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
4309
4722
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
4310
- function isRec5(v) {
4723
+ function isRec7(v) {
4311
4724
  return !!v && typeof v === "object" && !Array.isArray(v);
4312
4725
  }
4313
- function asArray28(v) {
4726
+ function asArray29(v) {
4314
4727
  if (Array.isArray(v)) return v;
4315
- if (isRec5(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec5(def) ? def : {} }));
4728
+ if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
4316
4729
  return [];
4317
4730
  }
4318
- function strName8(v) {
4731
+ function strName10(v) {
4319
4732
  return typeof v === "string" && v.length > 0 ? v : void 0;
4320
4733
  }
4321
- function distance4(a, b) {
4734
+ function distance5(a, b) {
4322
4735
  const m = a.length;
4323
4736
  const n = b.length;
4324
4737
  if (m === 0) return n;
@@ -4334,7 +4747,7 @@ function distance4(a, b) {
4334
4747
  }
4335
4748
  return prev[n];
4336
4749
  }
4337
- function suggest5(target, known) {
4750
+ function suggest6(target, known) {
4338
4751
  const names = [...known];
4339
4752
  const segmentMatch = names.find(
4340
4753
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -4343,7 +4756,7 @@ function suggest5(target, known) {
4343
4756
  let best;
4344
4757
  let bestScore = Infinity;
4345
4758
  for (const candidate of names) {
4346
- const d = distance4(target, candidate);
4759
+ const d = distance5(target, candidate);
4347
4760
  if (d < bestScore) {
4348
4761
  bestScore = d;
4349
4762
  best = candidate;
@@ -4358,20 +4771,10 @@ function listNames(names, max = 12) {
4358
4771
  const shown = all.slice(0, max).join(", ");
4359
4772
  return all.length > max ? `${shown}, \u2026 (${all.length} total)` : shown;
4360
4773
  }
4361
- var SYSTEM_FIELDS5 = /* @__PURE__ */ new Set([
4362
- "id",
4774
+ var IMPLICIT_FIELDS = /* @__PURE__ */ new Set([
4775
+ ...SYSTEM_FIELDS,
4363
4776
  "_id",
4364
4777
  "name",
4365
- "created_at",
4366
- "created_by",
4367
- "updated_at",
4368
- "updated_by",
4369
- "owner_id",
4370
- "organization_id",
4371
- "tenant_id",
4372
- "user_id",
4373
- "is_deleted",
4374
- "deleted_at",
4375
4778
  "space"
4376
4779
  ]);
4377
4780
  function emptyFacts() {
@@ -4383,20 +4786,20 @@ function collectViewRecord(view, factsFor) {
4383
4786
  const addView = (objectName, name) => {
4384
4787
  if (objectName && name) factsFor(objectName).views.add(name);
4385
4788
  };
4386
- const listBinding = isRec5(view.list) ? bindingOf(view.list) : void 0;
4387
- if (isRec5(view.list)) addView(listBinding, strName8(view.list.name));
4388
- addView(recordObject ?? listBinding, strName8(view.name));
4789
+ const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
4790
+ if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
4791
+ addView(recordObject ?? listBinding, strName10(view.name));
4389
4792
  for (const key of ["listViews", "formViews"]) {
4390
4793
  const container = view[key];
4391
- if (!isRec5(container)) continue;
4794
+ if (!isRec7(container)) continue;
4392
4795
  for (const [subKey, sub] of Object.entries(container)) {
4393
- if (!isRec5(sub)) continue;
4796
+ if (!isRec7(sub)) continue;
4394
4797
  const binding = bindingOf(sub) ?? listBinding;
4395
4798
  addView(binding, subKey);
4396
- addView(binding, strName8(sub.name));
4799
+ addView(binding, strName10(sub.name));
4397
4800
  if (binding) {
4398
- for (const section of asArray28(sub.sections)) {
4399
- const sectionName = strName8(section.name);
4801
+ for (const section of asArray29(sub.sections)) {
4802
+ const sectionName = strName10(section.name);
4400
4803
  if (sectionName) factsFor(binding).sections.add(sectionName);
4401
4804
  }
4402
4805
  }
@@ -4404,14 +4807,14 @@ function collectViewRecord(view, factsFor) {
4404
4807
  }
4405
4808
  const sectionBinding = recordObject ?? listBinding;
4406
4809
  if (sectionBinding) {
4407
- for (const section of asArray28(view.sections)) {
4408
- const sectionName = strName8(section.name);
4810
+ for (const section of asArray29(view.sections)) {
4811
+ const sectionName = strName10(section.name);
4409
4812
  if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
4410
4813
  }
4411
4814
  }
4412
4815
  }
4413
4816
  function viewObjectName(view) {
4414
- return strName8(view.objectName) ?? strName8(view.object) ?? (isRec5(view.data) ? strName8(view.data.object) : void 0);
4817
+ return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
4415
4818
  }
4416
4819
  function readOptions(field) {
4417
4820
  const raw = field.options;
@@ -4423,14 +4826,14 @@ function readOptions(field) {
4423
4826
  values.add(opt);
4424
4827
  continue;
4425
4828
  }
4426
- if (!isRec5(opt)) continue;
4427
- const value = strName8(opt.value);
4829
+ if (!isRec7(opt)) continue;
4830
+ const value = strName10(opt.value);
4428
4831
  if (!value) continue;
4429
4832
  values.add(value);
4430
- const label2 = strName8(opt.label);
4833
+ const label2 = strName10(opt.label);
4431
4834
  if (label2) byLabel.set(label2.toLowerCase(), value);
4432
4835
  }
4433
- } else if (isRec5(raw)) {
4836
+ } else if (isRec7(raw)) {
4434
4837
  for (const [value, label2] of Object.entries(raw)) {
4435
4838
  values.add(value);
4436
4839
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -4450,48 +4853,48 @@ function buildUniverse(stack) {
4450
4853
  }
4451
4854
  return facts;
4452
4855
  };
4453
- for (const obj of asArray28(stack.objects)) {
4454
- const objectName = strName8(obj.name);
4856
+ for (const obj of asArray29(stack.objects)) {
4857
+ const objectName = strName10(obj.name);
4455
4858
  if (!objectName) continue;
4456
4859
  const facts = factsFor(objectName);
4457
- for (const field of asArray28(obj.fields)) {
4458
- const fieldName = strName8(field.name);
4860
+ for (const field of asArray29(obj.fields)) {
4861
+ const fieldName = strName10(field.name);
4459
4862
  if (fieldName) facts.fields.set(fieldName, field);
4460
4863
  }
4461
- for (const action of asArray28(obj.actions)) {
4462
- const actionName = strName8(action.name);
4864
+ for (const action of asArray29(obj.actions)) {
4865
+ const actionName = strName10(action.name);
4463
4866
  if (actionName) facts.actions.set(actionName, action);
4464
4867
  }
4465
- for (const view of asArray28(obj.views)) {
4466
- collectViewRecord({ ...view, object: strName8(view.object) ?? objectName }, factsFor);
4868
+ for (const view of asArray29(obj.views)) {
4869
+ collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
4467
4870
  }
4468
4871
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
4469
- for (const group of asArray28(obj.fieldGroups)) {
4470
- const key = strName8(group.key) ?? strName8(group.name);
4872
+ for (const group of asArray29(obj.fieldGroups)) {
4873
+ const key = strName10(group.key) ?? strName10(group.name);
4471
4874
  if (key) facts.sections.add(key);
4472
4875
  }
4473
4876
  }
4474
- for (const view of asArray28(stack.views)) {
4877
+ for (const view of asArray29(stack.views)) {
4475
4878
  collectViewRecord(view, factsFor);
4476
4879
  }
4477
- const pages = asArray28(stack.pages);
4880
+ const pages = asArray29(stack.pages);
4478
4881
  for (let pi = 0; pi < pages.length; pi++) {
4479
4882
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
4480
4883
  if (!walked.objectName) continue;
4481
- const props = isRec5(walked.component.properties) ? walked.component.properties : void 0;
4884
+ const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
4482
4885
  if (!props) continue;
4483
- for (const section of asArray28(props.sections)) {
4484
- const sectionName = strName8(section.name);
4886
+ for (const section of asArray29(props.sections)) {
4887
+ const sectionName = strName10(section.name);
4485
4888
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
4486
4889
  }
4487
4890
  }
4488
4891
  }
4489
4892
  const globalActions = /* @__PURE__ */ new Map();
4490
4893
  const actionOwners = /* @__PURE__ */ new Map();
4491
- for (const action of asArray28(stack.actions)) {
4492
- const actionName = strName8(action.name);
4894
+ for (const action of asArray29(stack.actions)) {
4895
+ const actionName = strName10(action.name);
4493
4896
  if (!actionName) continue;
4494
- const owner = strName8(action.objectName) ?? strName8(action.object);
4897
+ const owner = strName10(action.objectName) ?? strName10(action.object);
4495
4898
  if (owner) {
4496
4899
  factsFor(owner).actions.set(actionName, action);
4497
4900
  actionOwners.set(actionName, owner);
@@ -4505,41 +4908,41 @@ function buildUniverse(stack) {
4505
4908
  }
4506
4909
  }
4507
4910
  const apps = /* @__PURE__ */ new Map();
4508
- for (const app of asArray28(stack.apps)) {
4509
- const appName = strName8(app.name);
4911
+ for (const app of asArray29(stack.apps)) {
4912
+ const appName = strName10(app.name);
4510
4913
  if (!appName) continue;
4511
4914
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
4512
4915
  const walkNav = (items) => {
4513
- for (const item of asArray28(items)) {
4514
- const id = strName8(item.id);
4916
+ for (const item of asArray29(items)) {
4917
+ const id = strName10(item.id);
4515
4918
  if (id) navIds.add(id);
4516
4919
  if (item.children) walkNav(item.children);
4517
4920
  }
4518
4921
  };
4519
4922
  walkNav(app.navigation);
4520
- for (const area of asArray28(app.areas)) {
4521
- const areaId = strName8(area.id);
4923
+ for (const area of asArray29(app.areas)) {
4924
+ const areaId = strName10(area.id);
4522
4925
  if (areaId) navIds.add(areaId);
4523
4926
  walkNav(area.navigation);
4524
4927
  }
4525
4928
  apps.set(appName, navIds);
4526
4929
  }
4527
4930
  const dashboards = /* @__PURE__ */ new Map();
4528
- for (const dash of asArray28(stack.dashboards)) {
4529
- const dashName = strName8(dash.name);
4931
+ for (const dash of asArray29(stack.dashboards)) {
4932
+ const dashName = strName10(dash.name);
4530
4933
  if (!dashName) continue;
4531
4934
  const widgets = /* @__PURE__ */ new Set();
4532
- for (const widget of asArray28(dash.widgets)) {
4533
- const id = strName8(widget.id) ?? strName8(widget.name);
4935
+ for (const widget of asArray29(dash.widgets)) {
4936
+ const id = strName10(widget.id) ?? strName10(widget.name);
4534
4937
  if (id) widgets.add(id);
4535
4938
  }
4536
4939
  const actions = /* @__PURE__ */ new Set();
4537
4940
  const headerActions = [
4538
- ...asArray28(isRec5(dash.header) ? dash.header.actions : void 0),
4539
- ...asArray28(dash.actions)
4941
+ ...asArray29(isRec7(dash.header) ? dash.header.actions : void 0),
4942
+ ...asArray29(dash.actions)
4540
4943
  ];
4541
4944
  for (const action of headerActions) {
4542
- const key = strName8(action.actionUrl) ?? strName8(action.url) ?? strName8(action.name);
4945
+ const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
4543
4946
  if (key) actions.add(key);
4544
4947
  }
4545
4948
  dashboards.set(dashName, { widgets, actions });
@@ -4551,7 +4954,7 @@ function localePath(bundleIndex, locale) {
4551
4954
  }
4552
4955
  function validateTranslationReferences(stack) {
4553
4956
  const findings = [];
4554
- if (!isRec5(stack)) return findings;
4957
+ if (!isRec7(stack)) return findings;
4555
4958
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
4556
4959
  if (bundles.length === 0) return findings;
4557
4960
  const universe = buildUniverse(stack);
@@ -4560,21 +4963,21 @@ function validateTranslationReferences(stack) {
4560
4963
  };
4561
4964
  for (let bi = 0; bi < bundles.length; bi++) {
4562
4965
  const bundle = bundles[bi];
4563
- if (!isRec5(bundle)) continue;
4966
+ if (!isRec7(bundle)) continue;
4564
4967
  for (const [locale, rawData] of Object.entries(bundle)) {
4565
- if (!isRec5(rawData)) continue;
4968
+ if (!isRec7(rawData)) continue;
4566
4969
  const base = localePath(bi, locale);
4567
4970
  const inLocale = `locale "${locale}"`;
4568
4971
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
4569
- if (!isRec5(rawNode)) continue;
4972
+ if (!isRec7(rawNode)) continue;
4570
4973
  const objPath = `${base}.objects.${objectName}`;
4571
4974
  const facts = universe.objects.get(objectName);
4572
4975
  if (!facts) {
4573
- if ((0, import_system3.isPlatformProvidedObjectName)(objectName)) continue;
4976
+ if ((0, import_system4.isPlatformProvidedObjectName)(objectName)) continue;
4574
4977
  orphan(
4575
4978
  `${inLocale} \xB7 object "${objectName}"`,
4576
4979
  objPath,
4577
- (0, import_system3.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + 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()),
4980
+ (0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest6(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest6(objectName, universe.objects.keys()),
4578
4981
  `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())}.` : "")
4579
4982
  );
4580
4983
  continue;
@@ -4583,16 +4986,16 @@ function validateTranslationReferences(stack) {
4583
4986
  const fieldPath = `${objPath}.fields.${fieldName}`;
4584
4987
  const field = facts.fields.get(fieldName);
4585
4988
  if (!field) {
4586
- if (SYSTEM_FIELDS5.has(fieldName)) continue;
4989
+ if (IMPLICIT_FIELDS.has(fieldName)) continue;
4587
4990
  orphan(
4588
4991
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
4589
4992
  fieldPath,
4590
- `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()),
4993
+ `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()),
4591
4994
  `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())}.` : "")
4592
4995
  );
4593
4996
  continue;
4594
4997
  }
4595
- if (!isRec5(rawField)) continue;
4998
+ if (!isRec7(rawField)) continue;
4596
4999
  checkOptionKeys(findings, {
4597
5000
  optionMap: rawField.options,
4598
5001
  field,
@@ -4607,7 +5010,7 @@ function validateTranslationReferences(stack) {
4607
5010
  orphan(
4608
5011
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
4609
5012
  `${objPath}._views.${viewName}`,
4610
- `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),
5013
+ `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),
4611
5014
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
4612
5015
  );
4613
5016
  }
@@ -4616,7 +5019,7 @@ function validateTranslationReferences(stack) {
4616
5019
  orphan(
4617
5020
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
4618
5021
  `${objPath}._sections.${sectionName}`,
4619
- `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),
5022
+ `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),
4620
5023
  `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.`)
4621
5024
  );
4622
5025
  }
@@ -4627,7 +5030,7 @@ function validateTranslationReferences(stack) {
4627
5030
  orphan(
4628
5031
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
4629
5032
  actionPath,
4630
- `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()),
5033
+ `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()),
4631
5034
  `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())}.` : "")
4632
5035
  );
4633
5036
  continue;
@@ -4649,7 +5052,7 @@ function validateTranslationReferences(stack) {
4649
5052
  orphan(
4650
5053
  `${inLocale} \xB7 global action "${actionName}"`,
4651
5054
  actionPath,
4652
- 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()),
5055
+ 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()),
4653
5056
  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())}.` : "")
4654
5057
  );
4655
5058
  continue;
@@ -4669,18 +5072,18 @@ function validateTranslationReferences(stack) {
4669
5072
  orphan(
4670
5073
  `${inLocale} \xB7 app "${appName}"`,
4671
5074
  appPath,
4672
- `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()),
5075
+ `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()),
4673
5076
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
4674
5077
  );
4675
5078
  continue;
4676
5079
  }
4677
- if (!isRec5(rawApp)) continue;
5080
+ if (!isRec7(rawApp)) continue;
4678
5081
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
4679
5082
  if (navIds.has(navId)) continue;
4680
5083
  orphan(
4681
5084
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
4682
5085
  `${appPath}.navigation.${navId}`,
4683
- `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest5(navId, navIds),
5086
+ `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
4684
5087
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
4685
5088
  );
4686
5089
  }
@@ -4692,18 +5095,18 @@ function validateTranslationReferences(stack) {
4692
5095
  orphan(
4693
5096
  `${inLocale} \xB7 dashboard "${dashName}"`,
4694
5097
  dashPath,
4695
- `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()),
5098
+ `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()),
4696
5099
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
4697
5100
  );
4698
5101
  continue;
4699
5102
  }
4700
- if (!isRec5(rawDash)) continue;
5103
+ if (!isRec7(rawDash)) continue;
4701
5104
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
4702
5105
  if (dash.widgets.has(widgetId)) continue;
4703
5106
  orphan(
4704
5107
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
4705
5108
  `${dashPath}.widgets.${widgetId}`,
4706
- `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest5(widgetId, dash.widgets),
5109
+ `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
4707
5110
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
4708
5111
  );
4709
5112
  }
@@ -4712,7 +5115,7 @@ function validateTranslationReferences(stack) {
4712
5115
  orphan(
4713
5116
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
4714
5117
  `${dashPath}.actions.${actionKey}`,
4715
- `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest5(actionKey, dash.actions),
5118
+ `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
4716
5119
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
4717
5120
  );
4718
5121
  }
@@ -4722,7 +5125,7 @@ function validateTranslationReferences(stack) {
4722
5125
  return findings;
4723
5126
  }
4724
5127
  function asRecord(v) {
4725
- return isRec5(v) ? v : {};
5128
+ return isRec7(v) ? v : {};
4726
5129
  }
4727
5130
  function checkOptionKeys(findings, ctx) {
4728
5131
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -4734,7 +5137,7 @@ function checkOptionKeys(findings, ctx) {
4734
5137
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
4735
5138
  where: ctx.where,
4736
5139
  path: ctx.path,
4737
- 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.`,
5140
+ 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.`,
4738
5141
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
4739
5142
  });
4740
5143
  return;
@@ -4747,17 +5150,17 @@ function checkOptionKeys(findings, ctx) {
4747
5150
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
4748
5151
  where: ctx.where,
4749
5152
  path: `${ctx.path}.${key}`,
4750
- 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),
5153
+ 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),
4751
5154
  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)}.`
4752
5155
  });
4753
5156
  }
4754
5157
  }
4755
5158
  function checkActionParams(findings, ctx) {
4756
- const rawParams = Object.keys(asRecord(isRec5(ctx.rawAction) ? ctx.rawAction.params : void 0));
5159
+ const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
4757
5160
  if (rawParams.length === 0) return;
4758
5161
  const declared = /* @__PURE__ */ new Set();
4759
- for (const param of asArray28(ctx.action.params)) {
4760
- const name = strName8(param.name) ?? strName8(param.field);
5162
+ for (const param of asArray29(ctx.action.params)) {
5163
+ const name = strName10(param.name) ?? strName10(param.field);
4761
5164
  if (name) declared.add(name);
4762
5165
  }
4763
5166
  for (const paramName of rawParams) {
@@ -4767,7 +5170,7 @@ function checkActionParams(findings, ctx) {
4767
5170
  rule: TRANSLATION_TARGET_UNKNOWN,
4768
5171
  where: `${ctx.where} \xB7 param "${paramName}"`,
4769
5172
  path: `${ctx.path}.params.${paramName}`,
4770
- 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),
5173
+ 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),
4771
5174
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
4772
5175
  });
4773
5176
  }
@@ -4775,14 +5178,14 @@ function checkActionParams(findings, ctx) {
4775
5178
 
4776
5179
  // src/validate-ai-surface-affinity.ts
4777
5180
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
4778
- function asArray29(v) {
5181
+ function asArray30(v) {
4779
5182
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
4780
5183
  if (v && typeof v === "object") {
4781
5184
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4782
5185
  }
4783
5186
  return [];
4784
5187
  }
4785
- function strName9(v) {
5188
+ function strName11(v) {
4786
5189
  return typeof v === "string" && v.length > 0 ? v : void 0;
4787
5190
  }
4788
5191
  function surfaceOf(v) {
@@ -4792,18 +5195,18 @@ function validateAiSurfaceAffinity(stack) {
4792
5195
  const findings = [];
4793
5196
  if (!stack || typeof stack !== "object") return findings;
4794
5197
  const skillsByName = /* @__PURE__ */ new Map();
4795
- for (const skill of asArray29(stack.skills)) {
4796
- const n = strName9(skill.name);
5198
+ for (const skill of asArray30(stack.skills)) {
5199
+ const n = strName11(skill.name);
4797
5200
  if (n) skillsByName.set(n, skill);
4798
5201
  }
4799
- const agents = asArray29(stack.agents);
5202
+ const agents = asArray30(stack.agents);
4800
5203
  for (let ai = 0; ai < agents.length; ai++) {
4801
5204
  const agent = agents[ai];
4802
- const agentName = strName9(agent.name) ?? `#${ai}`;
5205
+ const agentName = strName11(agent.name) ?? `#${ai}`;
4803
5206
  const agentSurface = surfaceOf(agent.surface);
4804
5207
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
4805
5208
  for (let si = 0; si < skillRefs.length; si++) {
4806
- const ref = strName9(skillRefs[si]);
5209
+ const ref = strName11(skillRefs[si]);
4807
5210
  if (!ref) continue;
4808
5211
  const skill = skillsByName.get(ref);
4809
5212
  if (!skill) continue;
@@ -4823,19 +5226,19 @@ function validateAiSurfaceAffinity(stack) {
4823
5226
  }
4824
5227
 
4825
5228
  // src/validate-ai-tool-references.ts
4826
- var import_system4 = require("@objectstack/spec/system");
5229
+ var import_system5 = require("@objectstack/spec/system");
4827
5230
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
4828
- function asArray30(v) {
5231
+ function asArray31(v) {
4829
5232
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
4830
5233
  if (v && typeof v === "object") {
4831
5234
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4832
5235
  }
4833
5236
  return [];
4834
5237
  }
4835
- function strName10(v) {
5238
+ function strName12(v) {
4836
5239
  return typeof v === "string" && v.length > 0 ? v : void 0;
4837
5240
  }
4838
- function distance5(a, b) {
5241
+ function distance6(a, b) {
4839
5242
  const m = a.length;
4840
5243
  const n = b.length;
4841
5244
  if (m === 0) return n;
@@ -4851,14 +5254,14 @@ function distance5(a, b) {
4851
5254
  }
4852
5255
  return prev[n];
4853
5256
  }
4854
- function suggest6(target, known) {
4855
- for (const prefix of import_system4.PLATFORM_TOOL_FAMILY_PREFIXES) {
5257
+ function suggest7(target, known) {
5258
+ for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
4856
5259
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
4857
5260
  }
4858
5261
  let best;
4859
5262
  let bestScore = Infinity;
4860
5263
  for (const candidate of known) {
4861
- const d = distance5(target, candidate);
5264
+ const d = distance6(target, candidate);
4862
5265
  if (d < bestScore) {
4863
5266
  bestScore = d;
4864
5267
  best = candidate;
@@ -4873,26 +5276,26 @@ function materialisesAsTool(action) {
4873
5276
  if (!ai || typeof ai !== "object") return false;
4874
5277
  const aiRec = ai;
4875
5278
  if (aiRec.exposed !== true) return false;
4876
- if (!strName10(aiRec.description)) return false;
4877
- const type = strName10(action.type);
5279
+ if (!strName12(aiRec.description)) return false;
5280
+ const type = strName12(action.type);
4878
5281
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
4879
5282
  if (type === "script") return Boolean(action.target || action.body);
4880
5283
  return Boolean(action.target);
4881
5284
  }
4882
5285
  function collectToolUniverse(stack) {
4883
- const universe = new Set(import_system4.PLATFORM_PROVIDED_TOOL_NAMES);
4884
- for (const tool of asArray30(stack.tools)) {
4885
- const n = strName10(tool.name);
5286
+ const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
5287
+ for (const tool of asArray31(stack.tools)) {
5288
+ const n = strName12(tool.name);
4886
5289
  if (n) universe.add(n);
4887
5290
  }
4888
5291
  const addActionFamily = (actions) => {
4889
- for (const action of asArray30(actions)) {
4890
- const n = strName10(action.name);
5292
+ for (const action of asArray31(actions)) {
5293
+ const n = strName12(action.name);
4891
5294
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
4892
5295
  }
4893
5296
  };
4894
5297
  addActionFamily(stack.actions);
4895
- for (const obj of asArray30(stack.objects)) {
5298
+ for (const obj of asArray31(stack.objects)) {
4896
5299
  addActionFamily(obj.actions);
4897
5300
  }
4898
5301
  return universe;
@@ -4900,13 +5303,13 @@ function collectToolUniverse(stack) {
4900
5303
  function collectUnexposedActionNames(stack) {
4901
5304
  const names = /* @__PURE__ */ new Set();
4902
5305
  const scan = (actions) => {
4903
- for (const action of asArray30(actions)) {
4904
- const n = strName10(action.name);
5306
+ for (const action of asArray31(actions)) {
5307
+ const n = strName12(action.name);
4905
5308
  if (n && !materialisesAsTool(action)) names.add(n);
4906
5309
  }
4907
5310
  };
4908
5311
  scan(stack.actions);
4909
- for (const obj of asArray30(stack.objects)) scan(obj.actions);
5312
+ for (const obj of asArray31(stack.objects)) scan(obj.actions);
4910
5313
  return names;
4911
5314
  }
4912
5315
  function validateAiToolReferences(stack) {
@@ -4924,13 +5327,13 @@ function validateAiToolReferences(stack) {
4924
5327
  }
4925
5328
  return universe.has(ref);
4926
5329
  };
4927
- const skills = asArray30(stack.skills);
5330
+ const skills = asArray31(stack.skills);
4928
5331
  for (let si = 0; si < skills.length; si++) {
4929
5332
  const skill = skills[si];
4930
- const skillName = strName10(skill.name) ?? `#${si}`;
5333
+ const skillName = strName12(skill.name) ?? `#${si}`;
4931
5334
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
4932
5335
  for (let ti = 0; ti < refs.length; ti++) {
4933
- const ref = strName10(refs[ti]);
5336
+ const ref = strName12(refs[ti]);
4934
5337
  if (!ref || resolves(ref)) continue;
4935
5338
  const isPattern = ref.endsWith("*");
4936
5339
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -4939,8 +5342,8 @@ function validateAiToolReferences(stack) {
4939
5342
  rule: AI_SKILL_TOOL_UNRESOLVED,
4940
5343
  where: `skill "${skillName}" \xB7 tools`,
4941
5344
  path: `skills[${si}].tools[${ti}]`,
4942
- 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),
4943
- hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${import_system4.PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
5345
+ 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),
5346
+ hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${import_system5.PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
4944
5347
  });
4945
5348
  }
4946
5349
  }
@@ -4949,24 +5352,24 @@ function validateAiToolReferences(stack) {
4949
5352
 
4950
5353
  // src/validate-ai-agent-authoring.ts
4951
5354
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
4952
- function asArray31(v) {
5355
+ function asArray32(v) {
4953
5356
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
4954
5357
  if (v && typeof v === "object") {
4955
5358
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4956
5359
  }
4957
5360
  return [];
4958
5361
  }
4959
- function strName11(v) {
5362
+ function strName13(v) {
4960
5363
  return typeof v === "string" && v.length > 0 ? v : void 0;
4961
5364
  }
4962
5365
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
4963
5366
  function validateAiAgentAuthoring(stack) {
4964
5367
  const findings = [];
4965
5368
  if (!stack || typeof stack !== "object") return findings;
4966
- const agents = asArray31(stack.agents);
5369
+ const agents = asArray32(stack.agents);
4967
5370
  for (let ai = 0; ai < agents.length; ai++) {
4968
5371
  const agent = agents[ai];
4969
- const name = strName11(agent.name) ?? `#${ai}`;
5372
+ const name = strName13(agent.name) ?? `#${ai}`;
4970
5373
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
4971
5374
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
4972
5375
  findings.push({
@@ -4981,9 +5384,495 @@ function validateAiAgentAuthoring(stack) {
4981
5384
  return findings;
4982
5385
  }
4983
5386
 
5387
+ // src/validate-hook-body-writes.ts
5388
+ var import_node_module3 = require("module");
5389
+ var import_shared = require("@objectstack/spec/shared");
5390
+ var import_meta3 = {};
5391
+ var cachedTs2 = null;
5392
+ function loadTypeScript2() {
5393
+ if (cachedTs2) return cachedTs2;
5394
+ const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5395
+ try {
5396
+ cachedTs2 = (0, import_node_module3.createRequire)(anchor)("typescript");
5397
+ } catch (err) {
5398
+ throw new Error(
5399
+ `@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.`
5400
+ );
5401
+ }
5402
+ return cachedTs2;
5403
+ }
5404
+ var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
5405
+ var HOOK_BODY_WRITE_PATTERNS = [
5406
+ {
5407
+ id: "input-property-assign",
5408
+ syntax: "ctx.input.<field> = \u2026 | ctx.input['<field>'] \u27E8op\u27E9= \u2026",
5409
+ example: {
5410
+ // Compound (`+=`) and logical (`??=`) assignment operators write their
5411
+ // LHS exactly like `=` does — the example pins the whole operator range.
5412
+ source: "ctx.input.total = 0; ctx.input['status'] ??= 'open'; ctx.input.retries += 1;",
5413
+ writes: [{ field: "total" }, { field: "status" }, { field: "retries" }]
5414
+ }
5415
+ },
5416
+ {
5417
+ id: "input-object-assign",
5418
+ syntax: "Object.assign(ctx.input, { <field>: \u2026 })",
5419
+ example: {
5420
+ source: "Object.assign(ctx.input, { total: 5, 'status': 'open', discount });",
5421
+ writes: [{ field: "total" }, { field: "status" }, { field: "discount" }]
5422
+ }
5423
+ },
5424
+ {
5425
+ // ACTION-only shape (the hook sandbox context has no `ctx.record` at all).
5426
+ // Declared here because this ledger is the extractor's shape inventory, not
5427
+ // any one rule's; every consumer declares which shapes it consumes.
5428
+ id: "record-property-assign",
5429
+ syntax: "ctx.record.<field> = \u2026 | ctx.record['<field>'] \u27E8op\u27E9= \u2026",
5430
+ example: {
5431
+ source: "ctx.record.stage = 'won'; ctx.record['amount'] += 1;",
5432
+ writes: [{ field: "stage" }, { field: "amount" }]
5433
+ }
5434
+ },
5435
+ {
5436
+ id: "api-crud-literal",
5437
+ syntax: "ctx.api.object('<object>').insert({\u2026}) | .create({\u2026}) | .update({\u2026}) | .updateById(id, {\u2026})",
5438
+ example: {
5439
+ // Real ObjectRepository signatures: the record payload is argument 0 for
5440
+ // insert/create/update and argument 1 for updateById. (`update(data)` —
5441
+ // NOT `update(id, data)`; the id travels inside the payload/options.)
5442
+ source: "await ctx.api.object('audit_log').insert({ event: 'won' }); await ctx.api.object('crm_deal').updateById(id, { stage: 'won' });",
5443
+ writes: [
5444
+ { field: "event", object: "audit_log" },
5445
+ { field: "stage", object: "crm_deal" }
5446
+ ]
5447
+ }
5448
+ }
5449
+ ];
5450
+ var HOOK_BODY_WRITE_PATTERN_IDS = [
5451
+ "input-property-assign",
5452
+ "input-object-assign",
5453
+ "api-crud-literal"
5454
+ ];
5455
+ var HOOK_BODY_WRITE_EXCLUSIONS = [
5456
+ {
5457
+ id: "record-property-assign",
5458
+ 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"
5459
+ }
5460
+ ];
5461
+ var HOOK_APPLICABLE_IDS = new Set(HOOK_BODY_WRITE_PATTERN_IDS);
5462
+ var API_WRITE_METHODS = /* @__PURE__ */ new Map([
5463
+ ["insert", 0],
5464
+ ["create", 0],
5465
+ ["update", 0],
5466
+ ["updateById", 1]
5467
+ ]);
5468
+ var INPUT_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["id", "options", "ast", "data"]);
5469
+ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
5470
+ ...SYSTEM_FIELDS,
5471
+ "_id",
5472
+ "name",
5473
+ "space",
5474
+ "owner",
5475
+ "record_type"
5476
+ ]);
5477
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5478
+ function asArray33(v) {
5479
+ if (Array.isArray(v)) return v.filter((x) => isRec8(x));
5480
+ if (isRec8(v)) {
5481
+ return Object.entries(v).map(([name, def]) => ({
5482
+ name,
5483
+ ...isRec8(def) ? def : {}
5484
+ }));
5485
+ }
5486
+ return [];
5487
+ }
5488
+ function indexObjectFields2(stack) {
5489
+ const out = /* @__PURE__ */ new Map();
5490
+ for (const obj of asArray33(stack.objects)) {
5491
+ const name = typeof obj.name === "string" ? obj.name : void 0;
5492
+ if (!name) continue;
5493
+ const names = /* @__PURE__ */ new Set();
5494
+ for (const f of asArray33(obj.fields)) {
5495
+ if (typeof f.name === "string" && f.name) names.add(f.name);
5496
+ }
5497
+ out.set(name, names);
5498
+ }
5499
+ return out;
5500
+ }
5501
+ function judgeableFieldsOf(index, objectName) {
5502
+ const declared = index.get(objectName);
5503
+ if (!declared || declared.size === 0) return void 0;
5504
+ return declared;
5505
+ }
5506
+ function extractHookBodyWrites(source) {
5507
+ return extractHookBodyWriteSet(source).writes;
5508
+ }
5509
+ function extractHookBodyWriteSet(source) {
5510
+ if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
5511
+ return { writes: [], ctxRecordEscapes: false };
5512
+ }
5513
+ const tsc = loadTypeScript2();
5514
+ const sf = tsc.createSourceFile(
5515
+ "hook-body.ts",
5516
+ `async function __body(ctx) {
5517
+ ${source}
5518
+ }`,
5519
+ tsc.ScriptTarget.Latest,
5520
+ /* setParentNodes */
5521
+ false,
5522
+ tsc.ScriptKind.TS
5523
+ );
5524
+ const writes = [];
5525
+ const recordRefs = [];
5526
+ const consumedRecordRefs = /* @__PURE__ */ new Set();
5527
+ const isCtxDot = (node, prop) => tsc.isPropertyAccessExpression(node) && tsc.isIdentifier(node.expression) && node.expression.text === "ctx" && node.name.text === prop;
5528
+ const fieldOfCtxLhs = (lhs, prop) => {
5529
+ if (tsc.isPropertyAccessExpression(lhs) && tsc.isIdentifier(lhs.name) && isCtxDot(lhs.expression, prop)) {
5530
+ return lhs.name.text;
5531
+ }
5532
+ if (tsc.isElementAccessExpression(lhs) && isCtxDot(lhs.expression, prop)) {
5533
+ const arg = lhs.argumentExpression;
5534
+ if (tsc.isStringLiteral(arg) || tsc.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
5535
+ }
5536
+ return void 0;
5537
+ };
5538
+ const literalObjectKeys = (node) => {
5539
+ if (!tsc.isObjectLiteralExpression(node)) return [];
5540
+ const keys = [];
5541
+ for (const p of node.properties) {
5542
+ if (tsc.isPropertyAssignment(p)) {
5543
+ if (tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name)) keys.push(p.name.text);
5544
+ } else if (tsc.isShorthandPropertyAssignment(p)) {
5545
+ keys.push(p.name.text);
5546
+ }
5547
+ }
5548
+ return keys;
5549
+ };
5550
+ const visit = (node) => {
5551
+ if (tsc.isBinaryExpression(node) && node.operatorToken.kind >= tsc.SyntaxKind.FirstAssignment && node.operatorToken.kind <= tsc.SyntaxKind.LastAssignment) {
5552
+ const inputField = fieldOfCtxLhs(node.left, "input");
5553
+ if (inputField !== void 0 && !INPUT_ENVELOPE_KEYS.has(inputField)) {
5554
+ writes.push({ patternId: "input-property-assign", field: inputField });
5555
+ }
5556
+ const recordField = fieldOfCtxLhs(node.left, "record");
5557
+ if (recordField !== void 0) {
5558
+ writes.push({ patternId: "record-property-assign", field: recordField });
5559
+ }
5560
+ }
5561
+ if (tsc.isPropertyAccessExpression(node) || tsc.isElementAccessExpression(node)) {
5562
+ if (isCtxDot(node.expression, "record")) consumedRecordRefs.add(node.expression);
5563
+ }
5564
+ if (tsc.isBinaryExpression(node)) {
5565
+ const op = node.operatorToken.kind;
5566
+ if ((op === tsc.SyntaxKind.AmpersandAmpersandToken || op === tsc.SyntaxKind.BarBarToken || op === tsc.SyntaxKind.QuestionQuestionToken) && isCtxDot(node.left, "record")) {
5567
+ consumedRecordRefs.add(node.left);
5568
+ }
5569
+ }
5570
+ if (tsc.isPrefixUnaryExpression(node) && node.operator === tsc.SyntaxKind.ExclamationToken) {
5571
+ if (isCtxDot(node.operand, "record")) consumedRecordRefs.add(node.operand);
5572
+ }
5573
+ if (tsc.isTypeOfExpression(node) && isCtxDot(node.expression, "record")) {
5574
+ consumedRecordRefs.add(node.expression);
5575
+ }
5576
+ if ((tsc.isIfStatement(node) || tsc.isWhileStatement(node) || tsc.isDoStatement(node)) && isCtxDot(node.expression, "record")) {
5577
+ consumedRecordRefs.add(node.expression);
5578
+ }
5579
+ if (tsc.isConditionalExpression(node) && isCtxDot(node.condition, "record")) {
5580
+ consumedRecordRefs.add(node.condition);
5581
+ }
5582
+ if (isCtxDot(node, "record")) recordRefs.push(node);
5583
+ if (tsc.isCallExpression(node)) {
5584
+ const callee = node.expression;
5585
+ 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")) {
5586
+ for (const arg of node.arguments.slice(1)) {
5587
+ for (const field of literalObjectKeys(arg)) {
5588
+ if (!INPUT_ENVELOPE_KEYS.has(field)) {
5589
+ writes.push({ patternId: "input-object-assign", field });
5590
+ }
5591
+ }
5592
+ }
5593
+ }
5594
+ if (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.name)) {
5595
+ const payloadIndex = API_WRITE_METHODS.get(callee.name.text);
5596
+ const recv = callee.expression;
5597
+ 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) {
5598
+ const objArg = recv.arguments[0];
5599
+ const objectName = tsc.isStringLiteral(objArg) || tsc.isNoSubstitutionTemplateLiteral(objArg) ? objArg.text : void 0;
5600
+ const payload = node.arguments[payloadIndex];
5601
+ if (objectName && payload !== void 0) {
5602
+ for (const field of literalObjectKeys(payload)) {
5603
+ writes.push({
5604
+ patternId: "api-crud-literal",
5605
+ object: objectName,
5606
+ method: callee.name.text,
5607
+ field
5608
+ });
5609
+ }
5610
+ }
5611
+ }
5612
+ }
5613
+ }
5614
+ tsc.forEachChild(node, visit);
5615
+ };
5616
+ visit(sf);
5617
+ return {
5618
+ writes,
5619
+ ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
5620
+ };
5621
+ }
5622
+ function validateHookBodyWrites(stack) {
5623
+ const findings = [];
5624
+ const hooks = asArray33(stack.hooks);
5625
+ if (hooks.length === 0) return findings;
5626
+ let objectFields = null;
5627
+ hooks.forEach((hook, hookIndex) => {
5628
+ const body = hook.body;
5629
+ if (!isRec8(body) || body.language !== "js") return;
5630
+ const source = body.source;
5631
+ if (typeof source !== "string" || source.trim() === "") return;
5632
+ const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
5633
+ if (writes.length === 0) return;
5634
+ objectFields ?? (objectFields = indexObjectFields2(stack));
5635
+ const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
5636
+ const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
5637
+ (o) => typeof o === "string" && o.trim() !== ""
5638
+ );
5639
+ const targetSets = targets.map((t) => judgeableFieldsOf(objectFields, t));
5640
+ const inputJudgeable = targets.length > 0 && !targets.includes("*") && targetSets.every((s) => s !== void 0);
5641
+ const where = `hook "${hookName}" \u203A body`;
5642
+ const path = `hooks[${hookIndex}].body.source`;
5643
+ const reported = /* @__PURE__ */ new Set();
5644
+ for (const w of writes) {
5645
+ const dedupeKey = `${w.object ?? ""}\0${w.field}`;
5646
+ if (reported.has(dedupeKey)) continue;
5647
+ if (w.object === void 0) {
5648
+ if (!inputJudgeable) continue;
5649
+ if (IMPLICIT_FIELDS2.has(w.field)) continue;
5650
+ if (targetSets.some((s) => s.has(w.field))) continue;
5651
+ reported.add(dedupeKey);
5652
+ const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
5653
+ const declares = targets.length === 1 ? "declares no such field" : "declare that field";
5654
+ findings.push({
5655
+ severity: "warning",
5656
+ rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
5657
+ where,
5658
+ path,
5659
+ 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).`,
5660
+ hint: fixHint(w.field, unionCandidates(targetSets))
5661
+ });
5662
+ } else {
5663
+ const known = judgeableFieldsOf(objectFields, w.object);
5664
+ if (!known) continue;
5665
+ if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
5666
+ reported.add(dedupeKey);
5667
+ findings.push({
5668
+ severity: "warning",
5669
+ rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
5670
+ where,
5671
+ path,
5672
+ 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).`,
5673
+ hint: fixHint(w.field, [...known])
5674
+ });
5675
+ }
5676
+ }
5677
+ });
5678
+ return findings;
5679
+ }
5680
+ function unionCandidates(targetSets) {
5681
+ const out = /* @__PURE__ */ new Set();
5682
+ for (const s of targetSets) for (const f of s ?? []) out.add(f);
5683
+ return [...out];
5684
+ }
5685
+ function fixHint(field, declared) {
5686
+ const suggestion = (0, import_shared.formatSuggestion)((0, import_shared.findClosestMatches)(field, [...declared, ...IMPLICIT_FIELDS2]));
5687
+ 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.`;
5688
+ }
5689
+
5690
+ // src/validate-action-body-writes.ts
5691
+ var import_shared2 = require("@objectstack/spec/shared");
5692
+ var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
5693
+ var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
5694
+ var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
5695
+ var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
5696
+ var ACTION_BODY_WRITE_EXCLUSIONS = [
5697
+ {
5698
+ id: "input-property-assign",
5699
+ 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"
5700
+ },
5701
+ {
5702
+ id: "input-object-assign",
5703
+ reason: "same surface as input-property-assign \u2014 Object.assign(ctx.input, \u2026) targets the params bag"
5704
+ }
5705
+ ];
5706
+ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
5707
+ var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
5708
+ var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
5709
+ var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
5710
+ var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5711
+ function asArray34(v) {
5712
+ if (Array.isArray(v)) return v.filter((x) => isRec9(x));
5713
+ if (isRec9(v)) {
5714
+ return Object.entries(v).map(([name, def]) => ({
5715
+ name,
5716
+ ...isRec9(def) ? def : {}
5717
+ }));
5718
+ }
5719
+ return [];
5720
+ }
5721
+ function actionObjectBinding(action, parentObject) {
5722
+ if (typeof action.object === "string" && action.object) return action.object;
5723
+ if (typeof action.objectName === "string" && action.objectName) return action.objectName;
5724
+ return parentObject;
5725
+ }
5726
+ function collectActionBodies(stack) {
5727
+ const sites = [];
5728
+ const seen = /* @__PURE__ */ new Set();
5729
+ const collect = (actions, pathPrefix, parentObject) => {
5730
+ asArray34(actions).forEach((action, index) => {
5731
+ const body = action.body;
5732
+ if (!isRec9(body) || body.language !== "js") return;
5733
+ const source = body.source;
5734
+ if (typeof source !== "string" || source.trim() === "") return;
5735
+ const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
5736
+ const key = `${actionObjectBinding(action, parentObject) ?? ""}\0${name}\0${source}`;
5737
+ if (seen.has(key)) return;
5738
+ seen.add(key);
5739
+ sites.push({ name, source, path: `${pathPrefix}[${index}].body.source` });
5740
+ });
5741
+ };
5742
+ collect(stack.actions, "actions");
5743
+ asArray34(stack.objects).forEach((obj, objIndex) => {
5744
+ const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
5745
+ collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
5746
+ });
5747
+ return sites;
5748
+ }
5749
+ function validateActionBodyWrites(stack) {
5750
+ const findings = [];
5751
+ if (!isRec9(stack)) return findings;
5752
+ const sites = collectActionBodies(stack);
5753
+ if (sites.length === 0) return findings;
5754
+ let objectFields = null;
5755
+ for (const site of sites) {
5756
+ if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
5757
+ const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
5758
+ const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
5759
+ const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
5760
+ if (writes.length === 0 && recordWrites.length === 0) continue;
5761
+ const where = `action "${site.name}" \u203A body`;
5762
+ if (recordWrites.length > 0 && !ctxRecordEscapes) {
5763
+ const reportedFields = /* @__PURE__ */ new Set();
5764
+ for (const w of recordWrites) {
5765
+ if (reportedFields.has(w.field)) continue;
5766
+ reportedFields.add(w.field);
5767
+ findings.push({
5768
+ severity: "warning",
5769
+ rule: ACTION_RECORD_WRITE_DISCARDED,
5770
+ where,
5771
+ path: site.path,
5772
+ 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).`,
5773
+ 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.`
5774
+ });
5775
+ }
5776
+ }
5777
+ if (writes.length === 0) continue;
5778
+ objectFields ?? (objectFields = indexObjectFields2(stack));
5779
+ const reported = /* @__PURE__ */ new Set();
5780
+ for (const w of writes) {
5781
+ if (w.object === void 0) continue;
5782
+ const dedupeKey = `${w.object}\0${w.field}`;
5783
+ if (reported.has(dedupeKey)) continue;
5784
+ const known = judgeableFieldsOf(objectFields, w.object);
5785
+ if (!known) continue;
5786
+ if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
5787
+ reported.add(dedupeKey);
5788
+ findings.push({
5789
+ severity: "warning",
5790
+ rule: ACTION_BODY_WRITE_UNKNOWN_FIELD,
5791
+ where,
5792
+ path: site.path,
5793
+ 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).`,
5794
+ hint: fixHint2(w.field, [...known])
5795
+ });
5796
+ }
5797
+ }
5798
+ return findings;
5799
+ }
5800
+ function fixHint2(field, declared) {
5801
+ const suggestion = (0, import_shared2.formatSuggestion)((0, import_shared2.findClosestMatches)(field, [...declared, ...IMPLICIT_FIELDS2]));
5802
+ 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.`;
5803
+ }
5804
+
5805
+ // src/validate-flow-node-writes.ts
5806
+ var import_shared3 = require("@objectstack/spec/shared");
5807
+ var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
5808
+ var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
5809
+ var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
5810
+ var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5811
+ function asArray35(v) {
5812
+ if (Array.isArray(v)) return v.filter((x) => isRec10(x));
5813
+ if (isRec10(v)) {
5814
+ return Object.entries(v).map(([name, def]) => ({
5815
+ name,
5816
+ ...isRec10(def) ? def : {}
5817
+ }));
5818
+ }
5819
+ return [];
5820
+ }
5821
+ function readLiteralObjectName2(config) {
5822
+ const raw = config.objectName ?? config.object;
5823
+ if (typeof raw !== "string" || raw.includes("{")) return void 0;
5824
+ return raw || void 0;
5825
+ }
5826
+ var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
5827
+ function validateFlowNodeWrites(stack) {
5828
+ const findings = [];
5829
+ if (!isRec10(stack)) return findings;
5830
+ const flows = asArray35(stack.flows);
5831
+ if (flows.length === 0) return findings;
5832
+ let objectFields = null;
5833
+ flows.forEach((flow, flowIndex) => {
5834
+ const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
5835
+ const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
5836
+ walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
5837
+ if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
5838
+ const config = isRec10(node.config) ? node.config : void 0;
5839
+ if (!config) return;
5840
+ const fields = config.fields;
5841
+ if (!isRec10(fields)) return;
5842
+ const written = Object.keys(fields);
5843
+ if (written.length === 0) return;
5844
+ const objectName = readLiteralObjectName2(config);
5845
+ if (!objectName) return;
5846
+ objectFields ?? (objectFields = indexObjectFields2(stack));
5847
+ const known = judgeableFieldsOf(objectFields, objectName);
5848
+ if (!known) return;
5849
+ const nodeName = flowNodeLabel(node, walkIndex);
5850
+ const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
5851
+ for (const fieldName of written) {
5852
+ if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
5853
+ if (fieldName.includes(".")) continue;
5854
+ findings.push({
5855
+ severity: "error",
5856
+ rule: FLOW_NODE_WRITE_UNKNOWN_FIELD,
5857
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
5858
+ path: `${nodePath}.config.fields.${fieldName}`,
5859
+ 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.`,
5860
+ hint: fixHint3(fieldName, [...known])
5861
+ });
5862
+ }
5863
+ });
5864
+ });
5865
+ return findings;
5866
+ }
5867
+ function fixHint3(field, declared) {
5868
+ const suggestion = (0, import_shared3.formatSuggestion)((0, import_shared3.findClosestMatches)(field, [...declared, ...IMPLICIT_FIELDS2]));
5869
+ 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.`;
5870
+ }
5871
+
4984
5872
  // src/reference-integrity-suite.ts
4985
5873
  var REFERENCE_INTEGRITY_RULES = [
4986
5874
  { name: "validateObjectReferences", run: validateObjectReferences },
5875
+ { name: "validateSearchableFields", run: validateSearchableFields },
4987
5876
  { name: "validateActionNameRefs", run: validateActionNameRefs },
4988
5877
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
4989
5878
  { name: "validateChartBindings", run: validateChartBindings },
@@ -4992,7 +5881,70 @@ var REFERENCE_INTEGRITY_RULES = [
4992
5881
  { name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
4993
5882
  { name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
4994
5883
  { name: "validateAiToolReferences", run: validateAiToolReferences },
4995
- { name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring }
5884
+ { name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring },
5885
+ // Field names WRITTEN by an L2 hook body (`ctx.input.x = …`,
5886
+ // `ctx.api.object('y').update({ x })`), resolved against the target object's
5887
+ // declared fields — the write-side counterpart of validateFlowTemplatePaths'
5888
+ // read-side membership (#4271). Lazy: only a hook that actually carries a
5889
+ // `language:'js'` body loads the TypeScript parser.
5890
+ { name: "validateHookBodyWrites", run: validateHookBodyWrites },
5891
+ // The same check on the other surface that carries a `HookBodySchema` body:
5892
+ // action bodies, run by the same sandbox. Only the `ctx.api` write family
5893
+ // carries over — an action's `ctx.input` is its params bag, not a record
5894
+ // (see that module's ledger). Lazy on the same terms.
5895
+ //
5896
+ // The first member here to emit more than one rule id (`validateReactPageProps`
5897
+ // below is the other, and carries the most). Besides resolving `ctx.api`
5898
+ // writes against declared fields (`action-body-write-unknown-field`), it
5899
+ // reports a `ctx.record` write that can reach nothing
5900
+ // (`action-record-write-discarded`, #4345) — not a resolution question, so
5901
+ // by the charter above it does not belong in the suite. It rides along
5902
+ // anyway because it falls out of the SAME parse of the SAME source: a
5903
+ // separate member would parse every action body twice to say two things
5904
+ // about one walk, and hand-wiring it into the CLI instead is exactly the
5905
+ // drift this suite exists to end — which `validateReadonlyFlowWrites` was
5906
+ // the standing proof of, until it joined the suite below.
5907
+ { name: "validateActionBodyWrites", run: validateActionBodyWrites },
5908
+ // The third surface that writes a record field set: a flow `update_record`
5909
+ // node's `config.fields`. Same question as the two rules above, but the map
5910
+ // is structural metadata rather than parsed JS, so a finding is a certainty
5911
+ // and gates (`error`) — see that module for why, and why the docs' long-
5912
+ // standing "prefer a flow node, it's checked" advice was the least true of
5913
+ // the three until it landed.
5914
+ { name: "validateFlowNodeWrites", run: validateFlowNodeWrites },
5915
+ // The OTHER question about that same `config.fields` map: not "does this
5916
+ // field exist?" but "is it writable?" — a `runAs:'user'` update_record
5917
+ // writing a static-`readonly` field is stripped by the engine and the step
5918
+ // still reports success (#2948/#3425). It walks the identical map the rule
5919
+ // above walks, so the two splitting call sites was never defensible: hand-
5920
+ // wired into `validate` and `compile` only, it left `os lint` PASSING a flow
5921
+ // `os validate` refuses — and this one gates, so the divergence shipped a
5922
+ // build the other command would have stopped. Joining the suite is the whole
5923
+ // fix; the two hand-wired call sites are deleted with it (#4345 follow-up).
5924
+ { name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
5925
+ // The `kind:'react'` page surface. Every prop a react block binds BY FIELD
5926
+ // NAME is resolved against the object it names (#4340) — `<ListView columns>`,
5927
+ // `<ObjectForm fields>`, the `record:*` family through the SAME
5928
+ // `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
5929
+ // over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
5930
+ // `searchableFields` (#4329). Squarely the charter's question, on the surface
5931
+ // where it had no answer at all.
5932
+ //
5933
+ // It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
5934
+ // accepted a react page whose every field binding was stale — including the
5935
+ // gating ones (a missing required binding, a filter position naming no field:
5936
+ // the predicate can never match and the list comes back empty). That is
5937
+ // `validateReadonlyFlowWrites`' divergence again, one surface over, and it is
5938
+ // the reason this entry exists rather than a fourth hand-wiring.
5939
+ //
5940
+ // Like `validateActionBodyWrites` above, it emits ids that are not resolution
5941
+ // questions — `react-prop-missing-required` and `react-prop-typo` are shape,
5942
+ // and by the charter belong outside. They ride along for the same reason: they
5943
+ // fall out of the SAME TypeScript parse of the SAME page source, and splitting
5944
+ // them into a second member would parse every react page twice to say two
5945
+ // things about one walk. Lazy on the same terms as the hook/action body rules
5946
+ // — only a page that is actually `kind:'react'` loads the compiler.
5947
+ { name: "validateReactPageProps", run: validateReactPageProps }
4996
5948
  ];
4997
5949
  function validateReferenceIntegrity(stack) {
4998
5950
  const findings = [];
@@ -5003,7 +5955,14 @@ function validateReferenceIntegrity(stack) {
5003
5955
  }
5004
5956
  // Annotate the CommonJS export names for ESM import in node:
5005
5957
  0 && (module.exports = {
5958
+ ACTION_BODY_WRITE_EXCLUSIONS,
5959
+ ACTION_BODY_WRITE_PATTERNS,
5960
+ ACTION_BODY_WRITE_PATTERN_IDS,
5961
+ ACTION_BODY_WRITE_UNKNOWN_FIELD,
5006
5962
  ACTION_NAME_UNDEFINED,
5963
+ ACTION_RECORD_WRITE_DISCARDED,
5964
+ ACTION_RECORD_WRITE_PATTERNS,
5965
+ ACTION_RECORD_WRITE_PATTERN_IDS,
5007
5966
  AGENT_AUTHORING_WITHDRAWN,
5008
5967
  AI_SKILL_SURFACE_MISMATCH,
5009
5968
  AI_SKILL_TOOL_UNRESOLVED,
@@ -5030,13 +5989,20 @@ function validateReferenceIntegrity(stack) {
5030
5989
  FIELD_GROUP_UNDECLARED,
5031
5990
  FILTER_TOKEN_UNKNOWN,
5032
5991
  FLOW_DRAFT_STATUS_AMBIGUOUS,
5992
+ FLOW_NODE_WRITE_UNKNOWN_FIELD,
5033
5993
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
5034
5994
  FLOW_TEMPLATE_UNKNOWN_FIELD,
5035
5995
  FLOW_TRIGGER_UNKNOWN_OBJECT,
5036
5996
  FLOW_UPDATE_READONLY_FIELD,
5037
5997
  FLOW_UPDATE_READONLY_WHEN_FIELD,
5998
+ FLOW_WRITE_NODE_TYPES,
5999
+ FLOW_WRITE_NODE_TYPES_DEFERRED,
5038
6000
  FORM_COLSPAN_ABSOLUTE,
5039
6001
  FORM_FIELD_UNKNOWN,
6002
+ HOOK_BODY_WRITE_EXCLUSIONS,
6003
+ HOOK_BODY_WRITE_PATTERNS,
6004
+ HOOK_BODY_WRITE_PATTERN_IDS,
6005
+ HOOK_BODY_WRITE_UNKNOWN_FIELD,
5040
6006
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
5041
6007
  MEASURE_AGGREGATE_INCOHERENT,
5042
6008
  NAV_OBJECT_UNGRANTED,
@@ -5050,6 +6016,7 @@ function validateReferenceIntegrity(stack) {
5050
6016
  REACT_CHART_AXIS_UNKNOWN,
5051
6017
  REACT_CHART_FIELD_UNKNOWN,
5052
6018
  REFERENCE_INTEGRITY_RULES,
6019
+ SEARCHABLE_FIELD_UNKNOWN,
5053
6020
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
5054
6021
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
5055
6022
  SECURITY_DELEGATION_MISSING_REASON,
@@ -5082,6 +6049,9 @@ function validateReferenceIntegrity(stack) {
5082
6049
  WIDGET_MEASURE_UNKNOWN,
5083
6050
  buildAccessMatrix,
5084
6051
  diffAccessMatrix,
6052
+ extractHookBodyWriteSet,
6053
+ extractHookBodyWrites,
6054
+ validateActionBodyWrites,
5085
6055
  validateActionNameRefs,
5086
6056
  validateAiAgentAuthoring,
5087
6057
  validateAiSurfaceAffinity,
@@ -5091,9 +6061,11 @@ function validateReferenceIntegrity(stack) {
5091
6061
  validateChartBindings,
5092
6062
  validateDashboardActionRefs,
5093
6063
  validateFilterTokens,
6064
+ validateFlowNodeWrites,
5094
6065
  validateFlowTemplatePaths,
5095
6066
  validateFlowTriggerReadiness,
5096
6067
  validateFormLayout,
6068
+ validateHookBodyWrites,
5097
6069
  validateJsxPages,
5098
6070
  validateListViewMode,
5099
6071
  validateNavAccess,
@@ -5107,6 +6079,7 @@ function validateReferenceIntegrity(stack) {
5107
6079
  validateRecordTitle,
5108
6080
  validateReferenceIntegrity,
5109
6081
  validateResponsiveStyles,
6082
+ validateSearchableFields,
5110
6083
  validateSecurityPosture,
5111
6084
  validateSeedReplaySafety,
5112
6085
  validateSeedStateMachine,