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

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
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  ACTION_BODY_WRITE_PATTERN_IDS: () => ACTION_BODY_WRITE_PATTERN_IDS,
26
26
  ACTION_BODY_WRITE_UNKNOWN_FIELD: () => ACTION_BODY_WRITE_UNKNOWN_FIELD,
27
27
  ACTION_NAME_UNDEFINED: () => ACTION_NAME_UNDEFINED,
28
+ ACTION_NO_PLACEMENT: () => ACTION_NO_PLACEMENT,
28
29
  ACTION_RECORD_WRITE_DISCARDED: () => ACTION_RECORD_WRITE_DISCARDED,
29
30
  ACTION_RECORD_WRITE_PATTERNS: () => ACTION_RECORD_WRITE_PATTERNS,
30
31
  ACTION_RECORD_WRITE_PATTERN_IDS: () => ACTION_RECORD_WRITE_PATTERN_IDS,
@@ -40,6 +41,13 @@ __export(index_exports, {
40
41
  APPROVAL_ESCALATION_REASSIGN_NO_TARGET: () => APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
41
42
  APPROVAL_EXPRESSION_INVALID: () => APPROVAL_EXPRESSION_INVALID,
42
43
  APPROVAL_EXPRESSION_NO_EMPTY_POLICY: () => APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
44
+ AUTHORING_COMMANDS: () => AUTHORING_COMMANDS,
45
+ AUTHORING_RULES: () => AUTHORING_RULES,
46
+ AUTHORING_SURFACES: () => AUTHORING_SURFACES,
47
+ AUTONUMBER_LITERAL_TOKEN: () => AUTONUMBER_LITERAL_TOKEN,
48
+ AUTONUMBER_OPTIONAL_FIELD: () => AUTONUMBER_OPTIONAL_FIELD,
49
+ AUTONUMBER_SELF_REFERENCE: () => AUTONUMBER_SELF_REFERENCE,
50
+ AUTONUMBER_UNKNOWN_FIELD: () => AUTONUMBER_UNKNOWN_FIELD,
43
51
  CAPABILITY_REFERENCE_UNKNOWN: () => CAPABILITY_REFERENCE_UNKNOWN,
44
52
  CHART_AXIS_NOT_SELECTED: () => CHART_AXIS_NOT_SELECTED,
45
53
  CHART_CONFIG_MISSING: () => CHART_CONFIG_MISSING,
@@ -50,13 +58,29 @@ __export(index_exports, {
50
58
  DASHBOARD_ACTION_ROUTE_UNRESOLVED: () => DASHBOARD_ACTION_ROUTE_UNRESOLVED,
51
59
  DASHBOARD_ACTION_TARGET_UNDEFINED: () => DASHBOARD_ACTION_TARGET_UNDEFINED,
52
60
  DASHBOARD_FILTER_FIELD_UNKNOWN: () => DASHBOARD_FILTER_FIELD_UNKNOWN,
61
+ EXPRESSION_INVALID: () => EXPRESSION_INVALID,
53
62
  FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
54
63
  FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
55
64
  FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
65
+ FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
66
+ FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
67
+ FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE: () => FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
68
+ FLOW_BARE_DOLLAR_REF: () => FLOW_BARE_DOLLAR_REF,
69
+ FLOW_BRANCH_LABEL_UNMATCHED: () => FLOW_BRANCH_LABEL_UNMATCHED,
70
+ FLOW_DATE_EQUALITY_FILTER: () => FLOW_DATE_EQUALITY_FILTER,
71
+ FLOW_DECISION_UNCONDITIONAL_BRANCH: () => FLOW_DECISION_UNCONDITIONAL_BRANCH,
72
+ FLOW_DEFAULT_EDGE_WITH_CONDITION: () => FLOW_DEFAULT_EDGE_WITH_CONDITION,
73
+ FLOW_DOUBLE_BRACE_INTERP: () => FLOW_DOUBLE_BRACE_INTERP,
56
74
  FLOW_DRAFT_STATUS_AMBIGUOUS: () => FLOW_DRAFT_STATUS_AMBIGUOUS,
75
+ FLOW_ERROR_LABEL_NOT_FAULT: () => FLOW_ERROR_LABEL_NOT_FAULT,
76
+ FLOW_INERT_NODE_CONDITION: () => FLOW_INERT_NODE_CONDITION,
77
+ FLOW_MULTIPLE_DEFAULT_EDGES: () => FLOW_MULTIPLE_DEFAULT_EDGES,
57
78
  FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
79
+ FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
80
+ FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
58
81
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL: () => FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
59
82
  FLOW_TEMPLATE_UNKNOWN_FIELD: () => FLOW_TEMPLATE_UNKNOWN_FIELD,
83
+ FLOW_TIME_RELATIVE_ANTIPATTERN: () => FLOW_TIME_RELATIVE_ANTIPATTERN,
60
84
  FLOW_TRIGGER_UNKNOWN_OBJECT: () => FLOW_TRIGGER_UNKNOWN_OBJECT,
61
85
  FLOW_UPDATE_READONLY_FIELD: () => FLOW_UPDATE_READONLY_FIELD,
62
86
  FLOW_UPDATE_READONLY_WHEN_FIELD: () => FLOW_UPDATE_READONLY_WHEN_FIELD,
@@ -69,19 +93,25 @@ __export(index_exports, {
69
93
  HOOK_BODY_WRITE_PATTERN_IDS: () => HOOK_BODY_WRITE_PATTERN_IDS,
70
94
  HOOK_BODY_WRITE_UNKNOWN_FIELD: () => HOOK_BODY_WRITE_UNKNOWN_FIELD,
71
95
  LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
96
+ LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
97
+ LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
72
98
  MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
73
99
  NAV_OBJECT_UNGRANTED: () => NAV_OBJECT_UNGRANTED,
100
+ NAV_TARGET_UNRESOLVED: () => NAV_TARGET_UNRESOLVED,
101
+ NULL_GUARD_HINT: () => NULL_GUARD_HINT,
74
102
  OBJECT_REFERENCE_UNKNOWN: () => OBJECT_REFERENCE_UNKNOWN,
75
103
  OBJECT_REFERENCE_UNREGISTERED_PLATFORM: () => OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
76
104
  ORG_AXIS_CROSS_ORG_BU_GRANT: () => ORG_AXIS_CROSS_ORG_BU_GRANT,
77
105
  ORG_AXIS_PERMISSION_INHERITANCE: () => ORG_AXIS_PERMISSION_INHERITANCE,
78
106
  PAGE_FIELD_UNKNOWN: () => PAGE_FIELD_UNKNOWN,
79
107
  PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
108
+ REACT_BLOCK_NEEDS_RECORD_CONTEXT: () => REACT_BLOCK_NEEDS_RECORD_CONTEXT,
80
109
  REACT_CHART_AGGREGATE_INVALID: () => REACT_CHART_AGGREGATE_INVALID,
81
110
  REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
82
111
  REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
83
112
  REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
84
113
  SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
114
+ SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
85
115
  SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
86
116
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
87
117
  SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
@@ -106,17 +136,37 @@ __export(index_exports, {
106
136
  TITLE_UNRESOLVABLE: () => TITLE_UNRESOLVABLE,
107
137
  TRANSLATION_OPTION_KEY_UNKNOWN: () => TRANSLATION_OPTION_KEY_UNKNOWN,
108
138
  TRANSLATION_TARGET_UNKNOWN: () => TRANSLATION_TARGET_UNKNOWN,
139
+ UNIQUE_DOUBLE_DECLARATION: () => UNIQUE_DOUBLE_DECLARATION,
109
140
  VIEW_CONTAINER_SHAPE: () => VIEW_CONTAINER_SHAPE,
141
+ VIEW_KEY_COLLISION: () => VIEW_KEY_COLLISION,
142
+ VIEW_REF_FORM_TARGET_KIND: () => VIEW_REF_FORM_TARGET_KIND,
143
+ VIEW_REF_FORM_TARGET_MISSING: () => VIEW_REF_FORM_TARGET_MISSING,
110
144
  VISIBILITY_ALIAS_DEPRECATED: () => VISIBILITY_ALIAS_DEPRECATED,
111
145
  VISIBILITY_ROOT_MISLAYERED: () => VISIBILITY_ROOT_MISLAYERED,
112
146
  WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
113
147
  WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
114
148
  WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
149
+ authoringRulesFor: () => authoringRulesFor,
115
150
  buildAccessMatrix: () => buildAccessMatrix,
116
151
  diffAccessMatrix: () => diffAccessMatrix,
117
152
  extractHookBodyWriteSet: () => extractHookBodyWriteSet,
118
153
  extractHookBodyWrites: () => extractHookBodyWrites,
154
+ findUnguardedNullableOperands: () => findUnguardedNullableOperands,
155
+ lintAutonumberFormats: () => lintAutonumberFormats,
156
+ lintDataModel: () => lintDataModel,
157
+ lintFlowPatterns: () => lintFlowPatterns,
158
+ lintLivenessProperties: () => lintLivenessProperties,
159
+ lintUniqueDeclarations: () => lintUniqueDeclarations,
160
+ lintViewRefs: () => lintViewRefs,
161
+ nullGuardMessage: () => nullGuardMessage,
162
+ runAuthoringRules: () => runAuthoringRules,
163
+ runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
164
+ runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
165
+ runtimeGatedTypes: () => runtimeGatedTypes,
166
+ splitBySeverity: () => splitBySeverity,
167
+ stackKeyForType: () => stackKeyForType,
119
168
  validateActionBodyWrites: () => validateActionBodyWrites,
169
+ validateActionLocations: () => validateActionLocations,
120
170
  validateActionNameRefs: () => validateActionNameRefs,
121
171
  validateAiAgentAuthoring: () => validateAiAgentAuthoring,
122
172
  validateAiSurfaceAffinity: () => validateAiSurfaceAffinity,
@@ -130,10 +180,12 @@ __export(index_exports, {
130
180
  validateFlowTemplatePaths: () => validateFlowTemplatePaths,
131
181
  validateFlowTriggerReadiness: () => validateFlowTriggerReadiness,
132
182
  validateFormLayout: () => validateFormLayout,
183
+ validateFunctionalCompleteness: () => validateFunctionalCompleteness,
133
184
  validateHookBodyWrites: () => validateHookBodyWrites,
134
185
  validateJsxPages: () => validateJsxPages,
135
186
  validateListViewMode: () => validateListViewMode,
136
187
  validateNavAccess: () => validateNavAccess,
188
+ validateNavTargetRefs: () => validateNavTargetRefs,
137
189
  validateObjectReferences: () => validateObjectReferences,
138
190
  validateOrgAxisRedLines: () => validateOrgAxisRedLines,
139
191
  validatePageFieldBindings: () => validatePageFieldBindings,
@@ -335,7 +387,7 @@ function validateWidgetBindings(stack) {
335
387
  const where = `dashboard "${dashName}" \u203A widget "${widgetId}"`;
336
388
  const path = `dashboards[${i}].widgets[${j}]`;
337
389
  const suppressed = (rule) => Array.isArray(w.suppressWarnings) && w.suppressWarnings.includes(rule);
338
- const push = (f) => {
390
+ const push2 = (f) => {
339
391
  if (f.severity === "warning" && suppressed(f.rule)) return;
340
392
  findings.push({ ...f, where, path });
341
393
  };
@@ -347,14 +399,14 @@ function validateWidgetBindings(stack) {
347
399
  const plural = legacyUsed.length > 1;
348
400
  const datasetHint = `Bind a semantic dataset and select fields BY NAME: \`dataset: '<name>', dimensions: [...], values: [...]\`. Dataset-bound widgets render through DatasetWidget (pivot rows/cols come from \`dimensions\`, cell values from \`values\`).`;
349
401
  if (!hasDataSource) {
350
- push({
402
+ push2({
351
403
  severity: "error",
352
404
  rule: WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
353
405
  message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} (removed by the ADR-0021 single-form cutover) and binds no data source (no \`dataset\`, \`object\`, or inline \`data\`) \u2014 it renders nothing.`,
354
406
  hint: `${datasetHint} The renderer ignores the legacy keys, so without a data source this widget has no data at all.`
355
407
  });
356
408
  } else {
357
- push({
409
+ push2({
358
410
  severity: "warning",
359
411
  rule: WIDGET_LEGACY_ANALYTICS_SHAPE,
360
412
  message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} that the ADR-0021 single-form cutover removed \u2014 the dashboard renderer ignores ${plural ? "them" : "it"}.`,
@@ -365,7 +417,7 @@ function validateWidgetBindings(stack) {
365
417
  const dsName = typeof w.dataset === "string" ? w.dataset : void 0;
366
418
  const dataset = dsName ? datasets.get(dsName) : void 0;
367
419
  if (dsName && !dataset) {
368
- push({
420
+ push2({
369
421
  severity: "error",
370
422
  rule: WIDGET_DATASET_UNKNOWN,
371
423
  message: `dataset "${dsName}" does not resolve to a declared dataset.`,
@@ -373,7 +425,7 @@ function validateWidgetBindings(stack) {
373
425
  });
374
426
  }
375
427
  if (!dsName) {
376
- push({
428
+ push2({
377
429
  severity: "error",
378
430
  rule: WIDGET_DATASET_UNKNOWN,
379
431
  message: `binds no \`dataset\` \u2014 the ADR-0021 widget shape requires one, so this widget resolves no data and renders empty.`,
@@ -392,7 +444,7 @@ function validateWidgetBindings(stack) {
392
444
  const field = eff.field;
393
445
  if (field.includes(".")) continue;
394
446
  if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
395
- push({
447
+ push2({
396
448
  severity: "error",
397
449
  rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
398
450
  message: eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.` : `inherits dashboard filter \`${def.name}(${field})\`, but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.`,
@@ -412,7 +464,7 @@ function validateWidgetBindings(stack) {
412
464
  const dims = asStrings(w.dimensions);
413
465
  for (let k = 0; k < dims.length; k++) {
414
466
  if (dimensionNames.has(dims[k])) continue;
415
- push({
467
+ push2({
416
468
  severity: "error",
417
469
  rule: WIDGET_DIMENSION_UNKNOWN,
418
470
  message: `dimensions[${k}] "${dims[k]}" is not a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
@@ -422,7 +474,7 @@ function validateWidgetBindings(stack) {
422
474
  const values = asStrings(w.values);
423
475
  for (let k = 0; k < values.length; k++) {
424
476
  if (measures.has(values[k])) continue;
425
- push({
477
+ push2({
426
478
  severity: "error",
427
479
  rule: WIDGET_MEASURE_UNKNOWN,
428
480
  message: `values[${k}] "${values[k]}" is not a measure of dataset "${dsName}" (declared measures: ${list(measures.keys())}).`,
@@ -435,7 +487,7 @@ function validateWidgetBindings(stack) {
435
487
  const selectedValues = new Set(values.filter((v) => measures.has(v)));
436
488
  const xAxis = chartConfig.xAxis && typeof chartConfig.xAxis === "object" ? chartConfig.xAxis : void 0;
437
489
  if (xAxis && typeof xAxis.field === "string" && !dimensionNames.has(xAxis.field) && !dims.includes(xAxis.field)) {
438
- push({
490
+ push2({
439
491
  severity: "error",
440
492
  rule: CHART_FIELD_UNKNOWN,
441
493
  message: `chartConfig.xAxis.field "${xAxis.field}" does not resolve to a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
@@ -445,7 +497,7 @@ function validateWidgetBindings(stack) {
445
497
  const measureField = (label2, field) => {
446
498
  if (values.includes(field)) return;
447
499
  const declaredButUnselected = measures.has(field);
448
- push({
500
+ push2({
449
501
  severity: "error",
450
502
  rule: CHART_FIELD_UNKNOWN,
451
503
  message: declaredButUnselected ? `chartConfig.${label2} "${field}" is a measure of dataset "${dsName}" but is not selected in the widget's values (${list(values)}), so the query result will not contain it.` : `chartConfig.${label2} "${field}" does not resolve to a measure of dataset "${dsName}" (declared measures: ${list(measures.keys())}).`,
@@ -463,7 +515,7 @@ function validateWidgetBindings(stack) {
463
515
  if (typeof name === "string") measureField(`series[${k}].name`, name);
464
516
  }
465
517
  } else if (isChartType) {
466
- push({
518
+ push2({
467
519
  severity: "warning",
468
520
  rule: CHART_CONFIG_MISSING,
469
521
  message: `chart-type widget ('${w.type}') has no chartConfig \u2014 the renderer cannot determine which measure to plot, so the series renders empty.`,
@@ -477,7 +529,7 @@ function validateWidgetBindings(stack) {
477
529
  if (resolved.some((m) => !m)) continue;
478
530
  const countOnly = resolved.every((m) => m.aggregate === "count" && !m.derived);
479
531
  if (!countOnly) continue;
480
- push({
532
+ push2({
481
533
  severity: "warning",
482
534
  rule: TABLE_COUNT_ONLY,
483
535
  message: `a '${w.type}' widget bound to dataset "${dsName}" selects only count measure(s) (${values.join(", ")}) and no dimensions, so it renders a single summary row \u2014 not a per-record list.`,
@@ -491,6 +543,226 @@ function validateWidgetBindings(stack) {
491
543
  // src/validate-expressions.ts
492
544
  var import_formula = require("@objectstack/formula");
493
545
  var import_automation = require("@objectstack/spec/automation");
546
+
547
+ // src/validate-null-guards.ts
548
+ var import_cel_js = require("@marcbachmann/cel-js");
549
+ var NULL_GUARD_HINT = `Guard it with '!= null' \u2014 'has(x)' does NOT do that: a declared field holding null is still PRESENT, so has(x) is true.`;
550
+ var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
551
+ var DEFAULT_RECORD_ROOTS = ["record", "previous"];
552
+ var parseEnv;
553
+ function getParseEnv() {
554
+ if (!parseEnv) {
555
+ parseEnv = new import_cel_js.Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
556
+ }
557
+ return parseEnv;
558
+ }
559
+ function isNode(v) {
560
+ return !!v && typeof v === "object" && typeof v.op === "string";
561
+ }
562
+ function isNullLiteral(node) {
563
+ return isNode(node) && node.op === "value" && node.args === null;
564
+ }
565
+ function fieldOf(node, roots) {
566
+ if (!isNode(node)) return null;
567
+ if (node.op !== "." && node.op !== ".?") return null;
568
+ const args = node.args;
569
+ if (!Array.isArray(args) || args.length < 2) return null;
570
+ const [recv, seg] = args;
571
+ if (typeof seg !== "string") return null;
572
+ if (!isNode(recv) || recv.op !== "id") return null;
573
+ const root = recv.args;
574
+ if (typeof root !== "string" || !roots.includes(root)) return null;
575
+ return { operand: `${root}.${seg}`, field: seg };
576
+ }
577
+ function childNodes(node) {
578
+ const args = node.args;
579
+ if (isNode(args)) return [args];
580
+ if (!Array.isArray(args)) return [];
581
+ const out = [];
582
+ for (const a of args) {
583
+ if (isNode(a)) out.push(a);
584
+ else if (Array.isArray(a)) {
585
+ for (const b of a) if (isNode(b)) out.push(b);
586
+ }
587
+ }
588
+ return out;
589
+ }
590
+ function callName(node) {
591
+ if (node.op !== "call") return null;
592
+ const args = node.args;
593
+ if (!Array.isArray(args) || typeof args[0] !== "string") return null;
594
+ return args[0];
595
+ }
596
+ function callArgs(node) {
597
+ const args = node.args;
598
+ if (!Array.isArray(args) || !Array.isArray(args[1])) return [];
599
+ return args[1];
600
+ }
601
+ function union(a, b) {
602
+ return /* @__PURE__ */ new Set([...a, ...b]);
603
+ }
604
+ function intersect(a, b) {
605
+ const out = /* @__PURE__ */ new Set();
606
+ for (const v of a) if (b.has(v)) out.add(v);
607
+ return out;
608
+ }
609
+ function truthGuards(node, roots) {
610
+ if (!isNode(node)) return /* @__PURE__ */ new Set();
611
+ switch (node.op) {
612
+ case "!=": {
613
+ const [l, r] = node.args ?? [];
614
+ if (isNullLiteral(r)) {
615
+ const f = fieldOf(l, roots);
616
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
617
+ }
618
+ if (isNullLiteral(l)) {
619
+ const f = fieldOf(r, roots);
620
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
621
+ }
622
+ return /* @__PURE__ */ new Set();
623
+ }
624
+ case "&&": {
625
+ const [l, r] = node.args;
626
+ return union(truthGuards(l, roots), truthGuards(r, roots));
627
+ }
628
+ case "||": {
629
+ const [l, r] = node.args;
630
+ return intersect(truthGuards(l, roots), truthGuards(r, roots));
631
+ }
632
+ case "!_":
633
+ return falseGuards(node.args, roots);
634
+ case "?:": {
635
+ const [, t, f] = node.args;
636
+ return intersect(truthGuards(t, roots), truthGuards(f, roots));
637
+ }
638
+ default:
639
+ return /* @__PURE__ */ new Set();
640
+ }
641
+ }
642
+ function falseGuards(node, roots) {
643
+ if (!isNode(node)) return /* @__PURE__ */ new Set();
644
+ switch (node.op) {
645
+ case "==": {
646
+ const [l, r] = node.args ?? [];
647
+ if (isNullLiteral(r)) {
648
+ const f = fieldOf(l, roots);
649
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
650
+ }
651
+ if (isNullLiteral(l)) {
652
+ const f = fieldOf(r, roots);
653
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
654
+ }
655
+ return /* @__PURE__ */ new Set();
656
+ }
657
+ case "||": {
658
+ const [l, r] = node.args;
659
+ return union(falseGuards(l, roots), falseGuards(r, roots));
660
+ }
661
+ case "&&": {
662
+ const [l, r] = node.args;
663
+ return intersect(falseGuards(l, roots), falseGuards(r, roots));
664
+ }
665
+ case "!_":
666
+ return truthGuards(node.args, roots);
667
+ case "call": {
668
+ if (callName(node) !== "isBlank") return /* @__PURE__ */ new Set();
669
+ const [only] = callArgs(node);
670
+ const f = fieldOf(only, roots);
671
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
672
+ }
673
+ default:
674
+ return /* @__PURE__ */ new Set();
675
+ }
676
+ }
677
+ function collectHasOperands(node, roots, out) {
678
+ if (!isNode(node)) return;
679
+ if (callName(node) === "has") {
680
+ for (const a of callArgs(node)) {
681
+ const f = fieldOf(a, roots);
682
+ if (f) out.add(f.operand);
683
+ }
684
+ }
685
+ for (const child of childNodes(node)) collectHasOperands(child, roots, out);
686
+ }
687
+ function findUnguardedNullableOperands(source, opts) {
688
+ if (typeof source !== "string" || !source.trim()) return [];
689
+ if (opts.nullableFields.size === 0) return [];
690
+ const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
691
+ let ast;
692
+ try {
693
+ ast = getParseEnv().parse(source).ast;
694
+ } catch {
695
+ return [];
696
+ }
697
+ const hasOperands = /* @__PURE__ */ new Set();
698
+ collectHasOperands(ast, roots, hasOperands);
699
+ const findings = [];
700
+ const seen = /* @__PURE__ */ new Set();
701
+ const report = (operandNode, operator, guards) => {
702
+ const f = fieldOf(operandNode, roots);
703
+ if (!f) return;
704
+ if (!opts.nullableFields.has(f.field)) return;
705
+ if (guards.has(f.operand)) return;
706
+ const key = `${f.operand}\0${operator}`;
707
+ if (seen.has(key)) return;
708
+ seen.add(key);
709
+ findings.push({
710
+ operand: f.operand,
711
+ field: f.field,
712
+ operator,
713
+ hasOnlyGuard: hasOperands.has(f.operand)
714
+ });
715
+ };
716
+ const visit = (node, guards) => {
717
+ if (!isNode(node)) return;
718
+ const op = node.op;
719
+ if (op === "&&") {
720
+ const [l, r] = node.args;
721
+ visit(l, guards);
722
+ visit(r, union(guards, truthGuards(l, roots)));
723
+ return;
724
+ }
725
+ if (op === "||") {
726
+ const [l, r] = node.args;
727
+ visit(l, guards);
728
+ visit(r, union(guards, falseGuards(l, roots)));
729
+ return;
730
+ }
731
+ if (op === "?:") {
732
+ const [c, t, f] = node.args;
733
+ visit(c, guards);
734
+ visit(t, union(guards, truthGuards(c, roots)));
735
+ visit(f, union(guards, falseGuards(c, roots)));
736
+ return;
737
+ }
738
+ if (op === "call" && callName(node) === "has") {
739
+ return;
740
+ }
741
+ if (FAULTING_BINARY_OPS.has(op)) {
742
+ const [l, r] = node.args;
743
+ report(l, op, guards);
744
+ report(r, op, guards);
745
+ visit(l, guards);
746
+ visit(r, guards);
747
+ return;
748
+ }
749
+ if (op === "-_") {
750
+ report(node.args, "-", guards);
751
+ visit(node.args, guards);
752
+ return;
753
+ }
754
+ for (const child of childNodes(node)) visit(child, guards);
755
+ };
756
+ visit(ast, /* @__PURE__ */ new Set());
757
+ return findings;
758
+ }
759
+ function nullGuardMessage(subject, objectName, finding) {
760
+ const owner = objectName ? `'${objectName}'` : "this object";
761
+ const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
762
+ return `${subject} applies \`${finding.operator}\` to \`${finding.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding.operator}\` overload for null, and the whole predicate aborts \u2014 so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763). The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
763
+ }
764
+
765
+ // src/validate-expressions.ts
494
766
  function asArray2(v) {
495
767
  if (Array.isArray(v)) return v;
496
768
  if (v && typeof v === "object") {
@@ -534,11 +806,84 @@ function buildFieldTypeIndex(objects) {
534
806
  }
535
807
  return idx;
536
808
  }
809
+ function fieldEntries(obj) {
810
+ const fields = obj.fields;
811
+ if (Array.isArray(fields)) {
812
+ return fields.filter((f) => f && typeof f === "object" && typeof f.name === "string").map((f) => [f.name, f]);
813
+ }
814
+ if (fields && typeof fields === "object") {
815
+ return Object.entries(fields).filter(([, def]) => !!def && typeof def === "object").map(([n, def]) => [n, def]);
816
+ }
817
+ return [];
818
+ }
819
+ function isNullableField(def) {
820
+ if (def.required === true) return false;
821
+ if (def.defaultValue !== void 0 && def.defaultValue !== null) return false;
822
+ if (def.type === "autonumber") return false;
823
+ const options = def.options;
824
+ if (Array.isArray(options) && options.some((o) => !!o && typeof o === "object" && o.default === true)) {
825
+ return false;
826
+ }
827
+ return true;
828
+ }
829
+ function buildNullableFieldIndex(objects) {
830
+ const idx = /* @__PURE__ */ new Map();
831
+ for (const obj of objects) {
832
+ const name = typeof obj.name === "string" ? obj.name : void 0;
833
+ if (!name) continue;
834
+ const nullable = /* @__PURE__ */ new Set();
835
+ for (const [fname, def] of fieldEntries(obj)) {
836
+ if (isNullableField(def)) nullable.add(fname);
837
+ }
838
+ idx.set(name, nullable);
839
+ }
840
+ return idx;
841
+ }
842
+ function celSourceOf(raw) {
843
+ if (typeof raw === "string") return raw;
844
+ if (raw && typeof raw === "object") {
845
+ const rec = raw;
846
+ if (typeof rec.dialect === "string" && rec.dialect !== "cel") return void 0;
847
+ if (typeof rec.source === "string") return rec.source;
848
+ }
849
+ return void 0;
850
+ }
851
+ function rulePredicates(rule, path) {
852
+ const out = [];
853
+ const name = typeof rule.name === "string" ? rule.name : "?";
854
+ const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
855
+ const main = rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula;
856
+ if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
857
+ if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
858
+ for (const branch of ["then", "otherwise"]) {
859
+ const nested = rule[branch];
860
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
861
+ out.push(...rulePredicates(nested, `${here} ${branch}`));
862
+ }
863
+ }
864
+ return out;
865
+ }
537
866
  function validateStackExpressions(stack) {
538
867
  const issues = [];
539
868
  const objects = asArray2(stack.objects);
540
869
  const fieldIndex = buildFieldIndex(objects);
541
870
  const fieldTypeIndex = buildFieldTypeIndex(objects);
871
+ const nullableIndex = buildNullableFieldIndex(objects);
872
+ const checkNullGuards = (where, subject, raw, objectName) => {
873
+ if (!objectName) return;
874
+ const nullableFields = nullableIndex.get(objectName);
875
+ if (!nullableFields || nullableFields.size === 0) return;
876
+ const source = celSourceOf(raw);
877
+ if (!source) return;
878
+ for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
879
+ issues.push({
880
+ where,
881
+ message: nullGuardMessage(subject, objectName, finding),
882
+ source,
883
+ severity: "error"
884
+ });
885
+ }
886
+ };
542
887
  const check = (where, raw, objectName, scope = "flattened") => {
543
888
  if (raw == null) return;
544
889
  const fields = objectName ? fieldIndex.get(objectName) : void 0;
@@ -579,17 +924,17 @@ function validateStackExpressions(stack) {
579
924
  if (node.type === "script") {
580
925
  const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
581
926
  const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
582
- const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
583
- if (!fn && !action && !inline) {
927
+ const retired = ["actionType", "template", "recipients", "variables", "script"].filter((k) => cfg[k] != null);
928
+ if (retired.length > 0) {
584
929
  issues.push({
585
930
  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 })\`).`,
931
+ message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + `Run \`os migrate meta --from 16\` to rewrite it automatically.`,
587
932
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
588
933
  });
589
- } else if (action === "invoke_function" && !fn) {
934
+ } else if (!fn) {
590
935
  issues.push({
591
936
  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 })\`.`,
937
+ message: `script node declares no \`function\` \u2014 it would do nothing at runtime. Name a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
593
938
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
594
939
  });
595
940
  }
@@ -607,6 +952,9 @@ function validateStackExpressions(stack) {
607
952
  const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
608
953
  check(where, rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula, objectName, "record");
609
954
  check(`${where} when`, rule.when, objectName, "record");
955
+ for (const p of rulePredicates(rule, "")) {
956
+ checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
957
+ }
610
958
  }
611
959
  const fields = obj.fields;
612
960
  const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
@@ -659,6 +1007,12 @@ function validateStackExpressions(stack) {
659
1007
  const hookName = hook.name ?? "?";
660
1008
  if (typeof hook.object === "string") {
661
1009
  check(`hook '${hookName}' (${hook.object}) condition`, hook.condition, hook.object, "record");
1010
+ checkNullGuards(
1011
+ `hook '${hookName}' (${hook.object}) condition`,
1012
+ `hook '${hookName}' condition`,
1013
+ hook.condition,
1014
+ hook.object
1015
+ );
662
1016
  continue;
663
1017
  }
664
1018
  const targets = Array.isArray(hook.object) ? hook.object.filter((o) => typeof o === "string" && o !== "*") : [];
@@ -672,6 +1026,12 @@ function validateStackExpressions(stack) {
672
1026
  for (const target of targets) {
673
1027
  const mark = issues.length;
674
1028
  check(`hook '${hookName}' (${target}) condition`, hook.condition, target, "record");
1029
+ checkNullGuards(
1030
+ `hook '${hookName}' (${target}) condition`,
1031
+ `hook '${hookName}' condition`,
1032
+ hook.condition,
1033
+ target
1034
+ );
675
1035
  for (let i = mark; i < issues.length; i++) {
676
1036
  const issue = issues[i];
677
1037
  const key = `${issue.message}\0${issue.source ?? ""}`;
@@ -753,6 +1113,72 @@ function validateListViewMode(stack) {
753
1113
  return out;
754
1114
  }
755
1115
 
1116
+ // src/validate-functional-completeness.ts
1117
+ var import_kernel = require("@objectstack/spec/kernel");
1118
+ var isRec = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1119
+ function entriesOf(v) {
1120
+ if (Array.isArray(v)) {
1121
+ return v.flatMap(
1122
+ (def, i) => isRec(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
1123
+ );
1124
+ }
1125
+ if (isRec(v)) {
1126
+ return Object.entries(v).flatMap(
1127
+ ([name, def]) => isRec(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
1128
+ );
1129
+ }
1130
+ return [];
1131
+ }
1132
+ function push(out, found, where, basePath) {
1133
+ for (const f of found) {
1134
+ out.push({
1135
+ severity: f.severity,
1136
+ rule: f.rule,
1137
+ where,
1138
+ path: `${basePath}.${f.path}`,
1139
+ message: f.message,
1140
+ hint: f.fix
1141
+ });
1142
+ }
1143
+ }
1144
+ function validateFunctionalCompleteness(stack) {
1145
+ const out = [];
1146
+ if (!isRec(stack)) return out;
1147
+ for (const [oi, obj] of entriesOf(stack.objects).entries()) {
1148
+ for (const field of entriesOf(obj.def.fields)) {
1149
+ push(
1150
+ out,
1151
+ (0, import_kernel.checkFieldCompleteness)(field.def),
1152
+ `object "${obj.name}" \u203A fields.${field.name}`,
1153
+ `objects[${oi}].fields${field.key}`
1154
+ );
1155
+ }
1156
+ }
1157
+ for (const [vi, container] of entriesOf(stack.views).entries()) {
1158
+ const where = container.def.object ? `view container "${container.name}"` : `view container [${vi}]`;
1159
+ if (isRec(container.def.list)) {
1160
+ push(out, (0, import_kernel.checkViewCompleteness)(container.def.list), `${where} \u203A list`, `views[${vi}].list`);
1161
+ }
1162
+ for (const lv of entriesOf(container.def.listViews)) {
1163
+ push(
1164
+ out,
1165
+ (0, import_kernel.checkViewCompleteness)(lv.def),
1166
+ `${where} \u203A listViews.${lv.name}`,
1167
+ `views[${vi}].listViews${lv.key}`
1168
+ );
1169
+ }
1170
+ }
1171
+ for (const hook of entriesOf(stack.webhooks)) {
1172
+ push(
1173
+ out,
1174
+ (0, import_kernel.checkWebhookCompleteness)(hook.def),
1175
+ `webhook "${hook.name}"`,
1176
+ `webhooks${hook.key}`
1177
+ );
1178
+ }
1179
+ return out;
1180
+ }
1181
+
756
1182
  // src/validate-flow-trigger-readiness.ts
757
1183
  var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
758
1184
  var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
@@ -852,7 +1278,7 @@ function validateFlowTriggerReadiness(stack) {
852
1278
 
853
1279
  // src/flow-walk.ts
854
1280
  var import_automation2 = require("@objectstack/spec/automation");
855
- function isRec(v) {
1281
+ function isRec2(v) {
856
1282
  return !!v && typeof v === "object" && !Array.isArray(v);
857
1283
  }
858
1284
  function strName(v) {
@@ -867,7 +1293,7 @@ function flowNodeLabel(node, index) {
867
1293
  return strName(node.label) ?? strName(node.id) ?? `#${index}`;
868
1294
  }
869
1295
  function stripRegions(config) {
870
- if (!isRec(config)) return void 0;
1296
+ if (!isRec2(config)) return void 0;
871
1297
  let out;
872
1298
  for (const key of Object.keys(config)) {
873
1299
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -878,11 +1304,11 @@ function stripRegions(config) {
878
1304
  }
879
1305
  function walkFlowNodes(flow, flowPath) {
880
1306
  const out = [];
881
- if (!isRec(flow)) return out;
1307
+ if (!isRec2(flow)) return out;
882
1308
  const visitList = (nodes, basePath, trail, depth) => {
883
1309
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
884
1310
  nodes.forEach((raw, index) => {
885
- if (!isRec(raw)) return;
1311
+ if (!isRec2(raw)) return;
886
1312
  const path = `${basePath}[${index}]`;
887
1313
  out.push({
888
1314
  node: raw,
@@ -893,7 +1319,7 @@ function walkFlowNodes(flow, flowPath) {
893
1319
  });
894
1320
  const type = strName(raw.type);
895
1321
  const slots = type ? REGION_SLOTS.get(type) : void 0;
896
- if (!slots || !isRec(raw.config)) return;
1322
+ if (!slots || !isRec2(raw.config)) return;
897
1323
  const config = raw.config;
898
1324
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
899
1325
  for (const slot of slots) {
@@ -901,7 +1327,7 @@ function walkFlowNodes(flow, flowPath) {
901
1327
  if (slot === "branches") {
902
1328
  if (!Array.isArray(value)) continue;
903
1329
  value.forEach((branch, b) => {
904
- if (!isRec(branch)) return;
1330
+ if (!isRec2(branch)) return;
905
1331
  const branchName = strName(branch.name) ?? `#${b}`;
906
1332
  visitList(
907
1333
  branch.nodes,
@@ -912,7 +1338,7 @@ function walkFlowNodes(flow, flowPath) {
912
1338
  });
913
1339
  continue;
914
1340
  }
915
- if (!isRec(value)) continue;
1341
+ if (!isRec2(value)) continue;
916
1342
  visitList(
917
1343
  value.nodes,
918
1344
  `${path}.config.${slot}.nodes`,
@@ -1661,10 +2087,12 @@ function validateReactPages(stack) {
1661
2087
  // src/validate-react-page-props.ts
1662
2088
  var import_node_module2 = require("module");
1663
2089
  var import_ui2 = require("@objectstack/spec/ui");
1664
- var import_data3 = require("@objectstack/spec/data");
2090
+ var import_data4 = require("@objectstack/spec/data");
1665
2091
 
1666
2092
  // src/validate-searchable-fields.ts
2093
+ var import_data3 = require("@objectstack/spec/data");
1667
2094
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
2095
+ var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1668
2096
  function asArray10(v) {
1669
2097
  if (Array.isArray(v)) return v;
1670
2098
  if (v && typeof v === "object") {
@@ -1672,21 +2100,50 @@ function asArray10(v) {
1672
2100
  }
1673
2101
  return [];
1674
2102
  }
1675
- function isRec2(v) {
2103
+ function isRec3(v) {
1676
2104
  return !!v && typeof v === "object" && !Array.isArray(v);
1677
2105
  }
1678
2106
  function strName2(v) {
1679
2107
  return typeof v === "string" && v.length > 0 ? v : void 0;
1680
2108
  }
1681
- function declaredFieldNames(obj) {
2109
+ function declaredFieldTarget(obj) {
1682
2110
  const fields = obj.fields;
1683
2111
  if (!fields || typeof fields !== "object") return null;
1684
2112
  const names = /* @__PURE__ */ new Set();
2113
+ const metas = {};
1685
2114
  for (const f of asArray10(fields)) {
1686
2115
  const n = strName2(f.name);
1687
- if (n) names.add(n);
2116
+ if (!n) continue;
2117
+ names.add(n);
2118
+ metas[n] = {
2119
+ type: typeof f.type === "string" ? f.type : void 0,
2120
+ hidden: f.hidden === true
2121
+ };
1688
2122
  }
1689
- return names.size > 0 ? names : null;
2123
+ if (names.size === 0) return null;
2124
+ const searchableFields = Array.isArray(obj.searchableFields) ? obj.searchableFields.filter((e) => typeof e === "string") : void 0;
2125
+ return {
2126
+ names,
2127
+ fields: metas,
2128
+ searchableFields,
2129
+ displayField: strName2(obj.nameField) ?? strName2(obj.displayNameField)
2130
+ };
2131
+ }
2132
+ function resolveAllowedSet(target) {
2133
+ let fields = target.fields;
2134
+ const systemDeclared = (target.searchableFields ?? []).filter(
2135
+ (f) => !target.names.has(f) && SYSTEM_FIELDS.has(f)
2136
+ );
2137
+ if (systemDeclared.length > 0) {
2138
+ fields = { ...fields };
2139
+ for (const f of systemDeclared) fields[f] = {};
2140
+ }
2141
+ const { allowed, source } = (0, import_data3.resolveSearchFieldResolution)({
2142
+ fields,
2143
+ searchableFields: target.searchableFields,
2144
+ displayField: target.displayField
2145
+ });
2146
+ return { allowed: new Set(allowed), source, declaredList: allowed };
1690
2147
  }
1691
2148
  function suggest2(target, known) {
1692
2149
  let best;
@@ -1719,50 +2176,87 @@ function distance(a, b) {
1719
2176
  }
1720
2177
  function indexObjectSearchTargets(stack) {
1721
2178
  const fieldsByObject = /* @__PURE__ */ new Map();
1722
- if (!isRec2(stack)) return fieldsByObject;
2179
+ if (!isRec3(stack)) return fieldsByObject;
1723
2180
  for (const obj of asArray10(stack.objects)) {
1724
2181
  const name = strName2(obj.name);
1725
- if (name) fieldsByObject.set(name, declaredFieldNames(obj));
2182
+ if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
1726
2183
  }
1727
2184
  return fieldsByObject;
1728
2185
  }
1729
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
2186
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
1730
2187
  const findings = [];
1731
2188
  if (!Array.isArray(declared) || declared.length === 0) return findings;
1732
2189
  if (!objectName) return findings;
1733
2190
  if (!fieldsByObject.has(objectName)) return findings;
1734
- const known = fieldsByObject.get(objectName);
1735
- if (!known) return findings;
2191
+ const target = fieldsByObject.get(objectName);
2192
+ if (!target) return findings;
2193
+ const known = target.names;
2194
+ const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
1736
2195
  for (let i = 0; i < declared.length; i++) {
1737
2196
  const entry = declared[i];
1738
2197
  const name = strName2(entry);
1739
2198
  if (!name) continue;
1740
- if (known.has(name) || SYSTEM_FIELDS.has(name)) continue;
1741
- const dotted = name.includes(".");
2199
+ if (!known.has(name) && !SYSTEM_FIELDS.has(name)) {
2200
+ const dotted = name.includes(".");
2201
+ findings.push({
2202
+ severity: "error",
2203
+ rule: SEARCHABLE_FIELD_UNKNOWN,
2204
+ where,
2205
+ path: `${path}[${i}]`,
2206
+ 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)),
2207
+ 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(", ")}.` : "")
2208
+ });
2209
+ continue;
2210
+ }
2211
+ if (!resolution || resolution.allowed.has(name)) continue;
2212
+ if (!known.has(name)) continue;
2213
+ const meta = target.fields[name];
2214
+ if (resolution.source === "declared") {
2215
+ findings.push({
2216
+ severity: "error",
2217
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
2218
+ where,
2219
+ path: `${path}[${i}]`,
2220
+ message: `${subject} entry "${name}" is outside object "${objectName}"'s declared searchableFields (${resolution.declaredList.join(", ")}) \u2014 the set 'search' scans. Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses an entry outside the allowed set: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
2221
+ hint: `Add "${name}" to ${objectName}.searchableFields, or drop it from this view \u2014 a view narrows the object's searchable set, never widens it (ADR-0061).`
2222
+ });
2223
+ continue;
2224
+ }
2225
+ const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
2226
+ let why;
2227
+ if (import_data3.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
2228
+ why = "a system/audit column, which the auto-default set never includes";
2229
+ } else if (meta?.hidden) {
2230
+ why = "hidden";
2231
+ } else if (typeof meta?.type === "string") {
2232
+ why = `of type '${meta.type}', which 'search' cannot scan`;
2233
+ } else {
2234
+ continue;
2235
+ }
1742
2236
  findings.push({
1743
2237
  severity: "error",
1744
- rule: SEARCHABLE_FIELD_UNKNOWN,
2238
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1745
2239
  where,
1746
2240
  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(", ")}.` : "")
2241
+ message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data3.SEARCHABLE_TEXTUAL_TYPES, ...import_data3.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
2242
+ hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text/formula field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1749
2243
  });
1750
2244
  }
1751
2245
  return findings;
1752
2246
  }
1753
2247
  function validateSearchableFields(stack) {
1754
2248
  const findings = [];
1755
- if (!isRec2(stack)) return findings;
2249
+ if (!isRec3(stack)) return findings;
1756
2250
  const objects = asArray10(stack.objects);
1757
2251
  const fieldsByObject = indexObjectSearchTargets(stack);
1758
- const check = (declared, objectName, where, path, subject) => {
2252
+ const check = (declared, objectName, where, path, subject, role) => {
1759
2253
  findings.push(
1760
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
2254
+ ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
1761
2255
  );
1762
2256
  };
1763
2257
  for (let oi = 0; oi < objects.length; oi++) {
1764
2258
  const obj = objects[oi];
1765
- if (!isRec2(obj)) continue;
2259
+ if (!isRec3(obj)) continue;
1766
2260
  const objName = strName2(obj.name);
1767
2261
  const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
1768
2262
  check(
@@ -1770,11 +2264,12 @@ function validateSearchableFields(stack) {
1770
2264
  objName,
1771
2265
  label2,
1772
2266
  `objects[${oi}].searchableFields`,
1773
- "searchableFields"
2267
+ "searchableFields",
2268
+ "canonical"
1774
2269
  );
1775
- if (isRec2(obj.listViews)) {
2270
+ if (isRec3(obj.listViews)) {
1776
2271
  for (const [key, lv] of Object.entries(obj.listViews)) {
1777
- if (!isRec2(lv)) continue;
2272
+ if (!isRec3(lv)) continue;
1778
2273
  check(
1779
2274
  lv.searchableFields,
1780
2275
  // A built-in list view belongs to its object; an inline `data.object`
@@ -1782,7 +2277,8 @@ function validateSearchableFields(stack) {
1782
2277
  listViewObject(lv) ?? objName,
1783
2278
  `${label2} \u203A listViews.${key}`,
1784
2279
  `objects[${oi}].listViews.${key}.searchableFields`,
1785
- "list-view searchableFields"
2280
+ "list-view searchableFields",
2281
+ "narrowing"
1786
2282
  );
1787
2283
  }
1788
2284
  }
@@ -1790,27 +2286,29 @@ function validateSearchableFields(stack) {
1790
2286
  const views = asArray10(stack.views);
1791
2287
  for (let vi = 0; vi < views.length; vi++) {
1792
2288
  const view = views[vi];
1793
- if (!isRec2(view)) continue;
2289
+ if (!isRec3(view)) continue;
1794
2290
  const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
1795
2291
  const viewObject = strName2(view.objectName) ?? strName2(view.object);
1796
- if (isRec2(view.list)) {
2292
+ if (isRec3(view.list)) {
1797
2293
  check(
1798
2294
  view.list.searchableFields,
1799
2295
  listViewObject(view.list) ?? viewObject,
1800
2296
  `view "${viewLabel}" \u203A list`,
1801
2297
  `views[${vi}].list.searchableFields`,
1802
- "list-view searchableFields"
2298
+ "list-view searchableFields",
2299
+ "narrowing"
1803
2300
  );
1804
2301
  }
1805
- if (isRec2(view.listViews)) {
2302
+ if (isRec3(view.listViews)) {
1806
2303
  for (const [key, lv] of Object.entries(view.listViews)) {
1807
- if (!isRec2(lv)) continue;
2304
+ if (!isRec3(lv)) continue;
1808
2305
  check(
1809
2306
  lv.searchableFields,
1810
2307
  listViewObject(lv) ?? viewObject,
1811
2308
  `view "${viewLabel}" \u203A listViews.${key}`,
1812
2309
  `views[${vi}].listViews.${key}.searchableFields`,
1813
- "list-view searchableFields"
2310
+ "list-view searchableFields",
2311
+ "narrowing"
1814
2312
  );
1815
2313
  }
1816
2314
  }
@@ -1819,11 +2317,11 @@ function validateSearchableFields(stack) {
1819
2317
  }
1820
2318
  function listViewObject(listView) {
1821
2319
  const data = listView.data;
1822
- return isRec2(data) ? strName2(data.object) : void 0;
2320
+ return isRec3(data) ? strName2(data.object) : void 0;
1823
2321
  }
1824
2322
 
1825
2323
  // src/page-walk.ts
1826
- function isRec3(v) {
2324
+ function isRec4(v) {
1827
2325
  return !!v && typeof v === "object" && !Array.isArray(v);
1828
2326
  }
1829
2327
  function strName3(v) {
@@ -1836,19 +2334,19 @@ function isSourceAuthoredPage(page) {
1836
2334
  }
1837
2335
  function walkPageComponents(page, pagePath) {
1838
2336
  const out = [];
1839
- if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
2337
+ if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
1840
2338
  const pageObject = strName3(page.object);
1841
2339
  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;
2340
+ if (!isRec4(node)) return;
2341
+ const props = isRec4(node.properties) ? node.properties : void 0;
2342
+ const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
1845
2343
  const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
1846
2344
  out.push({ component: node, path, objectName });
1847
2345
  if (!props) return;
1848
2346
  if (Array.isArray(props.items)) {
1849
2347
  for (let i = 0; i < props.items.length; i++) {
1850
2348
  const item = props.items[i];
1851
- if (!isRec3(item) || !Array.isArray(item.children)) continue;
2349
+ if (!isRec4(item) || !Array.isArray(item.children)) continue;
1852
2350
  for (let c = 0; c < item.children.length; c++) {
1853
2351
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
1854
2352
  }
@@ -1870,12 +2368,12 @@ function walkPageComponents(page, pagePath) {
1870
2368
  const regions = Array.isArray(page.regions) ? page.regions : [];
1871
2369
  for (let r = 0; r < regions.length; r++) {
1872
2370
  const region = regions[r];
1873
- if (!isRec3(region) || !Array.isArray(region.components)) continue;
2371
+ if (!isRec4(region) || !Array.isArray(region.components)) continue;
1874
2372
  for (let c = 0; c < region.components.length; c++) {
1875
2373
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
1876
2374
  }
1877
2375
  }
1878
- const slots = isRec3(page.slots) ? page.slots : void 0;
2376
+ const slots = isRec4(page.slots) ? page.slots : void 0;
1879
2377
  if (slots) {
1880
2378
  for (const [slot, value] of Object.entries(slots)) {
1881
2379
  const list3 = Array.isArray(value) ? value : [value];
@@ -1900,7 +2398,7 @@ function asArray11(v) {
1900
2398
  function strName4(v) {
1901
2399
  return typeof v === "string" && v.length > 0 ? v : void 0;
1902
2400
  }
1903
- function isRec4(v) {
2401
+ function isRec5(v) {
1904
2402
  return !!v && typeof v === "object" && !Array.isArray(v);
1905
2403
  }
1906
2404
  function fieldRefsFrom(value, basePath) {
@@ -1911,7 +2409,7 @@ function fieldRefsFrom(value, basePath) {
1911
2409
  out.push({ name: bare, path });
1912
2410
  return;
1913
2411
  }
1914
- if (!isRec4(v)) return;
2412
+ if (!isRec5(v)) return;
1915
2413
  const named = strName4(v.field) ?? strName4(v.name);
1916
2414
  if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
1917
2415
  };
@@ -1953,15 +2451,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
1953
2451
  const sections = Array.isArray(props[key]) ? props[key] : [];
1954
2452
  for (let si = 0; si < sections.length; si++) {
1955
2453
  const section = sections[si];
1956
- if (!isRec4(section)) continue;
2454
+ if (!isRec5(section)) continue;
1957
2455
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
1958
2456
  }
1959
2457
  }
1960
2458
  return refs;
1961
2459
  }
1962
2460
  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;
2461
+ const add = isRec5(props.add) ? props.add : void 0;
2462
+ const picker = add && isRec5(add.picker) ? add.picker : void 0;
1965
2463
  const at = (key) => `${basePath}${sep}${key}`;
1966
2464
  return {
1967
2465
  relatedObject: strName4(props.objectName),
@@ -1982,7 +2480,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
1982
2480
  }
1983
2481
  function indexObjectFields(stack) {
1984
2482
  const objectFields = /* @__PURE__ */ new Map();
1985
- if (!isRec4(stack)) return objectFields;
2483
+ if (!isRec5(stack)) return objectFields;
1986
2484
  for (const obj of asArray11(stack.objects)) {
1987
2485
  const name = strName4(obj.name);
1988
2486
  if (!name) continue;
@@ -2028,7 +2526,7 @@ function validatePageFieldBindings(stack) {
2028
2526
  };
2029
2527
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2030
2528
  const type = strName4(component.type);
2031
- const props = isRec4(component.properties) ? component.properties : void 0;
2529
+ const props = isRec5(component.properties) ? component.properties : void 0;
2032
2530
  if (!type || !props) continue;
2033
2531
  const where = `page "${pageName}" \xB7 ${type}`;
2034
2532
  const base = `${path}.properties`;
@@ -2043,7 +2541,7 @@ function validatePageFieldBindings(stack) {
2043
2541
  if (!refs) continue;
2044
2542
  checkRefs(refs, objectName, where);
2045
2543
  }
2046
- const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
2544
+ const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2047
2545
  if (cfg) {
2048
2546
  const cfgObject = strName4(cfg.source) ?? strName4(page.object);
2049
2547
  const base = `pages[${pi}].interfaceConfig`;
@@ -2052,7 +2550,7 @@ function validatePageFieldBindings(stack) {
2052
2550
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2053
2551
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2054
2552
  ];
2055
- const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
2553
+ const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2056
2554
  if (userFilters) {
2057
2555
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2058
2556
  }
@@ -2168,28 +2666,28 @@ var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
2168
2666
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
2169
2667
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
2170
2668
  var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
2171
- var isRec5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2669
+ var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2172
2670
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
2173
2671
  function checkObjectChart(attrs, objectFields, findings) {
2174
2672
  const { values, where, path } = attrs;
2175
- const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2673
+ const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2176
2674
  if (values.has("data")) return;
2177
2675
  const aggregate = values.get("aggregate");
2178
2676
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
2179
- if (!isRec5(aggregate)) return;
2677
+ if (!isRec6(aggregate)) return;
2180
2678
  const fn = strOf(aggregate.function);
2181
2679
  const field = strOf(aggregate.field);
2182
2680
  const groupBy = aggregate.groupBy;
2183
- const groupByField = strOf(groupBy) ?? (isRec5(groupBy) ? strOf(groupBy.field) : void 0);
2681
+ const groupByField = strOf(groupBy) ?? (isRec6(groupBy) ? strOf(groupBy.field) : void 0);
2184
2682
  if (fn && !CHART_FUNCTIONS.includes(fn)) {
2185
- push(
2683
+ push2(
2186
2684
  "error",
2187
2685
  REACT_CHART_AGGREGATE_INVALID,
2188
2686
  `aggregate.function "${fn}" is not an aggregation this chart can run.`,
2189
2687
  `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
2190
2688
  );
2191
2689
  } else if (fn && fn !== "count" && !field) {
2192
- push(
2690
+ push2(
2193
2691
  "error",
2194
2692
  REACT_CHART_AGGREGATE_INVALID,
2195
2693
  `aggregate.function "${fn}" has no "field" to aggregate.`,
@@ -2203,7 +2701,7 @@ function checkObjectChart(attrs, objectFields, findings) {
2203
2701
  if (!name) return;
2204
2702
  if (name.includes(".")) return;
2205
2703
  if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
2206
- push(
2704
+ push2(
2207
2705
  "error",
2208
2706
  REACT_CHART_FIELD_UNKNOWN,
2209
2707
  `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.`,
@@ -2220,7 +2718,7 @@ function checkObjectChart(attrs, objectFields, findings) {
2220
2718
  if (!name) return;
2221
2719
  if (columns.includes(name)) return;
2222
2720
  if (keys.comparison && name === keys.comparison) return;
2223
- push(
2721
+ push2(
2224
2722
  "error",
2225
2723
  REACT_CHART_AXIS_UNKNOWN,
2226
2724
  `"${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).`,
@@ -2228,24 +2726,24 @@ function checkObjectChart(attrs, objectFields, findings) {
2228
2726
  );
2229
2727
  };
2230
2728
  const xAxisRaw = values.get("xAxis");
2231
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec5(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2729
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec6(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2232
2730
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
2233
2731
  axisRef(categoryAxis, categoryProp);
2234
2732
  const yAxisRaw = values.get("yAxis");
2235
2733
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
2236
2734
  for (const a of yAxisList) {
2237
- axisRef(strOf(a) ?? (isRec5(a) ? strOf(a.field) : void 0), "yAxis[].field");
2735
+ axisRef(strOf(a) ?? (isRec6(a) ? strOf(a.field) : void 0), "yAxis[].field");
2238
2736
  }
2239
2737
  const series = values.get("series");
2240
2738
  if (Array.isArray(series)) {
2241
2739
  for (const s of series) {
2242
- if (!isRec5(s)) continue;
2740
+ if (!isRec6(s)) continue;
2243
2741
  const dataKey = strOf(s.dataKey);
2244
2742
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
2245
2743
  }
2246
2744
  }
2247
2745
  if (categoryAxis && keys.category && categoryAxis !== keys.category && categoryAxis === keys.value) {
2248
- push(
2746
+ push2(
2249
2747
  "error",
2250
2748
  REACT_CHART_AXIS_UNKNOWN,
2251
2749
  `${categoryProp} "${categoryAxis}" is the aggregate's VALUE column, not its category column.`,
@@ -2295,7 +2793,7 @@ function subformFieldRefs(value, basePath) {
2295
2793
  if (!Array.isArray(value)) return { child, parent };
2296
2794
  for (let i = 0; i < value.length; i++) {
2297
2795
  const sub = value[i];
2298
- if (!isRec5(sub)) continue;
2796
+ if (!isRec6(sub)) continue;
2299
2797
  const at = (key) => `${basePath}[${i}].${key}`;
2300
2798
  child.push({
2301
2799
  objectName: strOf(sub.childObject),
@@ -2320,7 +2818,7 @@ function filterFieldRefs(node, basePath, out) {
2320
2818
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
2321
2819
  return;
2322
2820
  }
2323
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data3.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
2821
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data4.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
2324
2822
  out.push({ name: head, path: `${basePath}[0]` });
2325
2823
  }
2326
2824
  }
@@ -2340,20 +2838,20 @@ function reactFieldRefs(spec, values, basePath) {
2340
2838
  }
2341
2839
  for (const key of spec.nestedFields ?? []) {
2342
2840
  const v = readable(key);
2343
- if (isRec5(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2841
+ if (isRec6(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2344
2842
  }
2345
2843
  for (const key of spec.sections ?? []) {
2346
2844
  const v = readable(key);
2347
2845
  if (!Array.isArray(v)) continue;
2348
2846
  for (let i = 0; i < v.length; i++) {
2349
2847
  const section = v[i];
2350
- if (!isRec5(section)) continue;
2848
+ if (!isRec6(section)) continue;
2351
2849
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
2352
2850
  }
2353
2851
  }
2354
2852
  for (const key of spec.keyedByField ?? []) {
2355
2853
  const v = readable(key);
2356
- if (!isRec5(v)) continue;
2854
+ if (!isRec6(v)) continue;
2357
2855
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
2358
2856
  }
2359
2857
  for (const key of spec.filterArrays ?? []) {
@@ -2379,25 +2877,41 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
2379
2877
  out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
2380
2878
  }
2381
2879
  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
- }
2880
+ if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
2881
+ out.push(
2882
+ ...checkFieldRefs(
2883
+ componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
2884
+ objectName,
2885
+ objectFields,
2886
+ where
2887
+ )
2888
+ );
2398
2889
  }
2399
2890
  return out;
2400
2891
  }
2892
+ var REACT_BLOCK_NEEDS_RECORD_CONTEXT = "react-block-needs-record-context";
2893
+ var RECORD_BLOCK_GENERIC_FIX = "author the page as `type:'record'` \u2014 a record page mounts the record context these blocks render from.";
2894
+ function recordContextFinding(tag, schemaType, where, path) {
2895
+ return {
2896
+ severity: "error",
2897
+ rule: REACT_BLOCK_NEEDS_RECORD_CONTEXT,
2898
+ where,
2899
+ path,
2900
+ message: `<${tag}> renders "${schemaType}", which reads its record from the record context a record page mounts \u2014 a kind:'react' page never mounts one, so the block renders empty no matter how it is bound (its objectName/recordId are not read by the renderer).`,
2901
+ hint: `On a react page bind the record yourself: ${import_ui2.REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
2902
+ };
2903
+ }
2904
+ function localComponentNames(tsc, sf) {
2905
+ const names = /* @__PURE__ */ new Set();
2906
+ const walk = (node) => {
2907
+ if (tsc.isFunctionDeclaration(node) && node.name) names.add(node.name.text);
2908
+ else if (tsc.isVariableDeclaration(node) && tsc.isIdentifier(node.name)) names.add(node.name.text);
2909
+ else if (tsc.isImportSpecifier(node)) names.add(node.name.text);
2910
+ tsc.forEachChild(node, walk);
2911
+ };
2912
+ walk(sf);
2913
+ return names;
2914
+ }
2401
2915
  function validateReactPageProps(stack) {
2402
2916
  const findings = [];
2403
2917
  const objectFields = indexObjectFields(stack);
@@ -2416,9 +2930,18 @@ function validateReactPageProps(stack) {
2416
2930
  } catch {
2417
2931
  continue;
2418
2932
  }
2933
+ const locals = localComponentNames(tsc, sf);
2419
2934
  const visit = (node) => {
2420
2935
  if (tsc.isJsxOpeningElement(node) || tsc.isJsxSelfClosingElement(node)) {
2421
2936
  const tag = node.tagName.getText(sf);
2937
+ const where = `page "${name}" \u203A <${tag}>`;
2938
+ const path = `pages[${p}].source`;
2939
+ const recordType = import_ui2.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
2940
+ if (recordType && !locals.has(tag)) {
2941
+ findings.push(recordContextFinding(tag, recordType, where, path));
2942
+ tsc.forEachChild(node, visit);
2943
+ return;
2944
+ }
2422
2945
  const block = BLOCKS.get(tag);
2423
2946
  if (block) {
2424
2947
  let hasSpread = false;
@@ -2438,8 +2961,14 @@ function validateReactPageProps(stack) {
2438
2961
  );
2439
2962
  }
2440
2963
  }
2441
- const where = `page "${name}" \u203A <${tag}>`;
2442
- const path = `pages[${p}].source`;
2964
+ if (tag === "Block") {
2965
+ const blockType = strOf(values.get("type"));
2966
+ if (blockType && (0, import_ui2.isRecordContextBlockType)(blockType)) {
2967
+ findings.push(recordContextFinding(tag, blockType, where, path));
2968
+ tsc.forEachChild(node, visit);
2969
+ return;
2970
+ }
2971
+ }
2443
2972
  if (!hasSpread) {
2444
2973
  for (const req of block.requiredBindings) {
2445
2974
  if (!used.has(req)) {
@@ -2528,7 +3057,7 @@ function validatePageSourceStyling(stack) {
2528
3057
  }
2529
3058
 
2530
3059
  // src/validate-record-title.ts
2531
- var import_data4 = require("@objectstack/spec/data");
3060
+ var import_data5 = require("@objectstack/spec/data");
2532
3061
  var TITLE_FORMAT_RETIRED = "title-format-retired";
2533
3062
  var TITLE_UNRESOLVABLE = "title-unresolvable";
2534
3063
  function asArray14(v) {
@@ -2556,7 +3085,7 @@ function validateRecordTitle(stack) {
2556
3085
  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.`
2557
3086
  });
2558
3087
  }
2559
- const completeness = (0, import_data4.objectTitleCompleteness)(obj);
3088
+ const completeness = (0, import_data5.objectTitleCompleteness)(obj);
2560
3089
  if (completeness.status === "none") {
2561
3090
  findings.push({
2562
3091
  severity: "warning",
@@ -3859,7 +4388,7 @@ function validateDashboardActionRefs(stack) {
3859
4388
  }
3860
4389
 
3861
4390
  // src/validate-filter-tokens.ts
3862
- var import_data5 = require("@objectstack/spec/data");
4391
+ var import_data6 = require("@objectstack/spec/data");
3863
4392
  var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
3864
4393
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
3865
4394
  function asArray23(v) {
@@ -3872,11 +4401,11 @@ function asArray23(v) {
3872
4401
  function label(v, fallback) {
3873
4402
  return typeof v === "string" && v.length > 0 ? v : fallback;
3874
4403
  }
3875
- var KNOWN_LIST = import_data5.CONTEXT_TOKENS.join("}, {");
4404
+ var KNOWN_LIST = import_data6.CONTEXT_TOKENS.join("}, {");
3876
4405
  function walkFilterValues(node, path, where, out, seen) {
3877
4406
  if (node === null || node === void 0) return;
3878
4407
  if (typeof node === "string") {
3879
- const cls = (0, import_data5.classifyFilterToken)(node);
4408
+ const cls = (0, import_data6.classifyFilterToken)(node);
3880
4409
  if (cls?.kind === "unknown") {
3881
4410
  const suggestion = cls.suggestion;
3882
4411
  out.push({
@@ -4145,16 +4674,86 @@ function validateObjectReferences(stack) {
4145
4674
  return findings;
4146
4675
  }
4147
4676
 
4677
+ // src/validate-nav-target-refs.ts
4678
+ var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
4679
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4680
+ function asArray25(v) {
4681
+ if (Array.isArray(v)) return v.filter(isRec7);
4682
+ if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
4683
+ return [];
4684
+ }
4685
+ function strName7(v) {
4686
+ return typeof v === "string" && v.length > 0 ? v : void 0;
4687
+ }
4688
+ var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
4689
+ var NAV_TARGETS = [
4690
+ ["page", "pageName", "pages", "page"],
4691
+ ["report", "reportName", "reports", "report"],
4692
+ ["dashboard", "dashboardName", "dashboards", "dashboard"]
4693
+ ];
4694
+ function namesOf(collection) {
4695
+ const out = /* @__PURE__ */ new Set();
4696
+ for (const entry of asArray25(collection)) {
4697
+ const n = strName7(entry.name);
4698
+ if (n) out.add(n);
4699
+ }
4700
+ return out;
4701
+ }
4702
+ function validateNavTargetRefs(stack) {
4703
+ const findings = [];
4704
+ if (!isRec7(stack)) return findings;
4705
+ const apps = asArray25(stack.apps);
4706
+ if (apps.length === 0) return findings;
4707
+ const declared = /* @__PURE__ */ new Map();
4708
+ for (const [, , collection] of NAV_TARGETS) {
4709
+ declared.set(collection, namesOf(stack[collection]));
4710
+ }
4711
+ for (const [ai, app] of apps.entries()) {
4712
+ const appName = strName7(app.name) ?? `#${ai}`;
4713
+ const walk = (items, basePath) => {
4714
+ if (!Array.isArray(items)) return;
4715
+ for (const [ni, raw] of items.entries()) {
4716
+ if (!isRec7(raw)) continue;
4717
+ const nav = raw;
4718
+ const navPath = `${basePath}[${ni}]`;
4719
+ for (const [type, prop, collection, noun] of NAV_TARGETS) {
4720
+ if (nav.type !== type) continue;
4721
+ const target = strName7(nav[prop]);
4722
+ if (!target || isInterpolated2(target)) continue;
4723
+ const known = declared.get(collection);
4724
+ if (known.has(target)) continue;
4725
+ const emptyCollection = known.size === 0;
4726
+ findings.push({
4727
+ severity: "warning",
4728
+ rule: NAV_TARGET_UNRESOLVED,
4729
+ where: `app "${appName}" \xB7 nav "${strName7(nav.id) ?? strName7(nav.label) ?? `#${ni}`}"`,
4730
+ path: `${navPath}.${prop}`,
4731
+ message: `Navigation targets ${noun} '${target}', which this stack does not declare in \`${collection}\`. ` + (emptyCollection ? `The stack declares NO ${collection} at all, so \`defineStack\`'s own cross-reference check skipped this entry entirely (it is gated on \`${collection === "pages" ? "pageNames" : collection === "reports" ? "reportNames" : "dashboardNames"}.size > 0\`) \u2014 nothing else will report it. ` : "") + `The entry renders in the sidebar and resolves to nothing when clicked. If another package provides this ${noun}, this is expected and advisory only.`,
4732
+ hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
4733
+ });
4734
+ }
4735
+ if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
4736
+ }
4737
+ };
4738
+ walk(app.navigation, `apps[${ai}].navigation`);
4739
+ for (const [ari, area] of asArray25(app.areas).entries()) {
4740
+ walk(area.items, `apps[${ai}].areas[${ari}].items`);
4741
+ walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
4742
+ }
4743
+ }
4744
+ return findings;
4745
+ }
4746
+
4148
4747
  // src/validate-action-name-refs.ts
4149
4748
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
4150
- function asArray25(v) {
4749
+ function asArray26(v) {
4151
4750
  if (Array.isArray(v)) return v;
4152
4751
  if (v && typeof v === "object") {
4153
4752
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4154
4753
  }
4155
4754
  return [];
4156
4755
  }
4157
- function strName7(v) {
4756
+ function strName8(v) {
4158
4757
  return typeof v === "string" && v.length > 0 ? v : void 0;
4159
4758
  }
4160
4759
  function strList(v) {
@@ -4191,14 +4790,14 @@ function suggest4(target, known) {
4191
4790
  }
4192
4791
  function collectActionNames(stack) {
4193
4792
  const names = /* @__PURE__ */ new Set();
4194
- for (const action of asArray25(stack.actions)) {
4195
- const n = strName7(action?.name);
4793
+ for (const action of asArray26(stack.actions)) {
4794
+ const n = strName8(action?.name);
4196
4795
  if (n) names.add(n);
4197
4796
  }
4198
- for (const obj of asArray25(stack.objects)) {
4797
+ for (const obj of asArray26(stack.objects)) {
4199
4798
  if (!obj || typeof obj !== "object") continue;
4200
- for (const action of asArray25(obj.actions)) {
4201
- const n = strName7(action?.name);
4799
+ for (const action of asArray26(obj.actions)) {
4800
+ const n = strName8(action?.name);
4202
4801
  if (n) names.add(n);
4203
4802
  }
4204
4803
  }
@@ -4208,7 +4807,7 @@ function validateActionNameRefs(stack) {
4208
4807
  const findings = [];
4209
4808
  if (!stack || typeof stack !== "object") return findings;
4210
4809
  const known = collectActionNames(stack);
4211
- const check = (name, where, path, surface) => {
4810
+ const check = (name, where, path, surface, placement = "with the location this surface needs") => {
4212
4811
  if (known.has(name)) return;
4213
4812
  findings.push({
4214
4813
  severity: "error",
@@ -4216,42 +4815,72 @@ function validateActionNameRefs(stack) {
4216
4815
  where,
4217
4816
  path,
4218
4817
  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),
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(", ")}.` : "")
4818
+ hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) ${placement}, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
4220
4819
  });
4221
4820
  };
4222
- const views = asArray25(stack.views);
4821
+ const SELECTION_BAR_PLACEMENT = "(no `locations` entry needed \u2014 the selection bar places it by name)";
4822
+ const checkListContainer = (container, owner, label2, path) => {
4823
+ if (!container || typeof container !== "object") return;
4824
+ const list3 = container;
4825
+ for (const key of ["rowActions", "bulkActions"]) {
4826
+ const names = strList(list3[key]);
4827
+ for (let ai = 0; ai < names.length; ai++) {
4828
+ check(
4829
+ names[ai],
4830
+ `${owner} \xB7 ${label2} \xB7 ${key}`,
4831
+ `${path}.${key}[${ai}]`,
4832
+ key === "bulkActions" ? "Bulk-action menu" : "Row-action menu",
4833
+ key === "bulkActions" ? SELECTION_BAR_PLACEMENT : void 0
4834
+ );
4835
+ }
4836
+ }
4837
+ const defs = Array.isArray(list3.bulkActionDefs) ? list3.bulkActionDefs : [];
4838
+ for (let di = 0; di < defs.length; di++) {
4839
+ const def = defs[di];
4840
+ if (!def || typeof def !== "object") continue;
4841
+ if (def.execution !== "aggregate") continue;
4842
+ if (def.actionDef !== void 0) continue;
4843
+ const name = strName8(def.name);
4844
+ if (!name) continue;
4845
+ check(
4846
+ name,
4847
+ `${owner} \xB7 ${label2} \xB7 bulkActionDefs[${di}]`,
4848
+ `${path}.bulkActionDefs[${di}].name`,
4849
+ "Aggregate bulk action",
4850
+ SELECTION_BAR_PLACEMENT
4851
+ );
4852
+ }
4853
+ };
4854
+ const views = asArray26(stack.views);
4223
4855
  for (let vi = 0; vi < views.length; vi++) {
4224
4856
  const view = views[vi];
4225
4857
  if (!view || typeof view !== "object") continue;
4226
- const viewName = strName7(view.name) ?? strName7(view.object) ?? `#${vi}`;
4227
- const checkListContainer = (container, label2, path) => {
4228
- if (!container || typeof container !== "object") return;
4229
- const list3 = container;
4230
- for (const key of ["rowActions", "bulkActions"]) {
4231
- const names = strList(list3[key]);
4232
- for (let ai = 0; ai < names.length; ai++) {
4233
- check(
4234
- names[ai],
4235
- `view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
4236
- `${path}.${key}[${ai}]`,
4237
- key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
4238
- );
4239
- }
4240
- }
4241
- };
4242
- checkListContainer(view.list, "list", `views[${vi}].list`);
4858
+ const viewName = strName8(view.name) ?? strName8(view.object) ?? `#${vi}`;
4859
+ const owner = `view "${viewName}"`;
4860
+ checkListContainer(view.list, owner, "list", `views[${vi}].list`);
4243
4861
  const listViews = view.listViews;
4244
4862
  if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
4245
4863
  for (const [key, lv] of Object.entries(listViews)) {
4246
- checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
4864
+ checkListContainer(lv, owner, `listViews.${key}`, `views[${vi}].listViews.${key}`);
4247
4865
  }
4248
4866
  }
4249
4867
  }
4250
- const pages = asArray25(stack.pages);
4868
+ const objects = asArray26(stack.objects);
4869
+ for (let oi = 0; oi < objects.length; oi++) {
4870
+ const obj = objects[oi];
4871
+ if (!obj || typeof obj !== "object") continue;
4872
+ const objListViews = obj.listViews;
4873
+ if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
4874
+ const owner = `object "${strName8(obj.name) ?? `#${oi}`}"`;
4875
+ for (const [key, lv] of Object.entries(objListViews)) {
4876
+ checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
4877
+ }
4878
+ }
4879
+ const pages = asArray26(stack.pages);
4251
4880
  for (let pi = 0; pi < pages.length; pi++) {
4252
4881
  const page = pages[pi];
4253
4882
  if (!page || typeof page !== "object") continue;
4254
- const pageName = strName7(page.name) ?? `#${pi}`;
4883
+ const pageName = strName8(page.name) ?? `#${pi}`;
4255
4884
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4256
4885
  const props = component.properties;
4257
4886
  if (!props || typeof props !== "object") continue;
@@ -4259,30 +4888,30 @@ function validateActionNameRefs(stack) {
4259
4888
  for (let ai = 0; ai < names.length; ai++) {
4260
4889
  check(
4261
4890
  names[ai],
4262
- `page "${pageName}" \xB7 component "${strName7(component.type) ?? "?"}"`,
4891
+ `page "${pageName}" \xB7 component "${strName8(component.type) ?? "?"}"`,
4263
4892
  `${path}.properties.actionNames[${ai}]`,
4264
4893
  "Quick-actions bar"
4265
4894
  );
4266
4895
  }
4267
4896
  }
4268
4897
  }
4269
- const apps = asArray25(stack.apps);
4898
+ const apps = asArray26(stack.apps);
4270
4899
  for (let ai = 0; ai < apps.length; ai++) {
4271
4900
  const app = apps[ai];
4272
4901
  if (!app || typeof app !== "object") continue;
4273
- const appName = strName7(app.name) ?? `#${ai}`;
4902
+ const appName = strName8(app.name) ?? `#${ai}`;
4274
4903
  const walkNav = (items, basePath) => {
4275
- const navItems = asArray25(items);
4904
+ const navItems = asArray26(items);
4276
4905
  for (let ni = 0; ni < navItems.length; ni++) {
4277
4906
  const nav = navItems[ni];
4278
4907
  if (!nav || typeof nav !== "object") continue;
4279
4908
  const navPath = `${basePath}[${ni}]`;
4280
4909
  const actionDef = nav.actionDef;
4281
- const actionName = strName7(actionDef?.actionName);
4910
+ const actionName = strName8(actionDef?.actionName);
4282
4911
  if (nav.type === "action" && actionName) {
4283
4912
  check(
4284
4913
  actionName,
4285
- `app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
4914
+ `app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
4286
4915
  `${navPath}.actionDef.actionName`,
4287
4916
  "Navigation action item"
4288
4917
  );
@@ -4291,7 +4920,7 @@ function validateActionNameRefs(stack) {
4291
4920
  }
4292
4921
  };
4293
4922
  walkNav(app.navigation, `apps[${ai}].navigation`);
4294
- const areas = asArray25(app.areas);
4923
+ const areas = asArray26(app.areas);
4295
4924
  for (let ri = 0; ri < areas.length; ri++) {
4296
4925
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4297
4926
  }
@@ -4299,42 +4928,116 @@ function validateActionNameRefs(stack) {
4299
4928
  return findings;
4300
4929
  }
4301
4930
 
4302
- // src/validate-chart-bindings.ts
4303
- var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
4304
- var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
4305
- var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
4306
- var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
4307
- function asArray26(v) {
4931
+ // src/validate-action-locations.ts
4932
+ var ACTION_NO_PLACEMENT = "action-no-placement";
4933
+ function asArray27(v) {
4308
4934
  if (Array.isArray(v)) return v;
4309
4935
  if (v && typeof v === "object") {
4310
4936
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4311
4937
  }
4312
4938
  return [];
4313
4939
  }
4314
- function strName8(v) {
4940
+ function strName9(v) {
4315
4941
  return typeof v === "string" && v.length > 0 ? v : void 0;
4316
4942
  }
4317
4943
  function strList2(v) {
4318
4944
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
4319
4945
  }
4320
- function isRec6(v) {
4321
- return !!v && typeof v === "object" && !Array.isArray(v);
4322
- }
4323
- function distance4(a, b) {
4324
- const m = a.length;
4325
- const n = b.length;
4326
- if (m === 0) return n;
4327
- if (n === 0) return m;
4328
- let prev = Array.from({ length: n + 1 }, (_, j) => j);
4329
- for (let i = 1; i <= m; i++) {
4330
- const curr = [i, ...new Array(n).fill(0)];
4331
- for (let j = 1; j <= n; j++) {
4332
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4333
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
4946
+ function collectNamePlacedActions(stack) {
4947
+ const placed = /* @__PURE__ */ new Set();
4948
+ const harvest = (container) => {
4949
+ if (!container || typeof container !== "object") return;
4950
+ const list3 = container;
4951
+ for (const key of ["rowActions", "bulkActions"]) {
4952
+ for (const n of strList2(list3[key])) placed.add(n);
4334
4953
  }
4335
- prev = curr;
4954
+ for (const def of asArray27(list3.bulkActionDefs)) {
4955
+ const n = strName9(def?.name);
4956
+ if (n) placed.add(n);
4957
+ }
4958
+ };
4959
+ const harvestListViews = (listViews) => {
4960
+ if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
4961
+ for (const lv of Object.values(listViews)) harvest(lv);
4962
+ };
4963
+ for (const view of asArray27(stack.views)) {
4964
+ if (!view || typeof view !== "object") continue;
4965
+ harvest(view.list);
4966
+ harvestListViews(view.listViews);
4336
4967
  }
4337
- return prev[n];
4968
+ for (const obj of asArray27(stack.objects)) {
4969
+ if (!obj || typeof obj !== "object") continue;
4970
+ harvestListViews(obj.listViews);
4971
+ }
4972
+ return placed;
4973
+ }
4974
+ function validateActionLocations(stack) {
4975
+ const findings = [];
4976
+ if (!stack || typeof stack !== "object") return findings;
4977
+ const namePlaced = collectNamePlacedActions(stack);
4978
+ const check = (action, path) => {
4979
+ if (!action || typeof action !== "object") return;
4980
+ if ("locations" in action) return;
4981
+ const name = strName9(action.name);
4982
+ if (!name) return;
4983
+ if (namePlaced.has(name)) return;
4984
+ findings.push({
4985
+ severity: "warning",
4986
+ rule: ACTION_NO_PLACEMENT,
4987
+ where: `action "${name}"`,
4988
+ path,
4989
+ message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
4990
+ hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`, `global_nav`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
4991
+ });
4992
+ };
4993
+ const actions = asArray27(stack.actions);
4994
+ for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
4995
+ const objects = asArray27(stack.objects);
4996
+ for (let oi = 0; oi < objects.length; oi++) {
4997
+ const obj = objects[oi];
4998
+ if (!obj || typeof obj !== "object") continue;
4999
+ const own = asArray27(obj.actions);
5000
+ for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
5001
+ }
5002
+ return findings;
5003
+ }
5004
+
5005
+ // src/validate-chart-bindings.ts
5006
+ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
5007
+ var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
5008
+ var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
5009
+ var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
5010
+ function asArray28(v) {
5011
+ if (Array.isArray(v)) return v;
5012
+ if (v && typeof v === "object") {
5013
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5014
+ }
5015
+ return [];
5016
+ }
5017
+ function strName10(v) {
5018
+ return typeof v === "string" && v.length > 0 ? v : void 0;
5019
+ }
5020
+ function strList3(v) {
5021
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
5022
+ }
5023
+ function isRec8(v) {
5024
+ return !!v && typeof v === "object" && !Array.isArray(v);
5025
+ }
5026
+ function distance4(a, b) {
5027
+ const m = a.length;
5028
+ const n = b.length;
5029
+ if (m === 0) return n;
5030
+ if (n === 0) return m;
5031
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
5032
+ for (let i = 1; i <= m; i++) {
5033
+ const curr = [i, ...new Array(n).fill(0)];
5034
+ for (let j = 1; j <= n; j++) {
5035
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
5036
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
5037
+ }
5038
+ prev = curr;
5039
+ }
5040
+ return prev[n];
4338
5041
  }
4339
5042
  function suggest5(target, known) {
4340
5043
  let best;
@@ -4355,17 +5058,17 @@ function list2(names) {
4355
5058
  }
4356
5059
  function indexDatasets(stack) {
4357
5060
  const out = /* @__PURE__ */ new Map();
4358
- for (const ds of asArray26(stack.datasets)) {
4359
- const name = strName8(ds.name);
5061
+ for (const ds of asArray28(stack.datasets)) {
5062
+ const name = strName10(ds.name);
4360
5063
  if (!name) continue;
4361
5064
  const dimensions = /* @__PURE__ */ new Set();
4362
- for (const d of asArray26(ds.dimensions)) {
4363
- const n = strName8(d.name);
5065
+ for (const d of asArray28(ds.dimensions)) {
5066
+ const n = strName10(d.name);
4364
5067
  if (n) dimensions.add(n);
4365
5068
  }
4366
5069
  const measures = /* @__PURE__ */ new Set();
4367
- for (const m of asArray26(ds.measures)) {
4368
- const n = strName8(m.name);
5070
+ for (const m of asArray28(ds.measures)) {
5071
+ const n = strName10(m.name);
4369
5072
  if (n) measures.add(n);
4370
5073
  }
4371
5074
  out.set(name, { dimensions, measures });
@@ -4443,75 +5146,75 @@ function validateChartBindings(stack) {
4443
5146
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
4444
5147
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
4445
5148
  };
4446
- const reports = asArray26(stack.reports);
5149
+ const reports = asArray28(stack.reports);
4447
5150
  for (let ri = 0; ri < reports.length; ri++) {
4448
5151
  const report = reports[ri];
4449
- if (!isRec6(report)) continue;
4450
- const reportName = strName8(report.name) ?? `#${ri}`;
5152
+ if (!isRec8(report)) continue;
5153
+ const reportName = strName10(report.name) ?? `#${ri}`;
4451
5154
  const checkReportChart = (chart, dataset, values, where, path) => {
4452
- if (!isRec6(chart)) return;
5155
+ if (!isRec8(chart)) return;
4453
5156
  check({
4454
5157
  dataset,
4455
5158
  // `values` is the report's measure SELECTION, not a chart ref; feeding
4456
5159
  // it in lets the yAxis "declared but not selected" check work without
4457
5160
  // reporting the selection itself twice.
4458
5161
  values: { names: values, path: `${path}.values` },
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),
5162
+ xAxis: strName10(chart.xAxis) ? { name: strName10(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
5163
+ yAxis: strName10(chart.yAxis) ? { name: strName10(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
5164
+ series: asArray28(chart.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
4462
5165
  where,
4463
5166
  path: `${path}.chart`
4464
5167
  });
4465
5168
  };
4466
5169
  checkReportChart(
4467
5170
  report.chart,
4468
- strName8(report.dataset),
4469
- strList2(report.values),
5171
+ strName10(report.dataset),
5172
+ strList3(report.values),
4470
5173
  `report "${reportName}" \xB7 chart`,
4471
5174
  `reports[${ri}]`
4472
5175
  );
4473
5176
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
4474
5177
  for (let bi = 0; bi < blocks.length; bi++) {
4475
5178
  const block = blocks[bi];
4476
- if (!isRec6(block)) continue;
5179
+ if (!isRec8(block)) continue;
4477
5180
  checkReportChart(
4478
5181
  block.chart,
4479
- strName8(block.dataset),
4480
- strList2(block.values),
4481
- `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
5182
+ strName10(block.dataset),
5183
+ strList3(block.values),
5184
+ `report "${reportName}" \xB7 block "${strName10(block.name) ?? `#${bi}`}" chart`,
4482
5185
  `reports[${ri}].blocks[${bi}]`
4483
5186
  );
4484
5187
  }
4485
5188
  }
4486
5189
  const checkListChart = (container, where, path) => {
4487
- if (!isRec6(container)) return;
5190
+ if (!isRec8(container)) return;
4488
5191
  const chart = container.chart;
4489
- if (!isRec6(chart)) return;
5192
+ if (!isRec8(chart)) return;
4490
5193
  check({
4491
- dataset: strName8(chart.dataset),
4492
- dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
4493
- values: { names: strList2(chart.values), path: `${path}.chart.values` },
5194
+ dataset: strName10(chart.dataset),
5195
+ dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
5196
+ values: { names: strList3(chart.values), path: `${path}.chart.values` },
4494
5197
  where,
4495
5198
  path: `${path}.chart`
4496
5199
  });
4497
5200
  };
4498
- const views = asArray26(stack.views);
5201
+ const views = asArray28(stack.views);
4499
5202
  for (let vi = 0; vi < views.length; vi++) {
4500
5203
  const view = views[vi];
4501
- if (!isRec6(view)) continue;
4502
- const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
5204
+ if (!isRec8(view)) continue;
5205
+ const viewName = strName10(view.name) ?? strName10(view.objectName) ?? `#${vi}`;
4503
5206
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
4504
- if (isRec6(view.listViews)) {
5207
+ if (isRec8(view.listViews)) {
4505
5208
  for (const [key, lv] of Object.entries(view.listViews)) {
4506
5209
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
4507
5210
  }
4508
5211
  }
4509
5212
  }
4510
- const objects = asArray26(stack.objects);
5213
+ const objects = asArray28(stack.objects);
4511
5214
  for (let oi = 0; oi < objects.length; oi++) {
4512
5215
  const obj = objects[oi];
4513
- if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
4514
- const objName = strName8(obj.name) ?? `#${oi}`;
5216
+ if (!isRec8(obj) || !isRec8(obj.listViews)) continue;
5217
+ const objName = strName10(obj.name) ?? `#${oi}`;
4515
5218
  for (const [key, lv] of Object.entries(obj.listViews)) {
4516
5219
  checkListChart(
4517
5220
  lv,
@@ -4520,22 +5223,22 @@ function validateChartBindings(stack) {
4520
5223
  );
4521
5224
  }
4522
5225
  }
4523
- const pages = asArray26(stack.pages);
5226
+ const pages = asArray28(stack.pages);
4524
5227
  for (let pi = 0; pi < pages.length; pi++) {
4525
5228
  const page = pages[pi];
4526
- if (!isRec6(page)) continue;
4527
- const pageName = strName8(page.name) ?? `#${pi}`;
5229
+ if (!isRec8(page)) continue;
5230
+ const pageName = strName10(page.name) ?? `#${pi}`;
4528
5231
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
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);
5232
+ const props = isRec8(component.properties) ? component.properties : void 0;
5233
+ if (!props || !strName10(props.dataset)) continue;
5234
+ const axisRefs = asArray28(props.yAxis).map((a, ai) => ({ name: strName10(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
5235
+ const seriesRefs = asArray28(props.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
4533
5236
  check({
4534
- dataset: strName8(props.dataset),
4535
- dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
4536
- values: { names: strList2(props.values), path: `${path}.properties.values` },
5237
+ dataset: strName10(props.dataset),
5238
+ dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
5239
+ values: { names: strList3(props.values), path: `${path}.properties.values` },
4537
5240
  series: [...axisRefs, ...seriesRefs],
4538
- where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
5241
+ where: `page "${pageName}" \xB7 ${strName10(component.type) ?? "chart"}`,
4539
5242
  path: `${path}.properties`
4540
5243
  });
4541
5244
  }
@@ -4547,7 +5250,7 @@ function validateChartBindings(stack) {
4547
5250
  var import_system3 = require("@objectstack/spec/system");
4548
5251
 
4549
5252
  // src/build-access-matrix.ts
4550
- function asArray27(v) {
5253
+ function asArray29(v) {
4551
5254
  if (Array.isArray(v)) return v;
4552
5255
  if (v && typeof v === "object") {
4553
5256
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4558,13 +5261,13 @@ function buildAccessMatrix(stack) {
4558
5261
  const entries = [];
4559
5262
  if (!stack || typeof stack !== "object") return { version: 1, entries };
4560
5263
  const owdByObject = /* @__PURE__ */ new Map();
4561
- for (const obj of asArray27(stack.objects)) {
5264
+ for (const obj of asArray29(stack.objects)) {
4562
5265
  const name = typeof obj.name === "string" ? obj.name : "";
4563
5266
  if (!name) continue;
4564
5267
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
4565
5268
  if (typeof owd === "string") owdByObject.set(name, owd);
4566
5269
  }
4567
- for (const ps of asArray27(stack.permissions)) {
5270
+ for (const ps of asArray29(stack.permissions)) {
4568
5271
  const psName = typeof ps.name === "string" ? ps.name : "";
4569
5272
  if (!psName) continue;
4570
5273
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -4637,34 +5340,34 @@ function diffAccessMatrix(before, after) {
4637
5340
 
4638
5341
  // src/validate-nav-access.ts
4639
5342
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
4640
- function asArray28(v) {
5343
+ function asArray30(v) {
4641
5344
  if (Array.isArray(v)) return v;
4642
5345
  if (v && typeof v === "object") {
4643
5346
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4644
5347
  }
4645
5348
  return [];
4646
5349
  }
4647
- function strName9(v) {
5350
+ function strName11(v) {
4648
5351
  return typeof v === "string" && v.length > 0 ? v : void 0;
4649
5352
  }
4650
5353
  function collectNavExposures(stack) {
4651
5354
  const out = [];
4652
- const apps = asArray28(stack.apps);
5355
+ const apps = asArray30(stack.apps);
4653
5356
  for (let ai = 0; ai < apps.length; ai++) {
4654
5357
  const app = apps[ai];
4655
5358
  if (!app || typeof app !== "object") continue;
4656
- const appName = strName9(app.name) ?? `#${ai}`;
5359
+ const appName = strName11(app.name) ?? `#${ai}`;
4657
5360
  const walk = (items, basePath) => {
4658
- const navItems = asArray28(items);
5361
+ const navItems = asArray30(items);
4659
5362
  for (let ni = 0; ni < navItems.length; ni++) {
4660
5363
  const nav = navItems[ni];
4661
5364
  if (!nav || typeof nav !== "object") continue;
4662
5365
  const navPath = `${basePath}[${ni}]`;
4663
- const objectName = strName9(nav.objectName);
5366
+ const objectName = strName11(nav.objectName);
4664
5367
  if (nav.type === "object" && objectName) {
4665
5368
  out.push({
4666
5369
  objectName,
4667
- where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
5370
+ where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
4668
5371
  path: `${navPath}.objectName`
4669
5372
  });
4670
5373
  }
@@ -4672,7 +5375,7 @@ function collectNavExposures(stack) {
4672
5375
  }
4673
5376
  };
4674
5377
  walk(app.navigation, `apps[${ai}].navigation`);
4675
- const areas = asArray28(app.areas);
5378
+ const areas = asArray30(app.areas);
4676
5379
  for (let ri = 0; ri < areas.length; ri++) {
4677
5380
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4678
5381
  }
@@ -4682,13 +5385,13 @@ function collectNavExposures(stack) {
4682
5385
  function validateNavAccess(stack) {
4683
5386
  const findings = [];
4684
5387
  if (!stack || typeof stack !== "object") return findings;
4685
- const permissionSets = asArray28(stack.permissions);
5388
+ const permissionSets = asArray30(stack.permissions);
4686
5389
  if (permissionSets.length === 0) return findings;
4687
5390
  const exposures = collectNavExposures(stack);
4688
5391
  if (exposures.length === 0) return findings;
4689
5392
  const ownObjects = /* @__PURE__ */ new Set();
4690
- for (const obj of asArray28(stack.objects)) {
4691
- const n = strName9(obj.name);
5393
+ for (const obj of asArray30(stack.objects)) {
5394
+ const n = strName11(obj.name);
4692
5395
  if (n) ownObjects.add(n);
4693
5396
  }
4694
5397
  const readable = /* @__PURE__ */ new Set();
@@ -4720,15 +5423,15 @@ function validateNavAccess(stack) {
4720
5423
  var import_system4 = require("@objectstack/spec/system");
4721
5424
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
4722
5425
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
4723
- function isRec7(v) {
5426
+ function isRec9(v) {
4724
5427
  return !!v && typeof v === "object" && !Array.isArray(v);
4725
5428
  }
4726
- function asArray29(v) {
5429
+ function asArray31(v) {
4727
5430
  if (Array.isArray(v)) return v;
4728
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
5431
+ if (isRec9(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec9(def) ? def : {} }));
4729
5432
  return [];
4730
5433
  }
4731
- function strName10(v) {
5434
+ function strName12(v) {
4732
5435
  return typeof v === "string" && v.length > 0 ? v : void 0;
4733
5436
  }
4734
5437
  function distance5(a, b) {
@@ -4786,20 +5489,20 @@ function collectViewRecord(view, factsFor) {
4786
5489
  const addView = (objectName, name) => {
4787
5490
  if (objectName && name) factsFor(objectName).views.add(name);
4788
5491
  };
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));
5492
+ const listBinding = isRec9(view.list) ? bindingOf(view.list) : void 0;
5493
+ if (isRec9(view.list)) addView(listBinding, strName12(view.list.name));
5494
+ addView(recordObject ?? listBinding, strName12(view.name));
4792
5495
  for (const key of ["listViews", "formViews"]) {
4793
5496
  const container = view[key];
4794
- if (!isRec7(container)) continue;
5497
+ if (!isRec9(container)) continue;
4795
5498
  for (const [subKey, sub] of Object.entries(container)) {
4796
- if (!isRec7(sub)) continue;
5499
+ if (!isRec9(sub)) continue;
4797
5500
  const binding = bindingOf(sub) ?? listBinding;
4798
5501
  addView(binding, subKey);
4799
- addView(binding, strName10(sub.name));
5502
+ addView(binding, strName12(sub.name));
4800
5503
  if (binding) {
4801
- for (const section of asArray29(sub.sections)) {
4802
- const sectionName = strName10(section.name);
5504
+ for (const section of asArray31(sub.sections)) {
5505
+ const sectionName = strName12(section.name);
4803
5506
  if (sectionName) factsFor(binding).sections.add(sectionName);
4804
5507
  }
4805
5508
  }
@@ -4807,14 +5510,14 @@ function collectViewRecord(view, factsFor) {
4807
5510
  }
4808
5511
  const sectionBinding = recordObject ?? listBinding;
4809
5512
  if (sectionBinding) {
4810
- for (const section of asArray29(view.sections)) {
4811
- const sectionName = strName10(section.name);
5513
+ for (const section of asArray31(view.sections)) {
5514
+ const sectionName = strName12(section.name);
4812
5515
  if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
4813
5516
  }
4814
5517
  }
4815
5518
  }
4816
5519
  function viewObjectName(view) {
4817
- return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
5520
+ return strName12(view.objectName) ?? strName12(view.object) ?? (isRec9(view.data) ? strName12(view.data.object) : void 0);
4818
5521
  }
4819
5522
  function readOptions(field) {
4820
5523
  const raw = field.options;
@@ -4826,14 +5529,14 @@ function readOptions(field) {
4826
5529
  values.add(opt);
4827
5530
  continue;
4828
5531
  }
4829
- if (!isRec7(opt)) continue;
4830
- const value = strName10(opt.value);
5532
+ if (!isRec9(opt)) continue;
5533
+ const value = strName12(opt.value);
4831
5534
  if (!value) continue;
4832
5535
  values.add(value);
4833
- const label2 = strName10(opt.label);
5536
+ const label2 = strName12(opt.label);
4834
5537
  if (label2) byLabel.set(label2.toLowerCase(), value);
4835
5538
  }
4836
- } else if (isRec7(raw)) {
5539
+ } else if (isRec9(raw)) {
4837
5540
  for (const [value, label2] of Object.entries(raw)) {
4838
5541
  values.add(value);
4839
5542
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -4853,48 +5556,48 @@ function buildUniverse(stack) {
4853
5556
  }
4854
5557
  return facts;
4855
5558
  };
4856
- for (const obj of asArray29(stack.objects)) {
4857
- const objectName = strName10(obj.name);
5559
+ for (const obj of asArray31(stack.objects)) {
5560
+ const objectName = strName12(obj.name);
4858
5561
  if (!objectName) continue;
4859
5562
  const facts = factsFor(objectName);
4860
- for (const field of asArray29(obj.fields)) {
4861
- const fieldName = strName10(field.name);
5563
+ for (const field of asArray31(obj.fields)) {
5564
+ const fieldName = strName12(field.name);
4862
5565
  if (fieldName) facts.fields.set(fieldName, field);
4863
5566
  }
4864
- for (const action of asArray29(obj.actions)) {
4865
- const actionName = strName10(action.name);
5567
+ for (const action of asArray31(obj.actions)) {
5568
+ const actionName = strName12(action.name);
4866
5569
  if (actionName) facts.actions.set(actionName, action);
4867
5570
  }
4868
- for (const view of asArray29(obj.views)) {
4869
- collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
5571
+ for (const view of asArray31(obj.views)) {
5572
+ collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
4870
5573
  }
4871
5574
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
4872
- for (const group of asArray29(obj.fieldGroups)) {
4873
- const key = strName10(group.key) ?? strName10(group.name);
5575
+ for (const group of asArray31(obj.fieldGroups)) {
5576
+ const key = strName12(group.key) ?? strName12(group.name);
4874
5577
  if (key) facts.sections.add(key);
4875
5578
  }
4876
5579
  }
4877
- for (const view of asArray29(stack.views)) {
5580
+ for (const view of asArray31(stack.views)) {
4878
5581
  collectViewRecord(view, factsFor);
4879
5582
  }
4880
- const pages = asArray29(stack.pages);
5583
+ const pages = asArray31(stack.pages);
4881
5584
  for (let pi = 0; pi < pages.length; pi++) {
4882
5585
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
4883
5586
  if (!walked.objectName) continue;
4884
- const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
5587
+ const props = isRec9(walked.component.properties) ? walked.component.properties : void 0;
4885
5588
  if (!props) continue;
4886
- for (const section of asArray29(props.sections)) {
4887
- const sectionName = strName10(section.name);
5589
+ for (const section of asArray31(props.sections)) {
5590
+ const sectionName = strName12(section.name);
4888
5591
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
4889
5592
  }
4890
5593
  }
4891
5594
  }
4892
5595
  const globalActions = /* @__PURE__ */ new Map();
4893
5596
  const actionOwners = /* @__PURE__ */ new Map();
4894
- for (const action of asArray29(stack.actions)) {
4895
- const actionName = strName10(action.name);
5597
+ for (const action of asArray31(stack.actions)) {
5598
+ const actionName = strName12(action.name);
4896
5599
  if (!actionName) continue;
4897
- const owner = strName10(action.objectName) ?? strName10(action.object);
5600
+ const owner = strName12(action.objectName) ?? strName12(action.object);
4898
5601
  if (owner) {
4899
5602
  factsFor(owner).actions.set(actionName, action);
4900
5603
  actionOwners.set(actionName, owner);
@@ -4908,41 +5611,41 @@ function buildUniverse(stack) {
4908
5611
  }
4909
5612
  }
4910
5613
  const apps = /* @__PURE__ */ new Map();
4911
- for (const app of asArray29(stack.apps)) {
4912
- const appName = strName10(app.name);
5614
+ for (const app of asArray31(stack.apps)) {
5615
+ const appName = strName12(app.name);
4913
5616
  if (!appName) continue;
4914
5617
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
4915
5618
  const walkNav = (items) => {
4916
- for (const item of asArray29(items)) {
4917
- const id = strName10(item.id);
5619
+ for (const item of asArray31(items)) {
5620
+ const id = strName12(item.id);
4918
5621
  if (id) navIds.add(id);
4919
5622
  if (item.children) walkNav(item.children);
4920
5623
  }
4921
5624
  };
4922
5625
  walkNav(app.navigation);
4923
- for (const area of asArray29(app.areas)) {
4924
- const areaId = strName10(area.id);
5626
+ for (const area of asArray31(app.areas)) {
5627
+ const areaId = strName12(area.id);
4925
5628
  if (areaId) navIds.add(areaId);
4926
5629
  walkNav(area.navigation);
4927
5630
  }
4928
5631
  apps.set(appName, navIds);
4929
5632
  }
4930
5633
  const dashboards = /* @__PURE__ */ new Map();
4931
- for (const dash of asArray29(stack.dashboards)) {
4932
- const dashName = strName10(dash.name);
5634
+ for (const dash of asArray31(stack.dashboards)) {
5635
+ const dashName = strName12(dash.name);
4933
5636
  if (!dashName) continue;
4934
5637
  const widgets = /* @__PURE__ */ new Set();
4935
- for (const widget of asArray29(dash.widgets)) {
4936
- const id = strName10(widget.id) ?? strName10(widget.name);
5638
+ for (const widget of asArray31(dash.widgets)) {
5639
+ const id = strName12(widget.id) ?? strName12(widget.name);
4937
5640
  if (id) widgets.add(id);
4938
5641
  }
4939
5642
  const actions = /* @__PURE__ */ new Set();
4940
5643
  const headerActions = [
4941
- ...asArray29(isRec7(dash.header) ? dash.header.actions : void 0),
4942
- ...asArray29(dash.actions)
5644
+ ...asArray31(isRec9(dash.header) ? dash.header.actions : void 0),
5645
+ ...asArray31(dash.actions)
4943
5646
  ];
4944
5647
  for (const action of headerActions) {
4945
- const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
5648
+ const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
4946
5649
  if (key) actions.add(key);
4947
5650
  }
4948
5651
  dashboards.set(dashName, { widgets, actions });
@@ -4954,7 +5657,7 @@ function localePath(bundleIndex, locale) {
4954
5657
  }
4955
5658
  function validateTranslationReferences(stack) {
4956
5659
  const findings = [];
4957
- if (!isRec7(stack)) return findings;
5660
+ if (!isRec9(stack)) return findings;
4958
5661
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
4959
5662
  if (bundles.length === 0) return findings;
4960
5663
  const universe = buildUniverse(stack);
@@ -4963,13 +5666,13 @@ function validateTranslationReferences(stack) {
4963
5666
  };
4964
5667
  for (let bi = 0; bi < bundles.length; bi++) {
4965
5668
  const bundle = bundles[bi];
4966
- if (!isRec7(bundle)) continue;
5669
+ if (!isRec9(bundle)) continue;
4967
5670
  for (const [locale, rawData] of Object.entries(bundle)) {
4968
- if (!isRec7(rawData)) continue;
5671
+ if (!isRec9(rawData)) continue;
4969
5672
  const base = localePath(bi, locale);
4970
5673
  const inLocale = `locale "${locale}"`;
4971
5674
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
4972
- if (!isRec7(rawNode)) continue;
5675
+ if (!isRec9(rawNode)) continue;
4973
5676
  const objPath = `${base}.objects.${objectName}`;
4974
5677
  const facts = universe.objects.get(objectName);
4975
5678
  if (!facts) {
@@ -4995,7 +5698,7 @@ function validateTranslationReferences(stack) {
4995
5698
  );
4996
5699
  continue;
4997
5700
  }
4998
- if (!isRec7(rawField)) continue;
5701
+ if (!isRec9(rawField)) continue;
4999
5702
  checkOptionKeys(findings, {
5000
5703
  optionMap: rawField.options,
5001
5704
  field,
@@ -5077,7 +5780,7 @@ function validateTranslationReferences(stack) {
5077
5780
  );
5078
5781
  continue;
5079
5782
  }
5080
- if (!isRec7(rawApp)) continue;
5783
+ if (!isRec9(rawApp)) continue;
5081
5784
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
5082
5785
  if (navIds.has(navId)) continue;
5083
5786
  orphan(
@@ -5100,7 +5803,7 @@ function validateTranslationReferences(stack) {
5100
5803
  );
5101
5804
  continue;
5102
5805
  }
5103
- if (!isRec7(rawDash)) continue;
5806
+ if (!isRec9(rawDash)) continue;
5104
5807
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
5105
5808
  if (dash.widgets.has(widgetId)) continue;
5106
5809
  orphan(
@@ -5125,7 +5828,7 @@ function validateTranslationReferences(stack) {
5125
5828
  return findings;
5126
5829
  }
5127
5830
  function asRecord(v) {
5128
- return isRec7(v) ? v : {};
5831
+ return isRec9(v) ? v : {};
5129
5832
  }
5130
5833
  function checkOptionKeys(findings, ctx) {
5131
5834
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -5137,7 +5840,7 @@ function checkOptionKeys(findings, ctx) {
5137
5840
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
5138
5841
  where: ctx.where,
5139
5842
  path: ctx.path,
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.`,
5843
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
5141
5844
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
5142
5845
  });
5143
5846
  return;
@@ -5156,11 +5859,11 @@ function checkOptionKeys(findings, ctx) {
5156
5859
  }
5157
5860
  }
5158
5861
  function checkActionParams(findings, ctx) {
5159
- const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
5862
+ const rawParams = Object.keys(asRecord(isRec9(ctx.rawAction) ? ctx.rawAction.params : void 0));
5160
5863
  if (rawParams.length === 0) return;
5161
5864
  const declared = /* @__PURE__ */ new Set();
5162
- for (const param of asArray29(ctx.action.params)) {
5163
- const name = strName10(param.name) ?? strName10(param.field);
5865
+ for (const param of asArray31(ctx.action.params)) {
5866
+ const name = strName12(param.name) ?? strName12(param.field);
5164
5867
  if (name) declared.add(name);
5165
5868
  }
5166
5869
  for (const paramName of rawParams) {
@@ -5178,14 +5881,14 @@ function checkActionParams(findings, ctx) {
5178
5881
 
5179
5882
  // src/validate-ai-surface-affinity.ts
5180
5883
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
5181
- function asArray30(v) {
5884
+ function asArray32(v) {
5182
5885
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5183
5886
  if (v && typeof v === "object") {
5184
5887
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5185
5888
  }
5186
5889
  return [];
5187
5890
  }
5188
- function strName11(v) {
5891
+ function strName13(v) {
5189
5892
  return typeof v === "string" && v.length > 0 ? v : void 0;
5190
5893
  }
5191
5894
  function surfaceOf(v) {
@@ -5195,18 +5898,18 @@ function validateAiSurfaceAffinity(stack) {
5195
5898
  const findings = [];
5196
5899
  if (!stack || typeof stack !== "object") return findings;
5197
5900
  const skillsByName = /* @__PURE__ */ new Map();
5198
- for (const skill of asArray30(stack.skills)) {
5199
- const n = strName11(skill.name);
5901
+ for (const skill of asArray32(stack.skills)) {
5902
+ const n = strName13(skill.name);
5200
5903
  if (n) skillsByName.set(n, skill);
5201
5904
  }
5202
- const agents = asArray30(stack.agents);
5905
+ const agents = asArray32(stack.agents);
5203
5906
  for (let ai = 0; ai < agents.length; ai++) {
5204
5907
  const agent = agents[ai];
5205
- const agentName = strName11(agent.name) ?? `#${ai}`;
5908
+ const agentName = strName13(agent.name) ?? `#${ai}`;
5206
5909
  const agentSurface = surfaceOf(agent.surface);
5207
5910
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
5208
5911
  for (let si = 0; si < skillRefs.length; si++) {
5209
- const ref = strName11(skillRefs[si]);
5912
+ const ref = strName13(skillRefs[si]);
5210
5913
  if (!ref) continue;
5211
5914
  const skill = skillsByName.get(ref);
5212
5915
  if (!skill) continue;
@@ -5228,14 +5931,14 @@ function validateAiSurfaceAffinity(stack) {
5228
5931
  // src/validate-ai-tool-references.ts
5229
5932
  var import_system5 = require("@objectstack/spec/system");
5230
5933
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
5231
- function asArray31(v) {
5934
+ function asArray33(v) {
5232
5935
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5233
5936
  if (v && typeof v === "object") {
5234
5937
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5235
5938
  }
5236
5939
  return [];
5237
5940
  }
5238
- function strName12(v) {
5941
+ function strName14(v) {
5239
5942
  return typeof v === "string" && v.length > 0 ? v : void 0;
5240
5943
  }
5241
5944
  function distance6(a, b) {
@@ -5276,26 +5979,26 @@ function materialisesAsTool(action) {
5276
5979
  if (!ai || typeof ai !== "object") return false;
5277
5980
  const aiRec = ai;
5278
5981
  if (aiRec.exposed !== true) return false;
5279
- if (!strName12(aiRec.description)) return false;
5280
- const type = strName12(action.type);
5982
+ if (!strName14(aiRec.description)) return false;
5983
+ const type = strName14(action.type);
5281
5984
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
5282
5985
  if (type === "script") return Boolean(action.target || action.body);
5283
5986
  return Boolean(action.target);
5284
5987
  }
5285
5988
  function collectToolUniverse(stack) {
5286
5989
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
5287
- for (const tool of asArray31(stack.tools)) {
5288
- const n = strName12(tool.name);
5990
+ for (const tool of asArray33(stack.tools)) {
5991
+ const n = strName14(tool.name);
5289
5992
  if (n) universe.add(n);
5290
5993
  }
5291
5994
  const addActionFamily = (actions) => {
5292
- for (const action of asArray31(actions)) {
5293
- const n = strName12(action.name);
5995
+ for (const action of asArray33(actions)) {
5996
+ const n = strName14(action.name);
5294
5997
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
5295
5998
  }
5296
5999
  };
5297
6000
  addActionFamily(stack.actions);
5298
- for (const obj of asArray31(stack.objects)) {
6001
+ for (const obj of asArray33(stack.objects)) {
5299
6002
  addActionFamily(obj.actions);
5300
6003
  }
5301
6004
  return universe;
@@ -5303,13 +6006,13 @@ function collectToolUniverse(stack) {
5303
6006
  function collectUnexposedActionNames(stack) {
5304
6007
  const names = /* @__PURE__ */ new Set();
5305
6008
  const scan = (actions) => {
5306
- for (const action of asArray31(actions)) {
5307
- const n = strName12(action.name);
6009
+ for (const action of asArray33(actions)) {
6010
+ const n = strName14(action.name);
5308
6011
  if (n && !materialisesAsTool(action)) names.add(n);
5309
6012
  }
5310
6013
  };
5311
6014
  scan(stack.actions);
5312
- for (const obj of asArray31(stack.objects)) scan(obj.actions);
6015
+ for (const obj of asArray33(stack.objects)) scan(obj.actions);
5313
6016
  return names;
5314
6017
  }
5315
6018
  function validateAiToolReferences(stack) {
@@ -5327,13 +6030,13 @@ function validateAiToolReferences(stack) {
5327
6030
  }
5328
6031
  return universe.has(ref);
5329
6032
  };
5330
- const skills = asArray31(stack.skills);
6033
+ const skills = asArray33(stack.skills);
5331
6034
  for (let si = 0; si < skills.length; si++) {
5332
6035
  const skill = skills[si];
5333
- const skillName = strName12(skill.name) ?? `#${si}`;
6036
+ const skillName = strName14(skill.name) ?? `#${si}`;
5334
6037
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
5335
6038
  for (let ti = 0; ti < refs.length; ti++) {
5336
- const ref = strName12(refs[ti]);
6039
+ const ref = strName14(refs[ti]);
5337
6040
  if (!ref || resolves(ref)) continue;
5338
6041
  const isPattern = ref.endsWith("*");
5339
6042
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -5352,24 +6055,24 @@ function validateAiToolReferences(stack) {
5352
6055
 
5353
6056
  // src/validate-ai-agent-authoring.ts
5354
6057
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
5355
- function asArray32(v) {
6058
+ function asArray34(v) {
5356
6059
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5357
6060
  if (v && typeof v === "object") {
5358
6061
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5359
6062
  }
5360
6063
  return [];
5361
6064
  }
5362
- function strName13(v) {
6065
+ function strName15(v) {
5363
6066
  return typeof v === "string" && v.length > 0 ? v : void 0;
5364
6067
  }
5365
6068
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
5366
6069
  function validateAiAgentAuthoring(stack) {
5367
6070
  const findings = [];
5368
6071
  if (!stack || typeof stack !== "object") return findings;
5369
- const agents = asArray32(stack.agents);
6072
+ const agents = asArray34(stack.agents);
5370
6073
  for (let ai = 0; ai < agents.length; ai++) {
5371
6074
  const agent = agents[ai];
5372
- const name = strName13(agent.name) ?? `#${ai}`;
6075
+ const name = strName15(agent.name) ?? `#${ai}`;
5373
6076
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
5374
6077
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
5375
6078
  findings.push({
@@ -5474,24 +6177,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
5474
6177
  "owner",
5475
6178
  "record_type"
5476
6179
  ]);
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)) {
6180
+ var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6181
+ function asArray35(v) {
6182
+ if (Array.isArray(v)) return v.filter((x) => isRec10(x));
6183
+ if (isRec10(v)) {
5481
6184
  return Object.entries(v).map(([name, def]) => ({
5482
6185
  name,
5483
- ...isRec8(def) ? def : {}
6186
+ ...isRec10(def) ? def : {}
5484
6187
  }));
5485
6188
  }
5486
6189
  return [];
5487
6190
  }
5488
6191
  function indexObjectFields2(stack) {
5489
6192
  const out = /* @__PURE__ */ new Map();
5490
- for (const obj of asArray33(stack.objects)) {
6193
+ for (const obj of asArray35(stack.objects)) {
5491
6194
  const name = typeof obj.name === "string" ? obj.name : void 0;
5492
6195
  if (!name) continue;
5493
6196
  const names = /* @__PURE__ */ new Set();
5494
- for (const f of asArray33(obj.fields)) {
6197
+ for (const f of asArray35(obj.fields)) {
5495
6198
  if (typeof f.name === "string" && f.name) names.add(f.name);
5496
6199
  }
5497
6200
  out.set(name, names);
@@ -5621,12 +6324,12 @@ ${source}
5621
6324
  }
5622
6325
  function validateHookBodyWrites(stack) {
5623
6326
  const findings = [];
5624
- const hooks = asArray33(stack.hooks);
6327
+ const hooks = asArray35(stack.hooks);
5625
6328
  if (hooks.length === 0) return findings;
5626
6329
  let objectFields = null;
5627
6330
  hooks.forEach((hook, hookIndex) => {
5628
6331
  const body = hook.body;
5629
- if (!isRec8(body) || body.language !== "js") return;
6332
+ if (!isRec10(body) || body.language !== "js") return;
5630
6333
  const source = body.source;
5631
6334
  if (typeof source !== "string" || source.trim() === "") return;
5632
6335
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -5707,13 +6410,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
5707
6410
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
5708
6411
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
5709
6412
  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)) {
6413
+ var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6414
+ function asArray36(v) {
6415
+ if (Array.isArray(v)) return v.filter((x) => isRec11(x));
6416
+ if (isRec11(v)) {
5714
6417
  return Object.entries(v).map(([name, def]) => ({
5715
6418
  name,
5716
- ...isRec9(def) ? def : {}
6419
+ ...isRec11(def) ? def : {}
5717
6420
  }));
5718
6421
  }
5719
6422
  return [];
@@ -5727,9 +6430,11 @@ function collectActionBodies(stack) {
5727
6430
  const sites = [];
5728
6431
  const seen = /* @__PURE__ */ new Set();
5729
6432
  const collect = (actions, pathPrefix, parentObject) => {
5730
- asArray34(actions).forEach((action, index) => {
6433
+ asArray36(actions).forEach((action, index) => {
6434
+ const type = typeof action.type === "string" ? action.type : "script";
6435
+ if (type !== "script") return;
5731
6436
  const body = action.body;
5732
- if (!isRec9(body) || body.language !== "js") return;
6437
+ if (!isRec11(body) || body.language !== "js") return;
5733
6438
  const source = body.source;
5734
6439
  if (typeof source !== "string" || source.trim() === "") return;
5735
6440
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -5740,7 +6445,7 @@ function collectActionBodies(stack) {
5740
6445
  });
5741
6446
  };
5742
6447
  collect(stack.actions, "actions");
5743
- asArray34(stack.objects).forEach((obj, objIndex) => {
6448
+ asArray36(stack.objects).forEach((obj, objIndex) => {
5744
6449
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
5745
6450
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
5746
6451
  });
@@ -5748,7 +6453,7 @@ function collectActionBodies(stack) {
5748
6453
  }
5749
6454
  function validateActionBodyWrites(stack) {
5750
6455
  const findings = [];
5751
- if (!isRec9(stack)) return findings;
6456
+ if (!isRec11(stack)) return findings;
5752
6457
  const sites = collectActionBodies(stack);
5753
6458
  if (sites.length === 0) return findings;
5754
6459
  let objectFields = null;
@@ -5807,13 +6512,13 @@ var import_shared3 = require("@objectstack/spec/shared");
5807
6512
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
5808
6513
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
5809
6514
  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)) {
6515
+ var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6516
+ function asArray37(v) {
6517
+ if (Array.isArray(v)) return v.filter((x) => isRec12(x));
6518
+ if (isRec12(v)) {
5814
6519
  return Object.entries(v).map(([name, def]) => ({
5815
6520
  name,
5816
- ...isRec10(def) ? def : {}
6521
+ ...isRec12(def) ? def : {}
5817
6522
  }));
5818
6523
  }
5819
6524
  return [];
@@ -5826,8 +6531,8 @@ function readLiteralObjectName2(config) {
5826
6531
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
5827
6532
  function validateFlowNodeWrites(stack) {
5828
6533
  const findings = [];
5829
- if (!isRec10(stack)) return findings;
5830
- const flows = asArray35(stack.flows);
6534
+ if (!isRec12(stack)) return findings;
6535
+ const flows = asArray37(stack.flows);
5831
6536
  if (flows.length === 0) return findings;
5832
6537
  let objectFields = null;
5833
6538
  flows.forEach((flow, flowIndex) => {
@@ -5835,10 +6540,10 @@ function validateFlowNodeWrites(stack) {
5835
6540
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
5836
6541
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
5837
6542
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
5838
- const config = isRec10(node.config) ? node.config : void 0;
6543
+ const config = isRec12(node.config) ? node.config : void 0;
5839
6544
  if (!config) return;
5840
6545
  const fields = config.fields;
5841
- if (!isRec10(fields)) return;
6546
+ if (!isRec12(fields)) return;
5842
6547
  const written = Object.keys(fields);
5843
6548
  if (written.length === 0) return;
5844
6549
  const objectName = readLiteralObjectName2(config);
@@ -5877,6 +6582,13 @@ var REFERENCE_INTEGRITY_RULES = [
5877
6582
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5878
6583
  { name: "validateChartBindings", run: validateChartBindings },
5879
6584
  { name: "validateNavAccess", run: validateNavAccess },
6585
+ // Nav targets that are NOT object names — page/report/dashboard. Restores the
6586
+ // coverage `defineStack`'s own cross-reference block switches off whenever the
6587
+ // stack declares none of that collection (`pageNames.size > 0 && …`), which is
6588
+ // exactly the state a stack is in when the target was never written.
6589
+ // `action` is deliberately absent (validateActionNameRefs owns it) and so is
6590
+ // `component` (an unregistered ref renders a named diagnostic, not silence).
6591
+ { name: "validateNavTargetRefs", run: validateNavTargetRefs },
5880
6592
  { name: "validateTranslationReferences", run: validateTranslationReferences },
5881
6593
  { name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
5882
6594
  { name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
@@ -5924,12 +6636,20 @@ var REFERENCE_INTEGRITY_RULES = [
5924
6636
  { name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
5925
6637
  // The `kind:'react'` page surface. Every prop a react block binds BY FIELD
5926
6638
  // NAME is resolved against the object it names (#4340) — `<ListView columns>`,
5927
- // `<ObjectForm fields>`, the `record:*` family through the SAME
6639
+ // `<ObjectForm fields>`, `<Block type="element:…">` through the SAME
5928
6640
  // `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
5929
6641
  // over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
5930
6642
  // `searchableFields` (#4329). Squarely the charter's question, on the surface
5931
6643
  // where it had no answer at all.
5932
6644
  //
6645
+ // It also carries `react-block-needs-record-context` (#4413) — a BINDING
6646
+ // question rather than a resolution one: the `record:*` family reads its
6647
+ // record from a record page's context, so on THIS surface the binding does
6648
+ // not exist at all and the props the contract published for it were read by
6649
+ // no renderer. This rule used to resolve those props' field names against
6650
+ // the object they named — lint standing guard over a binding that never ran.
6651
+ // It rejects the blocks now, out of the same parse.
6652
+ //
5933
6653
  // It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
5934
6654
  // accepted a react page whose every field binding was stale — including the
5935
6655
  // gating ones (a missing required binding, a filter position naming no field:
@@ -5953,6 +6673,1488 @@ function validateReferenceIntegrity(stack) {
5953
6673
  }
5954
6674
  return findings;
5955
6675
  }
6676
+
6677
+ // src/lint-flow-patterns.ts
6678
+ function asArray38(v) {
6679
+ if (Array.isArray(v)) return v;
6680
+ if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6681
+ return [];
6682
+ }
6683
+ function conditionSource(raw) {
6684
+ if (typeof raw === "string") return raw;
6685
+ if (raw && typeof raw === "object" && typeof raw.source === "string") return raw.source;
6686
+ return "";
6687
+ }
6688
+ var TIME_FNS = "daysFromNow|daysAgo|today|now|date|datetime";
6689
+ var TIME_FN_RE = new RegExp(`\\b(?:${TIME_FNS})\\s*\\(`);
6690
+ var DATE_EQ = new RegExp(
6691
+ `(?:(?:${TIME_FNS})\\s*\\([^)]*\\)\\s*(?:==|!=))|(?:(?:==|!=)\\s*(?:${TIME_FNS})\\s*\\()`
6692
+ );
6693
+ var FLOW_TIME_RELATIVE_ANTIPATTERN = "flow-time-relative-antipattern";
6694
+ var FLOW_DATE_EQUALITY_FILTER = "flow-date-equality-filter";
6695
+ var FLOW_PHANTOM_AGGREGATION = "flow-phantom-aggregation";
6696
+ var FLOW_DOUBLE_BRACE_INTERP = "flow-double-brace-interpolation";
6697
+ var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
6698
+ var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
6699
+ var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
6700
+ var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
6701
+ var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
6702
+ var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
6703
+ var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
6704
+ var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
6705
+ var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
6706
+ var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
6707
+ var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
6708
+ var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
6709
+ "decision",
6710
+ "assignment",
6711
+ "loop",
6712
+ "parallel",
6713
+ "try_catch",
6714
+ "create_record",
6715
+ "update_record",
6716
+ "delete_record",
6717
+ "get_record",
6718
+ "http",
6719
+ "notify",
6720
+ "script",
6721
+ "screen",
6722
+ "wait",
6723
+ "subflow",
6724
+ "map",
6725
+ "connector_action",
6726
+ "approval",
6727
+ "end"
6728
+ ]);
6729
+ var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
6730
+ var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
6731
+ var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
6732
+ function isScheduleTriggered(flow, startCfg) {
6733
+ if (flow.type === "schedule") return true;
6734
+ if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "schedule") return true;
6735
+ return startCfg.schedule != null;
6736
+ }
6737
+ function userLessTriggerKind(flow, startCfg) {
6738
+ if (isScheduleTriggered(flow, startCfg)) return "schedule";
6739
+ if (startCfg.timeRelative != null) return "time-relative";
6740
+ if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "time_relative") return "time-relative";
6741
+ if (flow.type === "api") return "api";
6742
+ if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "api") return "api";
6743
+ return null;
6744
+ }
6745
+ var PHANTOM_AGG_KEYS = /* @__PURE__ */ new Set(["aggregations", "aggregate", "groupBy", "rollup", "having"]);
6746
+ function celTimeSource(v) {
6747
+ if (v && typeof v === "object" && v.dialect === "cel") {
6748
+ const src = v.source;
6749
+ if (typeof src === "string" && TIME_FN_RE.test(src)) return src;
6750
+ }
6751
+ return null;
6752
+ }
6753
+ var RANGE_OPS = /* @__PURE__ */ new Set(["$gte", "$gt", "$lte", "$lt", "$ne"]);
6754
+ function scanFilterForDateEquality(filter, where, findings) {
6755
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) return;
6756
+ for (const [key, val] of Object.entries(filter)) {
6757
+ if (key === "$or" || key === "$and") {
6758
+ if (Array.isArray(val)) for (const sub of val) scanFilterForDateEquality(sub, where, findings);
6759
+ continue;
6760
+ }
6761
+ const direct = celTimeSource(val);
6762
+ let hit = direct ? { op: "==", src: direct } : null;
6763
+ if (!hit && val && typeof val === "object" && val.dialect !== "cel") {
6764
+ for (const [op, operand] of Object.entries(val)) {
6765
+ if (RANGE_OPS.has(op)) continue;
6766
+ if (op === "$eq") {
6767
+ const s = celTimeSource(operand);
6768
+ if (s) {
6769
+ hit = { op: "$eq", src: s };
6770
+ break;
6771
+ }
6772
+ } else if (op === "$in" && Array.isArray(operand)) {
6773
+ for (const item of operand) {
6774
+ const s = celTimeSource(item);
6775
+ if (s) {
6776
+ hit = { op: "$in", src: s };
6777
+ break;
6778
+ }
6779
+ }
6780
+ if (hit) break;
6781
+ }
6782
+ }
6783
+ }
6784
+ if (hit) {
6785
+ findings.push({
6786
+ where,
6787
+ message: `filter matches \`${key}\` by ${hit.op} against a time value (\`${hit.src}\`) \u2014 a date field carries a time component, so exact equality against \`${hit.src}\` (re-computed each run) silently matches nothing.`,
6788
+ hint: `Use a one-day window instead: \`${key}: { $gte: daysFromNow(N), $lt: daysFromNow(N+1) }\` (wrap multiple tiers in \`$or\`). The abutting windows tile the timeline so each row matches exactly once. (#1874)`,
6789
+ rule: FLOW_DATE_EQUALITY_FILTER
6790
+ });
6791
+ }
6792
+ }
6793
+ }
6794
+ var DOUBLE_BRACE = /\{\{\s*[\w$][\w$.\s]*\}\}/;
6795
+ var BARE_DOLLAR_REF = /(?:^|[^{])\$[A-Za-z_]\w*\.[A-Za-z_]/;
6796
+ var CEL_KEYS = /* @__PURE__ */ new Set(["condition", "expression", "conditions"]);
6797
+ function collectTemplateStrings(value, key, out) {
6798
+ if (key && CEL_KEYS.has(key)) return;
6799
+ if (typeof value === "string") {
6800
+ out.push(value);
6801
+ return;
6802
+ }
6803
+ if (Array.isArray(value)) {
6804
+ for (const v of value) collectTemplateStrings(v, key, out);
6805
+ return;
6806
+ }
6807
+ if (value && typeof value === "object") {
6808
+ for (const [k, v] of Object.entries(value)) collectTemplateStrings(v, k, out);
6809
+ }
6810
+ }
6811
+ function edgeLabelOf(e) {
6812
+ return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
6813
+ }
6814
+ function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
6815
+ const typeById = /* @__PURE__ */ new Map();
6816
+ for (const n of nodes) {
6817
+ if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
6818
+ }
6819
+ for (const e of edges) {
6820
+ const label2 = typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
6821
+ if (!ERROR_LABELS.has(label2)) continue;
6822
+ if (e.type === "fault") continue;
6823
+ if (e.condition) continue;
6824
+ const src = typeof e.source === "string" ? e.source : "";
6825
+ if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
6826
+ findings.push({
6827
+ where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6828
+ message: `edge is labelled '${String(e.label)}' but its type is '${String(e.type ?? "default")}', not 'fault' \u2014 so it is an ORDINARY out-edge. Unconditional out-edges all run in parallel, so '${String(e.target)}' executes on every SUCCESSFUL run of '${src}' and never on a failure. The error path the label describes does not exist, and the run still aborts when '${src}' fails.`,
6829
+ hint: `Add \`type: 'fault'\` to this edge. Only runtime failures route \u2014 a guard refusal (a filter token that resolved to nothing, a missing required config key, an unscoped run) stays fatal by design and must be fixed in the metadata, not handled. (#3863)`,
6830
+ rule: FLOW_ERROR_LABEL_NOT_FAULT
6831
+ });
6832
+ }
6833
+ }
6834
+ function scanBranchRouting(flowName, nodes, edges, findings) {
6835
+ const outEdgesBySource = /* @__PURE__ */ new Map();
6836
+ for (const e of edges) {
6837
+ if (e.type === "fault") continue;
6838
+ const src = typeof e.source === "string" ? e.source : "";
6839
+ if (!src) continue;
6840
+ if (!outEdgesBySource.has(src)) outEdgesBySource.set(src, []);
6841
+ outEdgesBySource.get(src).push(e);
6842
+ }
6843
+ for (const [src, outs] of outEdgesBySource) {
6844
+ for (const e of outs) {
6845
+ if (e.isDefault === true && e.condition) {
6846
+ findings.push({
6847
+ where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6848
+ message: `edge sets \`isDefault: true\` AND a \`condition\` \u2014 contradictory. \`isDefault\` means "take this edge when NO sibling condition matched"; a condition makes it an ordinary guarded branch. The condition wins and the default marker routes nothing.`,
6849
+ hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
6850
+ rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
6851
+ // Gating: the two keys contradict, the condition always wins, and the
6852
+ // marker never routes. No reading makes it do what it says.
6853
+ severity: "error"
6854
+ });
6855
+ }
6856
+ }
6857
+ const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
6858
+ if (defaults.length > 1) {
6859
+ findings.push({
6860
+ where: `flow '${flowName}' \xB7 node '${src}'`,
6861
+ message: `${defaults.length} out-edges are marked \`isDefault: true\` (${defaults.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 a node has at most ONE default path. All of them are traversed together when no condition matches, which is a parallel fan-out, not an "otherwise".`,
6862
+ hint: `Keep \`isDefault: true\` on the single fallback edge and give the others a \`condition\` (or leave them unconditional if the fan-out really is intended). (#4414)`,
6863
+ rule: FLOW_MULTIPLE_DEFAULT_EDGES
6864
+ });
6865
+ }
6866
+ }
6867
+ for (const node of nodes) {
6868
+ const nodeType = typeof node.type === "string" ? node.type : "";
6869
+ if (!INERT_CONDITION_NODE_TYPES.has(nodeType)) continue;
6870
+ const cfg = node.config ?? {};
6871
+ if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
6872
+ findings.push({
6873
+ where: `flow '${flowName}' \xB7 node '${String(node.id)}' (${nodeType})`,
6874
+ message: `\`config.condition\` is set but nothing reads it \u2014 the key is the trigger gate on a \`start\` node and is ignored on every other node type, so this predicate never gates anything. (It is still parse-validated at registration, which is why a malformed one is caught and an inert one is not.)`,
6875
+ hint: nodeType === "decision" ? `Branching lives on the OUT-EDGES: give each branch its own \`condition\` and mark the fallback \`isDefault: true\`. If the edges already carry the predicate, delete this copy. (#4414)` : `Delete it, or move the predicate to the incoming edge's \`condition\` if this step was meant to be conditional. (#4414)`,
6876
+ rule: FLOW_INERT_NODE_CONDITION
6877
+ });
6878
+ }
6879
+ for (const node of nodes) {
6880
+ if (node.type !== "decision") continue;
6881
+ const nid = typeof node.id === "string" ? node.id : "";
6882
+ if (!nid) continue;
6883
+ const outs = outEdgesBySource.get(nid) ?? [];
6884
+ if (outs.length === 0) continue;
6885
+ const cfg = node.config ?? {};
6886
+ const declaredLabels = new Set(
6887
+ (Array.isArray(cfg.conditions) ? cfg.conditions : []).map((c) => typeof c?.label === "string" ? c.label.trim().toLowerCase() : "").filter(Boolean)
6888
+ );
6889
+ const edgeLabels = new Set(outs.map(edgeLabelOf).filter(Boolean));
6890
+ const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
6891
+ if (unclaimed.length > 0) {
6892
+ findings.push({
6893
+ where: `flow '${flowName}' \xB7 decision '${nid}'`,
6894
+ message: `declares branch label(s) ${unclaimed.map((l) => `'${l}'`).join(", ")} that no out-edge carries \u2014 out-edge labels are [${[...edgeLabels].map((l) => `'${l}'`).join(", ") || "none"}]. Traversal cannot honour a label nothing claims, so it falls back to considering EVERY out-edge and the branch the decision computed is ignored.`,
6895
+ hint: `Make an out-edge's \`label\` match the declared branch exactly, or drop \`config.conditions\` and branch on the edges instead (\`condition\` per branch + \`isDefault: true\` on the fallback) \u2014 one mechanism per decision, never both. (#4414)`,
6896
+ rule: FLOW_BRANCH_LABEL_UNMATCHED,
6897
+ // Gating: a label nothing claims cannot route under ANY reading, on
6898
+ // every run. See the severity policy at the top of this file.
6899
+ severity: "error"
6900
+ });
6901
+ }
6902
+ const gated = outs.filter((e) => e.condition || e.isDefault === true);
6903
+ if (gated.length === 0) continue;
6904
+ const ungated = outs.filter(
6905
+ (e) => !e.condition && e.isDefault !== true && !declaredLabels.has(edgeLabelOf(e))
6906
+ );
6907
+ if (ungated.length > 0) {
6908
+ findings.push({
6909
+ where: `flow '${flowName}' \xB7 decision '${nid}'`,
6910
+ message: `has guarded out-edge(s) alongside unconditional one(s) (${ungated.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 an unconditional out-edge is traversed on EVERY pass, in parallel with whichever guarded branch matched, so the decision does not actually exclude it. A \`label\` alone does not select a path unless the decision declares a matching \`conditions[].label\`.`,
6911
+ hint: `Mark the fallback \`isDefault: true\` so it is taken only when no sibling condition matched (BPMN default flow), or give it its own \`condition\`. (#4414)`,
6912
+ rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
6913
+ });
6914
+ }
6915
+ }
6916
+ }
6917
+ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6918
+ const approvals = nodes.filter((n) => n.type === "approval");
6919
+ if (approvals.length === 0) return;
6920
+ const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
6921
+ const outEdges = /* @__PURE__ */ new Map();
6922
+ for (const e of edges) {
6923
+ const src = typeof e.source === "string" ? e.source : "";
6924
+ if (!src) continue;
6925
+ if (!outEdges.has(src)) outEdges.set(src, []);
6926
+ outEdges.get(src).push(e);
6927
+ }
6928
+ for (const a of approvals) {
6929
+ const aid = typeof a.id === "string" ? a.id : "";
6930
+ if (!aid) continue;
6931
+ const reviseTargets = edges.filter((e) => e.source === aid && edgeLabelOf(e) === "revise").map((e) => typeof e.target === "string" ? e.target : "").filter((t) => t && nodeIds.has(t));
6932
+ if (reviseTargets.length === 0) continue;
6933
+ const where = `flow '${flowName}' \xB7 approval '${aid}'`;
6934
+ const cfg = a.config ?? {};
6935
+ if (cfg.maxRevisions === 0) {
6936
+ findings.push({
6937
+ where,
6938
+ message: `declares a 'revise' out-edge but \`maxRevisions: 0\` disables send-back \u2014 every revise auto-rejects, so the revise branch never runs.`,
6939
+ hint: `Set \`maxRevisions\` >= 1 to allow N send-backs before auto-reject, or drop the 'revise' edge if send-back isn't intended (ADR-0044).`,
6940
+ rule: FLOW_APPROVAL_REVISE_DISABLED
6941
+ });
6942
+ }
6943
+ const seen = new Set(reviseTargets);
6944
+ const queue = [...reviseTargets];
6945
+ const returnEdges = [];
6946
+ while (queue.length) {
6947
+ const cur = queue.shift();
6948
+ for (const e of outEdges.get(cur) ?? []) {
6949
+ if (e.target === aid) returnEdges.push(e);
6950
+ const t = typeof e.target === "string" ? e.target : "";
6951
+ if (t && nodeIds.has(t) && !seen.has(t)) {
6952
+ seen.add(t);
6953
+ queue.push(t);
6954
+ }
6955
+ }
6956
+ }
6957
+ if (returnEdges.length === 0) {
6958
+ findings.push({
6959
+ where,
6960
+ message: `has a 'revise' out-edge but no path loops back to it \u2014 the submitter reworks the record with nowhere to resubmit, so the revise branch dead-ends. (registerFlow accepts this \u2014 it's a valid DAG.)`,
6961
+ hint: `Close the loop: the 'revise' edge should reach a wait node whose resubmit edge returns to '${aid}' marked \`type: 'back'\` (ADR-0044). See examples/app-showcase showcase_budget_approval.`,
6962
+ rule: FLOW_APPROVAL_REVISE_DEAD_END
6963
+ });
6964
+ } else if (!returnEdges.some((e) => e.type === "back")) {
6965
+ findings.push({
6966
+ where,
6967
+ message: `has a 'revise' loop that returns to it, but the closing edge isn't declared \`type: 'back'\` \u2014 registerFlow rejects this as an un-declared cycle.`,
6968
+ hint: `Mark the resubmit edge (whose target is '${aid}') \`type: 'back'\` so cycle validation skips it while it still traverses at runtime; \`maxRevisions\` guards the loop (ADR-0044).`,
6969
+ rule: FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE
6970
+ });
6971
+ }
6972
+ }
6973
+ }
6974
+ function lintFlowPatterns(stack) {
6975
+ const findings = [];
6976
+ for (const flow of asArray38(stack.flows)) {
6977
+ const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
6978
+ const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
6979
+ const edges = Array.isArray(flow.edges) ? flow.edges : [];
6980
+ const start = nodes.find((n) => n.type === "start");
6981
+ const startCfg = start?.config ?? {};
6982
+ const triggerType = typeof startCfg.triggerType === "string" ? startCfg.triggerType : "";
6983
+ if (triggerType.startsWith("record-")) {
6984
+ const src = conditionSource(startCfg.condition).trim();
6985
+ if (src && DATE_EQ.test(src)) {
6986
+ findings.push({
6987
+ where: `flow '${flowName}' \xB7 start condition`,
6988
+ message: `record-change trigger uses a date-EQUALITY time condition (\`${src}\`) \u2014 it only fires if the record happens to be written on that exact day, so unattended "N days before" rules never run.`,
6989
+ hint: `Use a SCHEDULE trigger (daily cron) + a range query instead \u2014 e.g. a scheduled flow whose get_record filters \`end_date\` BETWEEN {TODAY()} and {TODAY()+N}. (#1874)`,
6990
+ rule: FLOW_TIME_RELATIVE_ANTIPATTERN
6991
+ });
6992
+ }
6993
+ }
6994
+ const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
6995
+ const userLessKind = userLessTriggerKind(flow, startCfg);
6996
+ if (userLessKind && runAs !== "system") {
6997
+ const dataNode = nodes.find((n) => DATA_NODE_TYPES.has(typeof n.type === "string" ? n.type : ""));
6998
+ if (dataNode) {
6999
+ const declared = typeof flow.runAs === "string" ? `\`runAs:'${runAs}'\`` : `the default \`runAs:'user'\``;
7000
+ findings.push({
7001
+ where: `flow '${flowName}' \xB7 runAs`,
7002
+ message: `${userLessKind}-triggered flow runs as ${declared}, but a ${userLessKind} run has no trigger user \u2014 so its data node '${dataNode.id}' (${dataNode.type}) has no identity to scope to and will be REFUSED at run time.`,
7003
+ hint: `Declare \`runAs:'system'\` to make the elevation explicit and intended (the run reads/writes every record). A ${userLessKind} flow cannot scope to a user \u2014 there is none. (ADR-0049, ADR-0073 D5, #1888, #3760)`,
7004
+ rule: FLOW_RUNAS_UNSCOPED,
7005
+ severity: "error"
7006
+ });
7007
+ }
7008
+ }
7009
+ for (const node of nodes) {
7010
+ const nodeWhere = `flow '${flowName}' \xB7 node '${node.id}' (${node.type})`;
7011
+ const cfg = node.config ?? {};
7012
+ if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
7013
+ for (const key of Object.keys(cfg)) {
7014
+ if (PHANTOM_AGG_KEYS.has(key)) {
7015
+ findings.push({
7016
+ where: nodeWhere,
7017
+ message: `node config has \`${key}\` \u2014 the automation engine has no aggregate node, so \`${key}\` is silently ignored and this node computes nothing at runtime.`,
7018
+ hint: `Aggregation belongs in the data layer: use \`Field.summary\` for a cross-object rollup (sum/count of children), or \`Field.formula\` for a per-record computed value. (#1870)`,
7019
+ rule: FLOW_PHANTOM_AGGREGATION
7020
+ });
7021
+ }
7022
+ }
7023
+ const strings = [];
7024
+ collectTemplateStrings(node.config, void 0, strings);
7025
+ for (const str2 of strings) {
7026
+ if (DOUBLE_BRACE.test(str2)) {
7027
+ findings.push({
7028
+ where: nodeWhere,
7029
+ message: `double-brace interpolation \`${str2.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
7030
+ hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
7031
+ rule: FLOW_DOUBLE_BRACE_INTERP
7032
+ });
7033
+ }
7034
+ if (BARE_DOLLAR_REF.test(str2)) {
7035
+ findings.push({
7036
+ where: nodeWhere,
7037
+ message: `\`${str2.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
7038
+ hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
7039
+ rule: FLOW_BARE_DOLLAR_REF
7040
+ });
7041
+ }
7042
+ }
7043
+ }
7044
+ scanApprovalReviseLoops(flowName, nodes, edges, findings);
7045
+ scanErrorLabelledEdges(flowName, nodes, edges, findings);
7046
+ scanBranchRouting(flowName, nodes, edges, findings);
7047
+ }
7048
+ return findings;
7049
+ }
7050
+
7051
+ // src/lint-liveness-properties.ts
7052
+ var import_node_module4 = require("module");
7053
+ var import_node_path = require("path");
7054
+ var import_node_fs = require("fs");
7055
+ var import_meta4 = {};
7056
+ var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
7057
+ var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
7058
+ function asArray39(v) {
7059
+ if (Array.isArray(v)) return v;
7060
+ if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7061
+ return [];
7062
+ }
7063
+ function resolveLivenessDir() {
7064
+ try {
7065
+ const require2 = (0, import_node_module4.createRequire)(import_meta4.url);
7066
+ const pkgJson = require2.resolve("@objectstack/spec/package.json");
7067
+ const dir = (0, import_node_path.join)((0, import_node_path.dirname)(pkgJson), "liveness");
7068
+ return (0, import_node_fs.existsSync)(dir) ? dir : null;
7069
+ } catch {
7070
+ return null;
7071
+ }
7072
+ }
7073
+ function loadWarnMap(dir, type) {
7074
+ const map = /* @__PURE__ */ new Map();
7075
+ const file = (0, import_node_path.join)(dir, `${type}.json`);
7076
+ if (!(0, import_node_fs.existsSync)(file)) return map;
7077
+ let ledger;
7078
+ try {
7079
+ ledger = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
7080
+ } catch {
7081
+ return map;
7082
+ }
7083
+ const props = ledger.props || {};
7084
+ for (const [key, entry] of Object.entries(props)) {
7085
+ if (entry?.children) {
7086
+ for (const [ck, centry] of Object.entries(entry.children)) {
7087
+ if (shouldWarn(centry)) map.set(`${key}.${ck}`, centry);
7088
+ }
7089
+ }
7090
+ if (shouldWarn(entry)) map.set(key, entry);
7091
+ }
7092
+ return map;
7093
+ }
7094
+ function shouldWarn(entry) {
7095
+ if (!entry) return false;
7096
+ return entry.authorWarn === true || entry.status === "experimental";
7097
+ }
7098
+ function isAuthored(value) {
7099
+ if (value === void 0 || value === null) return false;
7100
+ if (typeof value === "boolean") return value === true;
7101
+ return true;
7102
+ }
7103
+ function describe(entry) {
7104
+ if (entry.status === "experimental") {
7105
+ return { kind: "is experimental \u2014 declared but NOT enforced at runtime", rule: LIVENESS_EXPERIMENTAL_PROPERTY };
7106
+ }
7107
+ return { kind: "has no runtime effect (liveness: dead)", rule: LIVENESS_DEAD_PROPERTY };
7108
+ }
7109
+ function checkItem(type, item, whereBase, warnMap, findings) {
7110
+ for (const [path, entry] of warnMap) {
7111
+ const values = path.includes(".") ? getNested(item, path) : [item[path]];
7112
+ for (const value of values instanceof Array ? values : [values]) {
7113
+ if (!isAuthored(value)) continue;
7114
+ const { kind, rule } = describe(entry);
7115
+ const hint = entry.authorHint ?? entry.note ?? "Remove it \u2014 it is declared in the spec but not consumed at runtime.";
7116
+ findings.push({
7117
+ where: whereBase,
7118
+ message: `sets \`${path}\` but this ${type} property ${kind}.`,
7119
+ hint,
7120
+ rule
7121
+ });
7122
+ break;
7123
+ }
7124
+ }
7125
+ }
7126
+ function getNested(obj, path) {
7127
+ let cur = [obj];
7128
+ for (const seg of path.split(".")) {
7129
+ const next = [];
7130
+ for (const c of cur) {
7131
+ if (c === null || typeof c !== "object") continue;
7132
+ const v = Array.isArray(c) ? void 0 : c[seg];
7133
+ if (Array.isArray(c)) {
7134
+ for (const el of c) {
7135
+ if (el && typeof el === "object") next.push(el[seg]);
7136
+ }
7137
+ } else {
7138
+ next.push(v);
7139
+ }
7140
+ }
7141
+ cur = next;
7142
+ }
7143
+ return cur.flatMap((v) => Array.isArray(v) ? v : [v]);
7144
+ }
7145
+ var TYPE_COLLECTIONS = [
7146
+ { type: "flow", key: "flows" },
7147
+ { type: "action", key: "actions" },
7148
+ { type: "agent", key: "agents" },
7149
+ { type: "tool", key: "tools" },
7150
+ { type: "skill", key: "skills" },
7151
+ { type: "dataset", key: "datasets" },
7152
+ { type: "permission", key: "permissions" },
7153
+ { type: "hook", key: "hooks" },
7154
+ { type: "page", key: "pages" },
7155
+ { type: "view", key: "views" },
7156
+ { type: "webhook", key: "webhooks" },
7157
+ // #4487. Note what adding a TYPE costs versus adding a warned property: the
7158
+ // doc below is right that coverage grows by marking entries `authorWarn` —
7159
+ // but only WITHIN a type already listed here. A newly governed type needs its
7160
+ // collection registered or its ledger warns nobody, which would leave the
7161
+ // ledger correct and silent: the exact shape this lint exists to prevent.
7162
+ { type: "datasource", key: "datasources" },
7163
+ // #4488 — the six newly governed types that carry `authorWarn` entries.
7164
+ // (doc / seed / validation are governed too but warn on nothing today, so
7165
+ // they are not listed; add them here the day one of their entries warns.)
7166
+ { type: "app", key: "apps" },
7167
+ { type: "book", key: "books" },
7168
+ { type: "job", key: "jobs" },
7169
+ { type: "email_template", key: "emailTemplates" },
7170
+ { type: "mapping", key: "mappings" },
7171
+ { type: "translation", key: "translations" }
7172
+ ];
7173
+ function lintLivenessProperties(stack) {
7174
+ const dir = resolveLivenessDir();
7175
+ if (!dir) return [];
7176
+ const findings = [];
7177
+ const objectWarn = loadWarnMap(dir, "object");
7178
+ const fieldWarn = loadWarnMap(dir, "field");
7179
+ for (const obj of asArray39(stack.objects)) {
7180
+ const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7181
+ if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
7182
+ if (fieldWarn.size > 0) {
7183
+ for (const field of asArray39(obj.fields)) {
7184
+ const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
7185
+ checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
7186
+ }
7187
+ }
7188
+ }
7189
+ for (const { type, key } of TYPE_COLLECTIONS) {
7190
+ const warnMap = loadWarnMap(dir, type);
7191
+ if (warnMap.size === 0) continue;
7192
+ for (const item of asArray39(stack[key])) {
7193
+ const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
7194
+ checkItem(type, item, `${type} '${name}'`, warnMap, findings);
7195
+ }
7196
+ }
7197
+ return findings;
7198
+ }
7199
+
7200
+ // src/lint-autonumber-formats.ts
7201
+ var import_data7 = require("@objectstack/spec/data");
7202
+ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7203
+ var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7204
+ var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
7205
+ var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
7206
+ function asArray40(v) {
7207
+ if (Array.isArray(v)) return v;
7208
+ if (v && typeof v === "object") {
7209
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7210
+ }
7211
+ return [];
7212
+ }
7213
+ function lintAutonumberFormats(stack) {
7214
+ const findings = [];
7215
+ for (const obj of asArray40(stack.objects)) {
7216
+ const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7217
+ const fields = asArray40(obj.fields);
7218
+ const fieldMeta = /* @__PURE__ */ new Map();
7219
+ for (const f of fields) {
7220
+ if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
7221
+ }
7222
+ for (const f of fields) {
7223
+ if (f.type !== "autonumber") continue;
7224
+ const name = typeof f.name === "string" ? f.name : "(unnamed field)";
7225
+ const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
7226
+ if (!fmt) continue;
7227
+ const tokens = (0, import_data7.parseAutonumberFormat)(fmt);
7228
+ const refs = (0, import_data7.referencedFields)(tokens);
7229
+ const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
7230
+ for (const t of tokens) {
7231
+ if (t.kind !== "literal") continue;
7232
+ const braced = t.text.match(/\{[^{}]*\}/g);
7233
+ if (!braced) continue;
7234
+ for (const tok of braced) {
7235
+ const body = tok.slice(1, -1);
7236
+ const isExtraSeq = /^0+$/.test(body);
7237
+ findings.push({
7238
+ where,
7239
+ message: isExtraSeq ? `format has a second sequence slot \`${tok}\` \u2014 only the first \`{0..0}\` counts; this one renders literally as "${tok}".` : `format has an unrecognized token \`${tok}\` \u2014 it is not a counter/date/{field} token, so it renders literally as "${tok}" in every record number.`,
7240
+ hint: isExtraSeq ? `Use a single \`{0000}\` slot; fold any second number into a literal or a {field} token.` : `Date tokens are case-sensitive and exact: {YYYY} {YY} {MM} {DD} {YYYYMMDD} (no spaces/punctuation inside). For a field value use {field_name}.`,
7241
+ rule: AUTONUMBER_LITERAL_TOKEN,
7242
+ severity: "warning"
7243
+ });
7244
+ }
7245
+ }
7246
+ for (const ref of refs) {
7247
+ if (ref === name) {
7248
+ findings.push({
7249
+ where,
7250
+ message: `format interpolates \`{${ref}}\` \u2014 its own value, which does not exist yet when the number is generated.`,
7251
+ hint: `Reference a DIFFERENT field that is set before create (e.g. \`{plan_no}{000}\`), or drop the token.`,
7252
+ rule: AUTONUMBER_SELF_REFERENCE,
7253
+ severity: "error"
7254
+ });
7255
+ continue;
7256
+ }
7257
+ const meta = fieldMeta.get(ref);
7258
+ if (!meta) {
7259
+ findings.push({
7260
+ where,
7261
+ message: `format interpolates \`{${ref}}\`, but object '${objectName}' has no field named '${ref}' \u2014 generation will always throw.`,
7262
+ hint: `Reference an existing field, or remove the \`{${ref}}\` token from the format.`,
7263
+ rule: AUTONUMBER_UNKNOWN_FIELD,
7264
+ severity: "error"
7265
+ });
7266
+ } else if (!meta.required) {
7267
+ findings.push({
7268
+ where,
7269
+ message: `format interpolates \`{${ref}}\`, but '${ref}' is optional \u2014 any record left blank fails autonumber generation at create time.`,
7270
+ hint: `Mark '${ref}' as \`required: true\` so it is always set before the record number is rendered.`,
7271
+ rule: AUTONUMBER_OPTIONAL_FIELD,
7272
+ severity: "warning"
7273
+ });
7274
+ }
7275
+ }
7276
+ }
7277
+ }
7278
+ return findings;
7279
+ }
7280
+
7281
+ // src/lint-view-refs.ts
7282
+ var import_spec2 = require("@objectstack/spec");
7283
+ function asArray41(v) {
7284
+ if (Array.isArray(v)) return v;
7285
+ if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7286
+ return [];
7287
+ }
7288
+ var VIEW_KEY_COLLISION = "view-key-collision";
7289
+ var VIEW_REF_FORM_TARGET_MISSING = "view-ref-form-target-missing";
7290
+ var VIEW_REF_FORM_TARGET_KIND = "view-ref-form-target-kind";
7291
+ function containerFromObject(obj) {
7292
+ return { list: obj.list, form: obj.form, listViews: obj.listViews, formViews: obj.formViews };
7293
+ }
7294
+ function viewContainerObjectName(item) {
7295
+ if (typeof item.name === "string" && item.name) return item.name;
7296
+ if (typeof item.id === "string" && item.id) return item.id;
7297
+ if (typeof item.object === "string" && item.object) return item.object;
7298
+ if (typeof item.list?.data?.object === "string") return item.list.data.object;
7299
+ if (typeof item.form?.data?.object === "string") return item.form.data.object;
7300
+ return void 0;
7301
+ }
7302
+ function lintViewRefs(stack) {
7303
+ const findings = [];
7304
+ const viewKinds = /* @__PURE__ */ new Map();
7305
+ const indexKind = (name, kind) => {
7306
+ let s = viewKinds.get(name);
7307
+ if (!s) viewKinds.set(name, s = /* @__PURE__ */ new Set());
7308
+ s.add(kind);
7309
+ };
7310
+ const containers = [];
7311
+ for (const v of asArray41(stack.views)) {
7312
+ if (v.viewKind) {
7313
+ if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7314
+ continue;
7315
+ }
7316
+ if (!(0, import_spec2.isAggregatedViewContainer)(v)) continue;
7317
+ const object = viewContainerObjectName(v);
7318
+ if (object) containers.push({ object, container: v });
7319
+ }
7320
+ for (const obj of asArray41(stack.objects)) {
7321
+ const object = typeof obj.name === "string" ? obj.name : void 0;
7322
+ if (!object) continue;
7323
+ if (obj.list || obj.form || obj.listViews || obj.formViews) {
7324
+ containers.push({ object, container: containerFromObject(obj) });
7325
+ }
7326
+ }
7327
+ for (const { object, container } of containers) {
7328
+ const { items, collisions } = (0, import_spec2.expandViewContainerWithDiagnostics)(object, container);
7329
+ for (const it of items) indexKind(it.name, it.viewKind);
7330
+ for (const col of collisions) {
7331
+ findings.push({
7332
+ where: `object '${object}' \xB7 view key '${col.key}'`,
7333
+ message: `View key collision: the ${col.viewKind} view '${col.requested}' clashes with another view in the same container and was renamed to '${col.renamedTo}'. Anything referencing '${col.requested}' (a form action target, a navigation viewName) resolves to the OTHER view, not this one.`,
7334
+ hint: `Give the ${col.viewKind} view a unique key \u2014 the default list implicitly claims '<object>.default'. Renaming key '${col.key}' fixes both this collision and any reference that targets it.`,
7335
+ rule: VIEW_KEY_COLLISION,
7336
+ severity: "warning"
7337
+ });
7338
+ }
7339
+ }
7340
+ const seenFormTargets = /* @__PURE__ */ new Set();
7341
+ const checkAction = (action, ownerObject) => {
7342
+ if (!action || action.type !== "form") return;
7343
+ const target = action.target;
7344
+ if (typeof target !== "string" || !target) return;
7345
+ if (target.includes("${")) return;
7346
+ if (!target.includes(".")) return;
7347
+ const actionName = typeof action.name === "string" ? action.name : "(unnamed)";
7348
+ const dedupeKey = `${actionName}\0${target}`;
7349
+ if (seenFormTargets.has(dedupeKey)) return;
7350
+ seenFormTargets.add(dedupeKey);
7351
+ const where = ownerObject ? `action '${actionName}' on object '${ownerObject}'` : `action '${actionName}'`;
7352
+ const kinds = viewKinds.get(target);
7353
+ if (!kinds) {
7354
+ findings.push({
7355
+ where,
7356
+ message: `Form action target '${target}' does not resolve to any view. A type:'form' action must point at an existing form view; at runtime this opens a blank/broken form.`,
7357
+ hint: `Check for a typo, or a form view renamed by a key collision. Expected a form view named '<object>.<formViewKey>'.`,
7358
+ rule: VIEW_REF_FORM_TARGET_MISSING,
7359
+ severity: "warning"
7360
+ });
7361
+ return;
7362
+ }
7363
+ if (!kinds.has("form")) {
7364
+ const actual = [...kinds].join("/");
7365
+ findings.push({
7366
+ where,
7367
+ message: `Form action target '${target}' resolves to a ${actual} view, not a form view. Opening a ${actual} view through a form action renders an empty form (and a submit can silently no-op) at runtime.`,
7368
+ hint: `Point target at a form view (viewKind 'form'). If the form view was renamed by a key collision, fix the colliding key so '${target}' names the form again.`,
7369
+ rule: VIEW_REF_FORM_TARGET_KIND,
7370
+ severity: "error"
7371
+ });
7372
+ }
7373
+ };
7374
+ for (const obj of asArray41(stack.objects)) {
7375
+ const object = typeof obj.name === "string" ? obj.name : void 0;
7376
+ for (const action of asArray41(obj.actions)) checkAction(action, object);
7377
+ }
7378
+ for (const action of asArray41(stack.actions)) checkAction(action);
7379
+ return findings;
7380
+ }
7381
+
7382
+ // src/data-model-rules.ts
7383
+ var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
7384
+ var NUMERIC_TYPES = /* @__PURE__ */ new Set([
7385
+ "number",
7386
+ "currency",
7387
+ "integer",
7388
+ "decimal",
7389
+ "percent",
7390
+ "float",
7391
+ "double"
7392
+ ]);
7393
+ var OPTION_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multiselect", "radio", "enum"]);
7394
+ var NAME_LIKE_FIELDS = ["name", "title", "subject", "label", "full_name", "display_name", "code"];
7395
+ var LINE_ITEM_RE = /_(line|lines|line_item|line_items|item|items|detail|details|entry|entries)$/;
7396
+ var ASSOCIATION_TOKENS = [
7397
+ "comment",
7398
+ "attachment",
7399
+ "note",
7400
+ "log",
7401
+ "audit",
7402
+ "activity",
7403
+ "activities",
7404
+ "history",
7405
+ "event",
7406
+ "reaction",
7407
+ "like",
7408
+ "mention",
7409
+ "notification",
7410
+ "message"
7411
+ ];
7412
+ function isLineItemName(name) {
7413
+ return LINE_ITEM_RE.test(name);
7414
+ }
7415
+ function isAssociationName(name) {
7416
+ const lc = name.toLowerCase();
7417
+ return ASSOCIATION_TOKENS.some((t) => lc === t || lc.endsWith(`_${t}`) || lc.endsWith(`_${t}s`));
7418
+ }
7419
+ function fieldEntries2(fields) {
7420
+ if (!fields) return [];
7421
+ if (Array.isArray(fields)) {
7422
+ return fields.filter((f) => f && f.name != null).map((f) => ({ name: String(f.name), def: f }));
7423
+ }
7424
+ return Object.entries(fields).map(([name, def]) => ({ name, def }));
7425
+ }
7426
+ function refOf2(def) {
7427
+ return def?.reference || def?.reference_to;
7428
+ }
7429
+ var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
7430
+ function uniqueDeclared(u) {
7431
+ return u === true || u === "global";
7432
+ }
7433
+ function lintUniqueDeclarations(objects) {
7434
+ const issues = [];
7435
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
7436
+ for (let i = 0; i < objects.length; i++) {
7437
+ const obj = objects[i];
7438
+ if (!obj?.name) continue;
7439
+ const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
7440
+ if (declaredIndexes.length === 0) continue;
7441
+ const singleColumnUniqueIndexes = /* @__PURE__ */ new Map();
7442
+ for (const idx of declaredIndexes) {
7443
+ if (!uniqueDeclared(idx?.unique)) continue;
7444
+ const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
7445
+ if (cols.length !== 1) continue;
7446
+ if (!singleColumnUniqueIndexes.has(cols[0])) singleColumnUniqueIndexes.set(cols[0], idx);
7447
+ }
7448
+ if (singleColumnUniqueIndexes.size === 0) continue;
7449
+ for (const { name, def } of fieldEntries2(obj.fields)) {
7450
+ if (!uniqueDeclared(def?.unique)) continue;
7451
+ if (def.unique === "global") continue;
7452
+ const idx = singleColumnUniqueIndexes.get(name);
7453
+ if (!idx) continue;
7454
+ const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
7455
+ issues.push({
7456
+ severity: "warning",
7457
+ rule: UNIQUE_DOUBLE_DECLARATION,
7458
+ message: `"${obj.name}.${name}" declares field-level \`unique: true\` AND a single-column unique index${indexLabel} on the same column. Since #3696 the field-level form is scoped per tenant \u2014 \`(tenant, ${name})\` \u2014 while a declared index is materialized over exactly its \`fields\`, i.e. platform-wide. On a tenant-scoped object the global index wins and the per-tenant constraint can never be reached; on a tenancy-less object the two are the same index declared twice. Either way one of the two declarations has no effect.`,
7459
+ path: `objects[${i}]`,
7460
+ fix: `Pick the intent: for platform-wide uniqueness set \`unique: 'global'\` on '${name}' and drop the duplicate index; for per-tenant uniqueness drop the index (the field-level declaration already builds the tenant composite), or spell the index out as \`fields: ['organization_id', '${name}']\` if you want it explicit.`
7461
+ });
7462
+ }
7463
+ }
7464
+ return issues;
7465
+ }
7466
+ function lintDataModel(objects) {
7467
+ const issues = lintUniqueDeclarations(objects);
7468
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
7469
+ const childrenByParent = {};
7470
+ for (const child of objects) {
7471
+ if (!child?.name) continue;
7472
+ for (const { name: fieldName, def } of fieldEntries2(child.fields)) {
7473
+ if (!RELATIONSHIP_TYPES.has(def?.type)) continue;
7474
+ const parent = refOf2(def);
7475
+ if (!parent) continue;
7476
+ (childrenByParent[parent] || (childrenByParent[parent] = [])).push({ child, fieldName, def });
7477
+ }
7478
+ }
7479
+ for (let i = 0; i < objects.length; i++) {
7480
+ const obj = objects[i];
7481
+ if (!obj?.name) continue;
7482
+ const objPath = `objects[${i}]`;
7483
+ const fields = fieldEntries2(obj.fields);
7484
+ const hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
7485
+ if (fields.length > 0 && !hasNameField) {
7486
+ issues.push({
7487
+ severity: "suggestion",
7488
+ rule: "object/missing-name-field",
7489
+ message: `Object "${obj.name}" has no name/title field or primaryField \u2014 records will display as raw IDs`,
7490
+ path: `${objPath}.fields`
7491
+ });
7492
+ }
7493
+ for (const { name: fieldName, def } of fields) {
7494
+ if (!def || typeof def !== "object") continue;
7495
+ const fieldPath = `${objPath}.fields.${fieldName}`;
7496
+ const type = def.type;
7497
+ if (OPTION_FIELD_TYPES.has(type)) {
7498
+ const hasOptions = Array.isArray(def.options) && def.options.length > 0 || !!def.optionsFrom || !!def.dataSource || !!def.reference;
7499
+ if (!hasOptions) {
7500
+ issues.push({
7501
+ severity: "warning",
7502
+ rule: "field/select-missing-options",
7503
+ message: `${type} field "${obj.name}.${fieldName}" has no options`,
7504
+ path: `${fieldPath}.options`
7505
+ });
7506
+ }
7507
+ }
7508
+ if (!RELATIONSHIP_TYPES.has(type)) continue;
7509
+ const parent = refOf2(def);
7510
+ if (!parent) {
7511
+ issues.push({
7512
+ severity: "error",
7513
+ rule: "relationship/missing-reference",
7514
+ message: `${type} field "${obj.name}.${fieldName}" is missing a reference target`,
7515
+ path: `${fieldPath}.reference`
7516
+ });
7517
+ continue;
7518
+ }
7519
+ if (type === "master_detail") {
7520
+ if (def.required !== true) {
7521
+ issues.push({
7522
+ severity: "warning",
7523
+ rule: "relationship/master-detail-required",
7524
+ message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should be required (a detail record cannot exist without its master)`,
7525
+ path: `${fieldPath}.required`,
7526
+ fix: "required: true"
7527
+ });
7528
+ }
7529
+ if (def.deleteBehavior === void 0) {
7530
+ issues.push({
7531
+ severity: "suggestion",
7532
+ rule: "relationship/delete-behavior",
7533
+ message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should declare deleteBehavior (cascade/restrict/set_null)`,
7534
+ path: `${fieldPath}.deleteBehavior`,
7535
+ fix: "deleteBehavior: 'cascade'"
7536
+ });
7537
+ }
7538
+ if (isLineItemName(obj.name) && def.inlineEdit !== true) {
7539
+ issues.push({
7540
+ severity: "suggestion",
7541
+ rule: "relationship/line-items-inline-edit",
7542
+ message: `"${obj.name}" looks like line items of ${parent}; consider inlineEdit: true on "${fieldName}" so it is entered inline within the ${parent} form`,
7543
+ path: `${fieldPath}.inlineEdit`,
7544
+ fix: "inlineEdit: true"
7545
+ });
7546
+ }
7547
+ }
7548
+ if (type === "lookup" && isLineItemName(obj.name)) {
7549
+ issues.push({
7550
+ severity: "suggestion",
7551
+ rule: "relationship/line-item-should-be-master-detail",
7552
+ message: `"${obj.name}" looks like line items of ${parent} but uses lookup; master_detail gives ownership + cascade + roll-ups`,
7553
+ path: `${fieldPath}.type`,
7554
+ fix: "type: 'master_detail'"
7555
+ });
7556
+ }
7557
+ if (def.inlineEdit === true && isAssociationName(obj.name)) {
7558
+ issues.push({
7559
+ severity: "warning",
7560
+ rule: "relationship/association-inline-edit",
7561
+ message: `"${obj.name}" is an association (comments/audit/activity), not line items \u2014 inlineEdit clutters the ${parent} entry form; surface it as a detail-page related list instead`,
7562
+ path: `${fieldPath}.inlineEdit`,
7563
+ fix: "remove inlineEdit (use relatedList on the detail page)"
7564
+ });
7565
+ }
7566
+ }
7567
+ const children = childrenByParent[obj.name] || [];
7568
+ const summaryChildObjects = new Set(
7569
+ fields.filter((f) => f.def?.type === "summary").map((f) => f.def?.summaryOperations?.object || f.def?.reference).filter(Boolean)
7570
+ );
7571
+ const seenSuggestedChild = /* @__PURE__ */ new Set();
7572
+ for (const { child, def } of children) {
7573
+ if (def?.type !== "master_detail") continue;
7574
+ if (!child?.name || seenSuggestedChild.has(child.name)) continue;
7575
+ if (summaryChildObjects.has(child.name)) continue;
7576
+ const numericChildField = fieldEntries2(child.fields).find((f) => NUMERIC_TYPES.has(f.def?.type));
7577
+ if (!numericChildField) continue;
7578
+ seenSuggestedChild.add(child.name);
7579
+ issues.push({
7580
+ severity: "suggestion",
7581
+ rule: "rollup/missing-summary",
7582
+ message: `"${obj.name}" owns "${child.name}" (master_detail) with numeric field "${numericChildField.name}" but has no roll-up summary; consider a summary field (count/sum) on ${obj.name}`,
7583
+ path: `${objPath}.fields`,
7584
+ fix: `summary field aggregating ${child.name}.${numericChildField.name}`
7585
+ });
7586
+ }
7587
+ }
7588
+ return issues;
7589
+ }
7590
+
7591
+ // src/authoring-rules.ts
7592
+ var AUTHORING_COMMANDS = ["validate", "build", "lint"];
7593
+ var AUTHORING_SURFACES = ["cli", "runtime-publish"];
7594
+ var ALL = AUTHORING_COMMANDS;
7595
+ var CLI_ONLY = ["cli"];
7596
+ var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
7597
+ var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
7598
+ var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
7599
+ var RUNTIME_OBJECT_WRITES_P2 = "P2 (#4463): judges an object/field declaration. Object writes are the hottest metadata path in the product, so P1 gates `flow` first and widens once the gate has real traffic behind it.";
7600
+ var EXPRESSION_INVALID = "expression-invalid";
7601
+ var AUTHORING_RULES = [
7602
+ // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
7603
+ // are parsed for syntax AND checked that each `record.<field>` resolves. This
7604
+ // is what catches a BARE field ref (`done` instead of `record.done`) that
7605
+ // would otherwise silently hide an action on every record (#2183/#2185).
7606
+ {
7607
+ name: "validateStackExpressions",
7608
+ tier: "gating",
7609
+ input: "parsed",
7610
+ commands: ALL,
7611
+ source: "packages/lint/src/validate-expressions.ts",
7612
+ // Runtime publish gate (#4463): the EXPRESSION family. On a flow write it
7613
+ // checks every script-node callable and every declared predicate the flow
7614
+ // carries, against the live object universe — the same parse `os build`
7615
+ // runs, now at the door Studio/REST/MCP authors actually use.
7616
+ surfaces: CLI_AND_RUNTIME,
7617
+ runtimeTypes: ["flow"],
7618
+ run: (stack) => validateStackExpressions(stack).map((i) => ({
7619
+ severity: i.severity ?? "error",
7620
+ rule: EXPRESSION_INVALID,
7621
+ where: i.where,
7622
+ path: i.where,
7623
+ message: i.message,
7624
+ hint: `source: \`${i.source}\``
7625
+ }))
7626
+ },
7627
+ // ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
7628
+ // mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
7629
+ // read the pre-parse tier or the evidence is already gone.
7630
+ {
7631
+ name: "validateListViewMode",
7632
+ tier: "gating",
7633
+ input: "normalized",
7634
+ commands: ALL,
7635
+ source: "packages/lint/src/validate-list-view-mode.ts",
7636
+ surfaces: CLI_ONLY,
7637
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7638
+ run: (stack) => validateListViewMode(stack)
7639
+ },
7640
+ // [ADR-0078] A Zod-VALID instance that silently does nothing: a `summary`
7641
+ // with no `summaryOperations`, a `lookup` with no `reference`, a `select`
7642
+ // with no `options`. Every key is one we know, so #4001's unknown-key
7643
+ // rejection cannot see it, and the liveness ledger cannot either (it is
7644
+ // per-property; the properties ARE live). This is the gate between them.
7645
+ //
7646
+ // `gating` because the error-severity shapes are fully inert — the field
7647
+ // reads 0 forever while authoring reports success, which is the failure the
7648
+ // ADR was written for (cloud#687). Pre-parse so the findings survive an
7649
+ // unrelated schema error elsewhere in the stack.
7650
+ {
7651
+ name: "validateFunctionalCompleteness",
7652
+ tier: "gating",
7653
+ input: "normalized",
7654
+ commands: ALL,
7655
+ source: "packages/lint/src/validate-functional-completeness.ts",
7656
+ surfaces: CLI_ONLY,
7657
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7658
+ run: (stack) => validateFunctionalCompleteness(stack)
7659
+ },
7660
+ // A flat list-view object in `views: []` parses to an EMPTY container
7661
+ // (ViewSchema strips unknown keys): the schema step passes, zero views
7662
+ // register, and the Console renders nothing. Pre-parse for the same reason.
7663
+ {
7664
+ name: "validateViewContainers",
7665
+ tier: "gating",
7666
+ input: "normalized",
7667
+ commands: ALL,
7668
+ source: "packages/lint/src/validate-view-containers.ts",
7669
+ surfaces: CLI_ONLY,
7670
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7671
+ run: (stack) => validateViewContainers(stack)
7672
+ },
7673
+ // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
7674
+ // chartConfig axis/series must resolve against the declared datasets.
7675
+ {
7676
+ name: "validateWidgetBindings",
7677
+ tier: "gating",
7678
+ input: "parsed",
7679
+ commands: ALL,
7680
+ source: "packages/lint/src/validate-widget-bindings.ts",
7681
+ surfaces: CLI_ONLY,
7682
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7683
+ run: (stack) => validateWidgetBindings(stack)
7684
+ },
7685
+ // ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
7686
+ // target that resolves to no defined action ships a button that renders and
7687
+ // silently does nothing on click. Unresolved `url` routes stay advisory.
7688
+ {
7689
+ name: "validateDashboardActionRefs",
7690
+ tier: "gating",
7691
+ input: "parsed",
7692
+ commands: ALL,
7693
+ source: "packages/lint/src/validate-dashboard-action-refs.ts",
7694
+ surfaces: CLI_ONLY,
7695
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7696
+ run: (stack) => validateDashboardActionRefs(stack)
7697
+ },
7698
+ // #3574 — a filter value like `{current_user}` resolves in no vocabulary,
7699
+ // reaches the data engine as a literal and matches nothing. The surface
7700
+ // renders empty with no error, and a silent zero is indistinguishable from a
7701
+ // genuine one at review time.
7702
+ {
7703
+ name: "validateFilterTokens",
7704
+ tier: "gating",
7705
+ input: "parsed",
7706
+ commands: ALL,
7707
+ source: "packages/lint/src/validate-filter-tokens.ts",
7708
+ surfaces: CLI_ONLY,
7709
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7710
+ run: (stack) => validateFilterTokens(stack)
7711
+ },
7712
+ // The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
7713
+ // rules that answer "does this name resolve to anything?". It reached all
7714
+ // three commands before this file existed; it is an entry here so the two
7715
+ // registries compose instead of competing, and so its members are covered by
7716
+ // the same guard as everything else.
7717
+ {
7718
+ name: "validateReferenceIntegrity",
7719
+ tier: "gating",
7720
+ input: "parsed",
7721
+ commands: ALL,
7722
+ source: "packages/lint/src/reference-integrity-suite.ts",
7723
+ // Runtime publish gate (#4463): the REFERENCE family. On a flow snapshot the
7724
+ // members that answer a flow question run (`validateFlowTemplatePaths`,
7725
+ // `validateFlowNodeWrites`, `validateReadonlyFlowWrites`,
7726
+ // `validateObjectReferences`); the page/nav/AI members see no such
7727
+ // collection in the snapshot and return nothing, and the two that would
7728
+ // load `typescript` need a hook/action/react body the snapshot never
7729
+ // carries — which is what `runtime-lazy-deps.test.ts` pins.
7730
+ surfaces: CLI_AND_RUNTIME,
7731
+ runtimeTypes: ["flow"],
7732
+ run: (stack) => validateReferenceIntegrity(stack)
7733
+ },
7734
+ // ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
7735
+ // `id`), name real CSS properties + design tokens, and carry a `large` base.
7736
+ {
7737
+ name: "validateResponsiveStyles",
7738
+ tier: "gating",
7739
+ input: "parsed",
7740
+ commands: ALL,
7741
+ source: "packages/lint/src/validate-responsive-styles.ts",
7742
+ surfaces: CLI_ONLY,
7743
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7744
+ run: (stack) => validateResponsiveStyles(stack)
7745
+ },
7746
+ // ADR-0080 — a `kind:'jsx'` page's `source` is parsed (never executed) and
7747
+ // compiled to the SDUI tree at save time, so malformed source must fail loudly
7748
+ // here (ADR-0078) instead of being stored and breaking only at render.
7749
+ {
7750
+ name: "validateJsxPages",
7751
+ tier: "gating",
7752
+ input: "parsed",
7753
+ commands: ALL,
7754
+ source: "packages/lint/src/validate-jsx-pages.ts",
7755
+ surfaces: CLI_ONLY,
7756
+ surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
7757
+ run: (stack, ctx) => validateJsxPages(stack, ctx.sduiManifest ? { manifest: ctx.sduiManifest } : {})
7758
+ },
7759
+ // ADR-0081 — a `kind:'react'` page's `source` is real React executed at
7760
+ // render. Transpiled here (Sucrase, never executed) so a syntax error fails at
7761
+ // author time, not at render. Lazy: only a stack with such a page pays.
7762
+ {
7763
+ name: "validateReactPages",
7764
+ tier: "gating",
7765
+ input: "parsed",
7766
+ commands: ALL,
7767
+ source: "packages/lint/src/validate-react-pages.ts",
7768
+ surfaces: CLI_ONLY,
7769
+ surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
7770
+ run: (stack) => validateReactPages(stack)
7771
+ },
7772
+ // ADR-0065, source tier — Tailwind `className` in a `kind:'html'`/`'react'`
7773
+ // page silently no-ops (the build never scans authored metadata).
7774
+ {
7775
+ name: "validatePageSourceStyling",
7776
+ tier: "advisory",
7777
+ input: "parsed",
7778
+ commands: ALL,
7779
+ source: "packages/lint/src/validate-page-source-styling.ts",
7780
+ surfaces: CLI_ONLY,
7781
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7782
+ run: (stack) => validatePageSourceStyling(stack)
7783
+ },
7784
+ // ADR-0066 ⑨ — a `requiredPermissions` entry naming a capability registered
7785
+ // nowhere fails closed at runtime. Advisory: another installed package may
7786
+ // legitimately provide it.
7787
+ {
7788
+ name: "validateCapabilityReferences",
7789
+ tier: "advisory",
7790
+ input: "parsed",
7791
+ commands: ALL,
7792
+ source: "packages/lint/src/validate-capability-references.ts",
7793
+ surfaces: CLI_ONLY,
7794
+ surfaceReason: 'P2 (#4463): the ONE rule the runtime universe makes strictly stronger \u2014 the advisory hedge ("another installed package may provide it") is decidable against the live capability registry, so it graduates from advisory to gating there rather than merely being ported. That promotion is a severity change on a published rule id and belongs in its own PR, not riding a wiring change.',
7795
+ run: (stack) => validateCapabilityReferences(stack)
7796
+ },
7797
+ // A record-change flow whose start-node objectName matches nothing never
7798
+ // fires — silently. Reads the pre-parse tier so an author sees what they
7799
+ // wrote. Advisory: the object may come from another installed package.
7800
+ {
7801
+ name: "validateFlowTriggerReadiness",
7802
+ tier: "advisory",
7803
+ input: "normalized",
7804
+ commands: ALL,
7805
+ source: "packages/lint/src/validate-flow-trigger-readiness.ts",
7806
+ // Runtime publish gate (#4463): the FLOW family. Advisory at this surface
7807
+ // too — its findings are logged, not thrown (P1 gates on `error` only; P2
7808
+ // puts advisories on the response for Studio to render).
7809
+ surfaces: CLI_AND_RUNTIME,
7810
+ runtimeTypes: ["flow"],
7811
+ run: (stack) => validateFlowTriggerReadiness(stack)
7812
+ },
7813
+ // ADR-0090 D3 fallout — an approval `{ type: 'role' }` resolves against the
7814
+ // better-auth org-membership tier, not positions, so a position name authored
7815
+ // there routes the approval to nobody; and an expression approver that does
7816
+ // not parse can never resolve. The rule whose absence from `os build` and
7817
+ // `os validate` was #4409's worked example: it gates, and it ran on `os lint`
7818
+ // alone, so a broken approval flow built and published green.
7819
+ {
7820
+ name: "validateApprovalApprovers",
7821
+ tier: "gating",
7822
+ input: "parsed",
7823
+ commands: ALL,
7824
+ source: "packages/lint/src/validate-approval-approvers.ts",
7825
+ // Runtime publish gate (#4463): the APPROVAL family, and the issue's worked
7826
+ // example both times. #4409 fixed it for `os build`; a tenant saving the
7827
+ // same broken expression approver from Studio still sailed through, because
7828
+ // `approver.value` is just a string to Zod. It is not just a string here.
7829
+ surfaces: CLI_AND_RUNTIME,
7830
+ runtimeTypes: ["flow"],
7831
+ run: (stack) => validateApprovalApprovers(stack)
7832
+ },
7833
+ // ADR-0079 — `titleFormat` is retired in favour of `nameField`, and an object
7834
+ // with no resolvable title ships records with no meaningful name. Advisory:
7835
+ // auto-provision and the `Record #<id>` floor keep it from ever being fatal.
7836
+ {
7837
+ name: "validateRecordTitle",
7838
+ tier: "advisory",
7839
+ input: "parsed",
7840
+ commands: ALL,
7841
+ source: "packages/lint/src/validate-record-title.ts",
7842
+ surfaces: CLI_ONLY,
7843
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7844
+ run: (stack) => validateRecordTitle(stack)
7845
+ },
7846
+ // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
7847
+ // into the object's field map; a dangling one is Zod-valid and silently inert
7848
+ // at render. Advisory: every consumer degrades gracefully.
7849
+ {
7850
+ name: "validateSemanticRoles",
7851
+ tier: "advisory",
7852
+ input: "parsed",
7853
+ commands: ALL,
7854
+ source: "packages/lint/src/validate-semantic-roles.ts",
7855
+ surfaces: CLI_ONLY,
7856
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7857
+ run: (stack) => validateSemanticRoles(stack)
7858
+ },
7859
+ // #2578 / #4449 — a form section's field reference that resolves to nothing
7860
+ // (silently not rendered) and an absolute `colSpan` under a per-surface
7861
+ // derived column count. Advisory: the renderer skips the unknown field and
7862
+ // clamps the span, so nothing is broken — but each is almost certainly an
7863
+ // authoring mistake, and until #4449 this rule ran on no command at all.
7864
+ // Pure structured-metadata walk (no lazy dependency), so wiring it to all
7865
+ // three costs nothing measurable.
7866
+ {
7867
+ name: "validateFormLayout",
7868
+ tier: "advisory",
7869
+ input: "parsed",
7870
+ commands: ALL,
7871
+ source: "packages/lint/src/validate-form-layout.ts",
7872
+ surfaces: CLI_ONLY,
7873
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7874
+ run: (stack) => validateFormLayout(stack)
7875
+ },
7876
+ // ADR-0078 Phase 3 (Tier-A `action-locations`) — an action that declares no
7877
+ // `locations` and that no view places by name renders on no surface at all.
7878
+ // objectui#3142 made that measurable: four renderers used to show an
7879
+ // undeclared action anyway, and now none does. Advisory: a view in another
7880
+ // installed package may be the one placing it, and `locations: []` (the
7881
+ // documented headless shape) is deliberately never flagged.
7882
+ {
7883
+ name: "validateActionLocations",
7884
+ tier: "advisory",
7885
+ input: "parsed",
7886
+ commands: ALL,
7887
+ source: "packages/lint/src/validate-action-locations.ts",
7888
+ surfaces: CLI_ONLY,
7889
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7890
+ run: (stack) => validateActionLocations(stack)
7891
+ },
7892
+ // framework#3434 — seeds replay on every boot, so a `mode: 'insert'` dataset
7893
+ // duplicates its table on every restart.
7894
+ {
7895
+ name: "validateSeedReplaySafety",
7896
+ tier: "advisory",
7897
+ input: "parsed",
7898
+ commands: ALL,
7899
+ source: "packages/lint/src/validate-seed-replay-safety.ts",
7900
+ surfaces: CLI_ONLY,
7901
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7902
+ run: (stack) => validateSeedReplaySafety(stack)
7903
+ },
7904
+ // framework#3433 follow-up — #3433 exempts seed writes from the
7905
+ // `state_machine` rule, so a seeded status the FSM does not declare is no
7906
+ // longer rejected at write time. Re-added at author time; advisory, because
7907
+ // the exemption itself is legitimate.
7908
+ {
7909
+ name: "validateSeedStateMachine",
7910
+ tier: "advisory",
7911
+ input: "parsed",
7912
+ commands: ALL,
7913
+ source: "packages/lint/src/validate-seed-state-machine.ts",
7914
+ surfaces: CLI_ONLY,
7915
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7916
+ run: (stack) => validateSeedStateMachine(stack)
7917
+ },
7918
+ // ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
7919
+ // Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
7920
+ // during parse, so the alias the author wrote is gone from `result.data`.
7921
+ {
7922
+ name: "validateVisibilityPredicates",
7923
+ tier: "advisory",
7924
+ input: "normalized",
7925
+ commands: ALL,
7926
+ source: "packages/lint/src/validate-visibility-predicates.ts",
7927
+ surfaces: CLI_ONLY,
7928
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7929
+ run: (stack) => validateVisibilityPredicates(stack)
7930
+ },
7931
+ // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
7932
+ // `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
7933
+ // runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
7934
+ // `flow-default-edge-with-condition` (#4414 — a declaration that is inert, so
7935
+ // the route silently differs from what the author wrote). The bar for
7936
+ // promoting one is stated at the top of `lint-flow-patterns.ts`.
7937
+ {
7938
+ name: "lintFlowPatterns",
7939
+ tier: "gating",
7940
+ input: "parsed",
7941
+ commands: ALL,
7942
+ source: "packages/lint/src/lint-flow-patterns.ts",
7943
+ // Runtime publish gate (#4463): the FLOW family's anti-pattern half. Its
7944
+ // three `error` rules are the sharpest fit for a publish gate that exists —
7945
+ // `flow-runas-unscoped` is metadata the automation engine REFUSES to
7946
+ // execute, so publishing it can only ever produce a broken flow.
7947
+ surfaces: CLI_AND_RUNTIME,
7948
+ runtimeTypes: ["flow"],
7949
+ run: (stack) => lintFlowPatterns(stack).map((f) => ({
7950
+ severity: f.severity ?? "warning",
7951
+ rule: f.rule,
7952
+ where: f.where,
7953
+ path: f.where,
7954
+ message: f.message,
7955
+ hint: f.hint
7956
+ }))
7957
+ },
7958
+ // The spec-liveness loop on the author side: a property the ledger marks
7959
+ // dead-and-misleading or experimental is set hopefully and does nothing.
7960
+ // Ledger-driven (entries opt in via `authorWarn`), so it is high-signal and
7961
+ // never fatal.
7962
+ {
7963
+ name: "lintLivenessProperties",
7964
+ tier: "advisory",
7965
+ input: "parsed",
7966
+ commands: ALL,
7967
+ source: "packages/lint/src/lint-liveness-properties.ts",
7968
+ surfaces: CLI_ONLY,
7969
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7970
+ run: (stack) => lintLivenessProperties(stack).map((f) => ({
7971
+ severity: "warning",
7972
+ rule: f.rule,
7973
+ where: f.where,
7974
+ path: f.where,
7975
+ message: f.message,
7976
+ hint: f.hint
7977
+ }))
7978
+ },
7979
+ // A format like `{plan_no}{000}` makes the referenced field part of the
7980
+ // counter scope, so it must exist and be set at create time. Unknown field →
7981
+ // broken (error); optional field → fragile (warning).
7982
+ {
7983
+ name: "lintAutonumberFormats",
7984
+ tier: "gating",
7985
+ input: "parsed",
7986
+ commands: ALL,
7987
+ source: "packages/lint/src/lint-autonumber-formats.ts",
7988
+ surfaces: CLI_ONLY,
7989
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7990
+ run: (stack) => lintAutonumberFormats(stack).map((f) => ({
7991
+ severity: f.severity,
7992
+ rule: f.rule,
7993
+ where: f.where,
7994
+ path: f.where,
7995
+ message: f.message,
7996
+ hint: f.hint
7997
+ }))
7998
+ },
7999
+ // #2554 — a `type:'form'` action target naming a missing or LIST view opens a
8000
+ // broken form at runtime; a list/form view-key collision silently renames one
8001
+ // view so references resolve to the OTHER. Both are broken.
8002
+ {
8003
+ name: "lintViewRefs",
8004
+ tier: "gating",
8005
+ input: "parsed",
8006
+ commands: ALL,
8007
+ source: "packages/lint/src/lint-view-refs.ts",
8008
+ surfaces: CLI_ONLY,
8009
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8010
+ run: (stack) => lintViewRefs(stack).map((f) => ({
8011
+ severity: f.severity,
8012
+ rule: f.rule,
8013
+ where: f.where,
8014
+ path: f.where,
8015
+ message: f.message,
8016
+ hint: f.hint
8017
+ }))
8018
+ },
8019
+ // #3991 — a column carrying BOTH a field-level `unique: true` and a
8020
+ // single-column declared unique index has two intents, of which exactly one
8021
+ // takes effect (the global index wins; the tenant composite is unreachable).
8022
+ {
8023
+ name: "lintUniqueDeclarations",
8024
+ tier: "advisory",
8025
+ input: "parsed",
8026
+ commands: ["validate", "build"],
8027
+ source: "packages/lint/src/data-model-rules.ts",
8028
+ surfaces: CLI_ONLY,
8029
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8030
+ scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly as R10 of its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
8031
+ run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
8032
+ severity: f.severity === "suggestion" ? "info" : f.severity,
8033
+ rule: f.rule,
8034
+ where: f.path,
8035
+ path: f.path,
8036
+ message: f.message,
8037
+ hint: f.fix ?? ""
8038
+ }))
8039
+ },
8040
+ // ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
8041
+ // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
8042
+ // binding gate, vocabulary freeze), moving the failure from a runtime deny to
8043
+ // an author-time fix-it. Per ADR-0049 this is not advisory security.
8044
+ {
8045
+ name: "validateSecurityPosture",
8046
+ tier: "gating",
8047
+ input: "parsed",
8048
+ commands: ALL,
8049
+ source: "packages/lint/src/validate-security-posture.ts",
8050
+ surfaces: CLI_ONLY,
8051
+ surfaceReason: "Already gated at this surface by a DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on `object` (`registerAuthoringGate`) that enforces the same OWD posture rules on every runtime write. Running the linter here as well would double-report one refusal in two vocabularies. Consolidating the two onto this table is P2 (#4463), and is a merge, not a hole.",
8052
+ run: (stack) => validateSecurityPosture(stack)
8053
+ },
8054
+ // ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
8055
+ // sharing rule that walks it builds a second permission hierarchy (the
8056
+ // dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
8057
+ // so it grants nothing it appears to.
8058
+ {
8059
+ name: "validateOrgAxisRedLines",
8060
+ tier: "gating",
8061
+ input: "parsed",
8062
+ commands: ALL,
8063
+ source: "packages/lint/src/validate-org-axis-red-lines.ts",
8064
+ surfaces: CLI_ONLY,
8065
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8066
+ run: (stack) => validateOrgAxisRedLines(stack)
8067
+ }
8068
+ ];
8069
+ function authoringRulesFor(command) {
8070
+ return AUTHORING_RULES.filter((r) => r.commands.includes(command));
8071
+ }
8072
+ function runAuthoringRules(command, run) {
8073
+ const findings = [];
8074
+ const ctx = { sduiManifest: run.sduiManifest };
8075
+ for (const rule of authoringRulesFor(command)) {
8076
+ const stack = rule.input === "normalized" ? run.normalized : run.parsed ?? run.normalized;
8077
+ findings.push(...rule.run(stack, ctx));
8078
+ }
8079
+ return findings;
8080
+ }
8081
+ function splitBySeverity(findings) {
8082
+ return {
8083
+ errors: findings.filter((f) => f.severity === "error"),
8084
+ advisories: findings.filter((f) => f.severity !== "error")
8085
+ };
8086
+ }
8087
+
8088
+ // src/runtime-gate.ts
8089
+ var TYPE_TO_STACK_KEY = {
8090
+ flow: "flows",
8091
+ object: "objects",
8092
+ view: "views",
8093
+ action: "actions",
8094
+ page: "pages",
8095
+ dashboard: "dashboards",
8096
+ agent: "agents",
8097
+ hook: "hooks",
8098
+ seed: "seeds"
8099
+ };
8100
+ function runtimeAuthoringRulesFor(type) {
8101
+ return AUTHORING_RULES.filter(
8102
+ (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
8103
+ );
8104
+ }
8105
+ function runtimeGatedTypes() {
8106
+ const types = /* @__PURE__ */ new Set();
8107
+ for (const rule of AUTHORING_RULES) {
8108
+ if (!rule.surfaces.includes("runtime-publish")) continue;
8109
+ for (const t of rule.runtimeTypes ?? []) types.add(t);
8110
+ }
8111
+ return [...types].sort();
8112
+ }
8113
+ function stackKeyForType(type) {
8114
+ return TYPE_TO_STACK_KEY[type] ?? null;
8115
+ }
8116
+ var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
8117
+ function runRules(rules, stack, ctx) {
8118
+ const findings = [];
8119
+ for (const rule of rules) {
8120
+ try {
8121
+ findings.push(...rule.run(stack, ctx));
8122
+ } catch (err) {
8123
+ findings.push({
8124
+ severity: "warning",
8125
+ rule: "authoring-rule-threw",
8126
+ where: rule.name,
8127
+ path: rule.source,
8128
+ message: `rule ${rule.name} threw while judging this write: ${err instanceof Error ? err.message : String(err)}`,
8129
+ hint: "This is a bug in the rule, not in the metadata \u2014 the write was not blocked by it. Please report it with the body that triggered it."
8130
+ });
8131
+ }
8132
+ }
8133
+ return findings;
8134
+ }
8135
+ function runRuntimeAuthoringRules(args) {
8136
+ const rules = runtimeAuthoringRulesFor(args.type);
8137
+ const empty = { errors: [], advisories: [], rulesRun: [] };
8138
+ if (rules.length === 0) return empty;
8139
+ const stackKey = stackKeyForType(args.type);
8140
+ if (!stackKey) return empty;
8141
+ if (!args.item || typeof args.item !== "object") return empty;
8142
+ const item = args.item;
8143
+ const itemName = typeof item.name === "string" ? item.name : void 0;
8144
+ const contextObjects = args.context?.objects ?? [];
8145
+ const ctx = { sduiManifest: args.sduiManifest };
8146
+ const writesIntoContext = stackKey === "objects";
8147
+ const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
8148
+ const baseline = { objects: baselineObjects };
8149
+ const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
8150
+ const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
8151
+ const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
8152
+ return {
8153
+ errors: added.filter((f) => f.severity === "error"),
8154
+ advisories: added.filter((f) => f.severity !== "error"),
8155
+ rulesRun: rules.map((r) => r.name)
8156
+ };
8157
+ }
5956
8158
  // Annotate the CommonJS export names for ESM import in node:
5957
8159
  0 && (module.exports = {
5958
8160
  ACTION_BODY_WRITE_EXCLUSIONS,
@@ -5960,6 +8162,7 @@ function validateReferenceIntegrity(stack) {
5960
8162
  ACTION_BODY_WRITE_PATTERN_IDS,
5961
8163
  ACTION_BODY_WRITE_UNKNOWN_FIELD,
5962
8164
  ACTION_NAME_UNDEFINED,
8165
+ ACTION_NO_PLACEMENT,
5963
8166
  ACTION_RECORD_WRITE_DISCARDED,
5964
8167
  ACTION_RECORD_WRITE_PATTERNS,
5965
8168
  ACTION_RECORD_WRITE_PATTERN_IDS,
@@ -5975,6 +8178,13 @@ function validateReferenceIntegrity(stack) {
5975
8178
  APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
5976
8179
  APPROVAL_EXPRESSION_INVALID,
5977
8180
  APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
8181
+ AUTHORING_COMMANDS,
8182
+ AUTHORING_RULES,
8183
+ AUTHORING_SURFACES,
8184
+ AUTONUMBER_LITERAL_TOKEN,
8185
+ AUTONUMBER_OPTIONAL_FIELD,
8186
+ AUTONUMBER_SELF_REFERENCE,
8187
+ AUTONUMBER_UNKNOWN_FIELD,
5978
8188
  CAPABILITY_REFERENCE_UNKNOWN,
5979
8189
  CHART_AXIS_NOT_SELECTED,
5980
8190
  CHART_CONFIG_MISSING,
@@ -5985,13 +8195,29 @@ function validateReferenceIntegrity(stack) {
5985
8195
  DASHBOARD_ACTION_ROUTE_UNRESOLVED,
5986
8196
  DASHBOARD_ACTION_TARGET_UNDEFINED,
5987
8197
  DASHBOARD_FILTER_FIELD_UNKNOWN,
8198
+ EXPRESSION_INVALID,
5988
8199
  FIELD_GROUP_EMPTY,
5989
8200
  FIELD_GROUP_UNDECLARED,
5990
8201
  FILTER_TOKEN_UNKNOWN,
8202
+ FLOW_APPROVAL_REVISE_DEAD_END,
8203
+ FLOW_APPROVAL_REVISE_DISABLED,
8204
+ FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
8205
+ FLOW_BARE_DOLLAR_REF,
8206
+ FLOW_BRANCH_LABEL_UNMATCHED,
8207
+ FLOW_DATE_EQUALITY_FILTER,
8208
+ FLOW_DECISION_UNCONDITIONAL_BRANCH,
8209
+ FLOW_DEFAULT_EDGE_WITH_CONDITION,
8210
+ FLOW_DOUBLE_BRACE_INTERP,
5991
8211
  FLOW_DRAFT_STATUS_AMBIGUOUS,
8212
+ FLOW_ERROR_LABEL_NOT_FAULT,
8213
+ FLOW_INERT_NODE_CONDITION,
8214
+ FLOW_MULTIPLE_DEFAULT_EDGES,
5992
8215
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
8216
+ FLOW_PHANTOM_AGGREGATION,
8217
+ FLOW_RUNAS_UNSCOPED,
5993
8218
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
5994
8219
  FLOW_TEMPLATE_UNKNOWN_FIELD,
8220
+ FLOW_TIME_RELATIVE_ANTIPATTERN,
5995
8221
  FLOW_TRIGGER_UNKNOWN_OBJECT,
5996
8222
  FLOW_UPDATE_READONLY_FIELD,
5997
8223
  FLOW_UPDATE_READONLY_WHEN_FIELD,
@@ -6004,19 +8230,25 @@ function validateReferenceIntegrity(stack) {
6004
8230
  HOOK_BODY_WRITE_PATTERN_IDS,
6005
8231
  HOOK_BODY_WRITE_UNKNOWN_FIELD,
6006
8232
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
8233
+ LIVENESS_DEAD_PROPERTY,
8234
+ LIVENESS_EXPERIMENTAL_PROPERTY,
6007
8235
  MEASURE_AGGREGATE_INCOHERENT,
6008
8236
  NAV_OBJECT_UNGRANTED,
8237
+ NAV_TARGET_UNRESOLVED,
8238
+ NULL_GUARD_HINT,
6009
8239
  OBJECT_REFERENCE_UNKNOWN,
6010
8240
  OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
6011
8241
  ORG_AXIS_CROSS_ORG_BU_GRANT,
6012
8242
  ORG_AXIS_PERMISSION_INHERITANCE,
6013
8243
  PAGE_FIELD_UNKNOWN,
6014
8244
  PAGE_SOURCE_CLASSNAME,
8245
+ REACT_BLOCK_NEEDS_RECORD_CONTEXT,
6015
8246
  REACT_CHART_AGGREGATE_INVALID,
6016
8247
  REACT_CHART_AXIS_UNKNOWN,
6017
8248
  REACT_CHART_FIELD_UNKNOWN,
6018
8249
  REFERENCE_INTEGRITY_RULES,
6019
8250
  SEARCHABLE_FIELD_UNKNOWN,
8251
+ SEARCHABLE_FIELD_UNSEARCHABLE,
6020
8252
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
6021
8253
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
6022
8254
  SECURITY_DELEGATION_MISSING_REASON,
@@ -6041,17 +8273,37 @@ function validateReferenceIntegrity(stack) {
6041
8273
  TITLE_UNRESOLVABLE,
6042
8274
  TRANSLATION_OPTION_KEY_UNKNOWN,
6043
8275
  TRANSLATION_TARGET_UNKNOWN,
8276
+ UNIQUE_DOUBLE_DECLARATION,
6044
8277
  VIEW_CONTAINER_SHAPE,
8278
+ VIEW_KEY_COLLISION,
8279
+ VIEW_REF_FORM_TARGET_KIND,
8280
+ VIEW_REF_FORM_TARGET_MISSING,
6045
8281
  VISIBILITY_ALIAS_DEPRECATED,
6046
8282
  VISIBILITY_ROOT_MISLAYERED,
6047
8283
  WIDGET_DATASET_UNKNOWN,
6048
8284
  WIDGET_DIMENSION_UNKNOWN,
6049
8285
  WIDGET_MEASURE_UNKNOWN,
8286
+ authoringRulesFor,
6050
8287
  buildAccessMatrix,
6051
8288
  diffAccessMatrix,
6052
8289
  extractHookBodyWriteSet,
6053
8290
  extractHookBodyWrites,
8291
+ findUnguardedNullableOperands,
8292
+ lintAutonumberFormats,
8293
+ lintDataModel,
8294
+ lintFlowPatterns,
8295
+ lintLivenessProperties,
8296
+ lintUniqueDeclarations,
8297
+ lintViewRefs,
8298
+ nullGuardMessage,
8299
+ runAuthoringRules,
8300
+ runRuntimeAuthoringRules,
8301
+ runtimeAuthoringRulesFor,
8302
+ runtimeGatedTypes,
8303
+ splitBySeverity,
8304
+ stackKeyForType,
6054
8305
  validateActionBodyWrites,
8306
+ validateActionLocations,
6055
8307
  validateActionNameRefs,
6056
8308
  validateAiAgentAuthoring,
6057
8309
  validateAiSurfaceAffinity,
@@ -6065,10 +8317,12 @@ function validateReferenceIntegrity(stack) {
6065
8317
  validateFlowTemplatePaths,
6066
8318
  validateFlowTriggerReadiness,
6067
8319
  validateFormLayout,
8320
+ validateFunctionalCompleteness,
6068
8321
  validateHookBodyWrites,
6069
8322
  validateJsxPages,
6070
8323
  validateListViewMode,
6071
8324
  validateNavAccess,
8325
+ validateNavTargetRefs,
6072
8326
  validateObjectReferences,
6073
8327
  validateOrgAxisRedLines,
6074
8328
  validatePageFieldBindings,