@objectstack/lint 17.0.0-rc.3 → 17.0.0-rc.5

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
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER: () => APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
38
38
  APPROVAL_APPROVER_TYPE_DEPRECATED: () => APPROVAL_APPROVER_TYPE_DEPRECATED,
39
39
  APPROVAL_APPROVER_TYPE_UNKNOWN: () => APPROVAL_APPROVER_TYPE_UNKNOWN,
40
+ APPROVAL_APPROVER_TYPE_UNSUPPORTED: () => APPROVAL_APPROVER_TYPE_UNSUPPORTED,
40
41
  APPROVAL_DECISION_OUTPUTS_RESERVED: () => APPROVAL_DECISION_OUTPUTS_RESERVED,
41
42
  APPROVAL_ESCALATION_REASSIGN_NO_TARGET: () => APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
42
43
  APPROVAL_EXPRESSION_INVALID: () => APPROVAL_EXPRESSION_INVALID,
@@ -55,15 +56,19 @@ __export(index_exports, {
55
56
  CHART_DIMENSION_UNKNOWN: () => CHART_DIMENSION_UNKNOWN,
56
57
  CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
57
58
  CHART_MEASURE_UNKNOWN: () => CHART_MEASURE_UNKNOWN,
59
+ COMPONENT_PROPS_INVALID: () => COMPONENT_PROPS_INVALID,
60
+ COMPONENT_PROPS_UNKNOWN_KEY: () => COMPONENT_PROPS_UNKNOWN_KEY,
58
61
  DASHBOARD_ACTION_ROUTE_UNRESOLVED: () => DASHBOARD_ACTION_ROUTE_UNRESOLVED,
59
62
  DASHBOARD_ACTION_TARGET_UNDEFINED: () => DASHBOARD_ACTION_TARGET_UNDEFINED,
60
63
  DASHBOARD_FILTER_FIELD_UNKNOWN: () => DASHBOARD_FILTER_FIELD_UNKNOWN,
61
64
  EXPRESSION_INVALID: () => EXPRESSION_INVALID,
62
65
  FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
66
+ FIELD_GROUP_SHADOWED: () => FIELD_GROUP_SHADOWED,
63
67
  FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
64
68
  FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
65
69
  FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
66
70
  FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
71
+ FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED: () => FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED,
67
72
  FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE: () => FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
68
73
  FLOW_BARE_DOLLAR_REF: () => FLOW_BARE_DOLLAR_REF,
69
74
  FLOW_BRANCH_LABEL_UNMATCHED: () => FLOW_BRANCH_LABEL_UNMATCHED,
@@ -75,12 +80,16 @@ __export(index_exports, {
75
80
  FLOW_ERROR_LABEL_NOT_FAULT: () => FLOW_ERROR_LABEL_NOT_FAULT,
76
81
  FLOW_INERT_NODE_CONDITION: () => FLOW_INERT_NODE_CONDITION,
77
82
  FLOW_MULTIPLE_DEFAULT_EDGES: () => FLOW_MULTIPLE_DEFAULT_EDGES,
83
+ FLOW_MULTI_WRITE_UNFILTERED: () => FLOW_MULTI_WRITE_UNFILTERED,
78
84
  FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
79
85
  FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
80
86
  FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
81
87
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL: () => FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
82
88
  FLOW_TEMPLATE_UNKNOWN_FIELD: () => FLOW_TEMPLATE_UNKNOWN_FIELD,
83
89
  FLOW_TIME_RELATIVE_ANTIPATTERN: () => FLOW_TIME_RELATIVE_ANTIPATTERN,
90
+ FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID: () => FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
91
+ FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE: () => FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
92
+ FLOW_TRIGGER_UNKNOWN_EVENT: () => FLOW_TRIGGER_UNKNOWN_EVENT,
84
93
  FLOW_TRIGGER_UNKNOWN_OBJECT: () => FLOW_TRIGGER_UNKNOWN_OBJECT,
85
94
  FLOW_UPDATE_READONLY_FIELD: () => FLOW_UPDATE_READONLY_FIELD,
86
95
  FLOW_UPDATE_READONLY_WHEN_FIELD: () => FLOW_UPDATE_READONLY_WHEN_FIELD,
@@ -95,6 +104,7 @@ __export(index_exports, {
95
104
  LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
96
105
  LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
97
106
  LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
107
+ MAX_SCHEMA_WALK_DEPTH: () => MAX_SCHEMA_WALK_DEPTH,
98
108
  MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
99
109
  NAV_OBJECT_UNGRANTED: () => NAV_OBJECT_UNGRANTED,
100
110
  NAV_TARGET_UNRESOLVED: () => NAV_TARGET_UNRESOLVED,
@@ -108,14 +118,19 @@ __export(index_exports, {
108
118
  REACT_BLOCK_NEEDS_RECORD_CONTEXT: () => REACT_BLOCK_NEEDS_RECORD_CONTEXT,
109
119
  REACT_CHART_AGGREGATE_INVALID: () => REACT_CHART_AGGREGATE_INVALID,
110
120
  REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
121
+ REACT_CHART_DRILLDOWN_INVALID: () => REACT_CHART_DRILLDOWN_INVALID,
111
122
  REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
112
123
  REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
124
+ RLS_PREDICATE_UNENFORCEABLE: () => RLS_PREDICATE_UNENFORCEABLE,
125
+ RLS_PREDICATE_UNPARSEABLE: () => RLS_PREDICATE_UNPARSEABLE,
126
+ RUNTIME_AJV_OPTIONS: () => RUNTIME_AJV_OPTIONS,
113
127
  SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
114
128
  SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
115
129
  SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
116
130
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
117
131
  SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
118
132
  SECURITY_EXTERNAL_WIDER: () => SECURITY_EXTERNAL_WIDER,
133
+ SECURITY_FLS_UNQUALIFIED_KEY: () => SECURITY_FLS_UNQUALIFIED_KEY,
119
134
  SECURITY_GRANT_EXPIRED_AT_AUTHORING: () => SECURITY_GRANT_EXPIRED_AT_AUTHORING,
120
135
  SECURITY_MASTER_DETAIL_UNGRANTED: () => SECURITY_MASTER_DETAIL_UNGRANTED,
121
136
  SECURITY_OWD_ALIAS: () => SECURITY_OWD_ALIAS,
@@ -126,6 +141,8 @@ __export(index_exports, {
126
141
  SEED_INSERT_MODE_DUPLICATES_ON_REPLAY: () => SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
127
142
  SEED_VALUE_OUTSIDE_STATE_MACHINE: () => SEED_VALUE_OUTSIDE_STATE_MACHINE,
128
143
  SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
144
+ SHARING_RULE_RUNTIME_VARIABLE_CONDITION: () => SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
145
+ SHARING_RULE_UNLOWERABLE_CONDITION: () => SHARING_RULE_UNLOWERABLE_CONDITION,
129
146
  STYLE_CLASSNAME_TAILWIND: () => STYLE_CLASSNAME_TAILWIND,
130
147
  STYLE_NODE_MISSING_ID: () => STYLE_NODE_MISSING_ID,
131
148
  STYLE_RESPONSIVE_NO_BASE: () => STYLE_RESPONSIVE_NO_BASE,
@@ -135,8 +152,14 @@ __export(index_exports, {
135
152
  TITLE_FORMAT_RETIRED: () => TITLE_FORMAT_RETIRED,
136
153
  TITLE_UNRESOLVABLE: () => TITLE_UNRESOLVABLE,
137
154
  TRANSLATION_OPTION_KEY_UNKNOWN: () => TRANSLATION_OPTION_KEY_UNKNOWN,
155
+ TRANSLATION_SECTION_NAME_MISSING: () => TRANSLATION_SECTION_NAME_MISSING,
138
156
  TRANSLATION_TARGET_UNKNOWN: () => TRANSLATION_TARGET_UNKNOWN,
139
157
  UNIQUE_DOUBLE_DECLARATION: () => UNIQUE_DOUBLE_DECLARATION,
158
+ UNIQUE_LEGACY_ORGANIZATION_COMPOSITE: () => UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
159
+ UNIQUE_UNSCOPED_DECLARED_INDEX: () => UNIQUE_UNSCOPED_DECLARED_INDEX,
160
+ VALIDATION_RULE_REGEX_UNCOMPILABLE: () => VALIDATION_RULE_REGEX_UNCOMPILABLE,
161
+ VALIDATION_RULE_SCHEMA_UNCOMPILABLE: () => VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
162
+ VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT: () => VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
140
163
  VIEW_CONTAINER_SHAPE: () => VIEW_CONTAINER_SHAPE,
141
164
  VIEW_KEY_COLLISION: () => VIEW_KEY_COLLISION,
142
165
  VIEW_REF_FORM_TARGET_KIND: () => VIEW_REF_FORM_TARGET_KIND,
@@ -145,6 +168,8 @@ __export(index_exports, {
145
168
  VISIBILITY_ROOT_MISLAYERED: () => VISIBILITY_ROOT_MISLAYERED,
146
169
  WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
147
170
  WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
171
+ WIDGET_LEGACY_ANALYTICS_SHAPE: () => WIDGET_LEGACY_ANALYTICS_SHAPE,
172
+ WIDGET_LEGACY_ANALYTICS_UNRENDERABLE: () => WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
148
173
  WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
149
174
  authoringRulesFor: () => authoringRulesFor,
150
175
  buildAccessMatrix: () => buildAccessMatrix,
@@ -152,12 +177,16 @@ __export(index_exports, {
152
177
  extractHookBodyWriteSet: () => extractHookBodyWriteSet,
153
178
  extractHookBodyWrites: () => extractHookBodyWrites,
154
179
  findUnguardedNullableOperands: () => findUnguardedNullableOperands,
180
+ isSourceAuthoredPage: () => isSourceAuthoredPage,
155
181
  lintAutonumberFormats: () => lintAutonumberFormats,
156
182
  lintDataModel: () => lintDataModel,
157
183
  lintFlowPatterns: () => lintFlowPatterns,
184
+ lintLegacyOrganizationComposites: () => lintLegacyOrganizationComposites,
158
185
  lintLivenessProperties: () => lintLivenessProperties,
159
186
  lintUniqueDeclarations: () => lintUniqueDeclarations,
187
+ lintUnscopedDeclaredIndexes: () => lintUnscopedDeclaredIndexes,
160
188
  lintViewRefs: () => lintViewRefs,
189
+ nearestRegisteredFormat: () => nearestRegisteredFormat,
161
190
  nullGuardMessage: () => nullGuardMessage,
162
191
  runAuthoringRules: () => runAuthoringRules,
163
192
  runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
@@ -174,6 +203,7 @@ __export(index_exports, {
174
203
  validateApprovalApprovers: () => validateApprovalApprovers,
175
204
  validateCapabilityReferences: () => validateCapabilityReferences,
176
205
  validateChartBindings: () => validateChartBindings,
206
+ validateComponentProps: () => validateComponentProps,
177
207
  validateDashboardActionRefs: () => validateDashboardActionRefs,
178
208
  validateFilterTokens: () => validateFilterTokens,
179
209
  validateFlowNodeWrites: () => validateFlowNodeWrites,
@@ -196,16 +226,22 @@ __export(index_exports, {
196
226
  validateRecordTitle: () => validateRecordTitle,
197
227
  validateReferenceIntegrity: () => validateReferenceIntegrity,
198
228
  validateResponsiveStyles: () => validateResponsiveStyles,
229
+ validateRlsPredicateEnforceability: () => validateRlsPredicateEnforceability,
230
+ validateRuleCompilability: () => validateRuleCompilability,
231
+ validateRuleSchemaFormats: () => validateRuleSchemaFormats,
199
232
  validateSearchableFields: () => validateSearchableFields,
200
233
  validateSecurityPosture: () => validateSecurityPosture,
201
234
  validateSeedReplaySafety: () => validateSeedReplaySafety,
202
235
  validateSeedStateMachine: () => validateSeedStateMachine,
203
236
  validateSemanticRoles: () => validateSemanticRoles,
237
+ validateSharingRuleEnforceability: () => validateSharingRuleEnforceability,
204
238
  validateStackExpressions: () => validateStackExpressions,
239
+ validateTranslatableSections: () => validateTranslatableSections,
205
240
  validateTranslationReferences: () => validateTranslationReferences,
206
241
  validateViewContainers: () => validateViewContainers,
207
242
  validateVisibilityPredicates: () => validateVisibilityPredicates,
208
- validateWidgetBindings: () => validateWidgetBindings
243
+ validateWidgetBindings: () => validateWidgetBindings,
244
+ walkPageComponents: () => walkPageComponents
209
245
  });
210
246
  module.exports = __toCommonJS(index_exports);
211
247
 
@@ -220,6 +256,9 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
220
256
  ...import_data.FIELD_GROUP_SYSTEM_FIELDS,
221
257
  ...Object.values(import_system.SystemFieldName)
222
258
  ]);
259
+ function injectedColumnsFor(objectDef) {
260
+ return (0, import_data.resolveInjectedSystemColumns)(objectDef).names;
261
+ }
223
262
 
224
263
  // src/validate-widget-bindings.ts
225
264
  var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
@@ -541,21 +580,14 @@ function validateWidgetBindings(stack) {
541
580
  }
542
581
 
543
582
  // src/validate-expressions.ts
544
- var import_formula = require("@objectstack/formula");
583
+ var import_formula2 = require("@objectstack/formula");
545
584
  var import_automation = require("@objectstack/spec/automation");
546
585
 
547
586
  // src/validate-null-guards.ts
548
- var import_cel_js = require("@marcbachmann/cel-js");
587
+ var import_formula = require("@objectstack/formula");
549
588
  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
589
  var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
551
590
  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
591
  function isNode(v) {
560
592
  return !!v && typeof v === "object" && typeof v.op === "string";
561
593
  }
@@ -688,12 +720,8 @@ function findUnguardedNullableOperands(source, opts) {
688
720
  if (typeof source !== "string" || !source.trim()) return [];
689
721
  if (opts.nullableFields.size === 0) return [];
690
722
  const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
691
- let ast;
692
- try {
693
- ast = getParseEnv().parse(source).ast;
694
- } catch {
695
- return [];
696
- }
723
+ const ast = (0, import_formula.parseCelToAst)(source);
724
+ if (!ast) return [];
697
725
  const hasOperands = /* @__PURE__ */ new Set();
698
726
  collectHasOperands(ast, roots, hasOperands);
699
727
  const findings = [];
@@ -756,10 +784,14 @@ function findUnguardedNullableOperands(source, opts) {
756
784
  visit(ast, /* @__PURE__ */ new Set());
757
785
  return findings;
758
786
  }
759
- function nullGuardMessage(subject, objectName, finding) {
787
+ var OUTCOME_CLAUSE = {
788
+ "fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
789
+ "fail-open": "so the predicate is SKIPPED fail-open \u2014 the field is never actually required, the write proceeds unchecked, and the only trace is a `requiredWhen \u2026 failed to evaluate \u2014 skipped` log line (#4649/#4811)"
790
+ };
791
+ function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
760
792
  const owner = objectName ? `'${objectName}'` : "this object";
761
793
  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}`;
794
+ 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 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
763
795
  }
764
796
 
765
797
  // src/validate-expressions.ts
@@ -779,7 +811,7 @@ function buildFieldIndex(objects) {
779
811
  let names = [];
780
812
  if (Array.isArray(fields)) names = fields.map((f) => f.name).filter((n) => typeof n === "string");
781
813
  else if (fields && typeof fields === "object") names = Object.keys(fields);
782
- idx.set(name, names);
814
+ idx.set(name, [.../* @__PURE__ */ new Set([...names, ...injectedColumnsFor(obj)])]);
783
815
  }
784
816
  return idx;
785
817
  }
@@ -839,6 +871,19 @@ function buildNullableFieldIndex(objects) {
839
871
  }
840
872
  return idx;
841
873
  }
874
+ function readsParentRoot(source) {
875
+ const roots = (0, import_formula2.collectCelRootIdentifiers)(source);
876
+ return roots.ok && roots.roots.includes("parent");
877
+ }
878
+ function masterDetailCount(obj) {
879
+ let n = 0;
880
+ for (const [, def] of fieldEntries(obj)) {
881
+ if (def.type !== "master_detail") continue;
882
+ const ref = def.reference;
883
+ if (typeof ref === "string" && ref.trim() !== "") n += 1;
884
+ }
885
+ return n;
886
+ }
842
887
  function celSourceOf(raw) {
843
888
  if (typeof raw === "string") return raw;
844
889
  if (raw && typeof raw === "object") {
@@ -852,7 +897,7 @@ function rulePredicates(rule, path) {
852
897
  const out = [];
853
898
  const name = typeof rule.name === "string" ? rule.name : "?";
854
899
  const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
855
- const main = rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula;
900
+ const main = rule.condition;
856
901
  if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
857
902
  if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
858
903
  for (const branch of ["then", "otherwise"]) {
@@ -869,7 +914,7 @@ function validateStackExpressions(stack) {
869
914
  const fieldIndex = buildFieldIndex(objects);
870
915
  const fieldTypeIndex = buildFieldTypeIndex(objects);
871
916
  const nullableIndex = buildNullableFieldIndex(objects);
872
- const checkNullGuards = (where, subject, raw, objectName) => {
917
+ const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
873
918
  if (!objectName) return;
874
919
  const nullableFields = nullableIndex.get(objectName);
875
920
  if (!nullableFields || nullableFields.size === 0) return;
@@ -878,7 +923,7 @@ function validateStackExpressions(stack) {
878
923
  for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
879
924
  issues.push({
880
925
  where,
881
- message: nullGuardMessage(subject, objectName, finding),
926
+ message: nullGuardMessage(subject, objectName, finding, outcome),
882
927
  source,
883
928
  severity: "error"
884
929
  });
@@ -888,7 +933,7 @@ function validateStackExpressions(stack) {
888
933
  if (raw == null) return;
889
934
  const fields = objectName ? fieldIndex.get(objectName) : void 0;
890
935
  const fieldTypes = objectName ? fieldTypeIndex.get(objectName) : void 0;
891
- const res = (0, import_formula.validateExpression)(
936
+ const res = (0, import_formula2.validateExpression)(
892
937
  "predicate",
893
938
  raw,
894
939
  objectName ? { objectName, fields, fieldTypes, scope } : { scope }
@@ -898,7 +943,7 @@ function validateStackExpressions(stack) {
898
943
  };
899
944
  const checkDeclaredPredicate = (where, raw) => {
900
945
  if (raw == null) return;
901
- const res = (0, import_formula.validateExpression)("predicate", raw);
946
+ const res = (0, import_formula2.validateExpression)("predicate", raw);
902
947
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
903
948
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
904
949
  };
@@ -947,31 +992,45 @@ function validateStackExpressions(stack) {
947
992
  }
948
993
  for (const obj of objects) {
949
994
  const objectName = typeof obj.name === "string" ? obj.name : void 0;
950
- const validations = obj.validations ?? obj.validationRules;
995
+ const validations = obj.validations;
951
996
  for (const rule of asArray2(validations)) {
952
997
  const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
953
- check(where, rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula, objectName, "record");
998
+ check(where, rule.condition, objectName, "record");
954
999
  check(`${where} when`, rule.when, objectName, "record");
955
1000
  for (const p of rulePredicates(rule, "")) {
956
1001
  checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
957
1002
  }
958
1003
  }
959
1004
  const fields = obj.fields;
960
- const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
961
- for (const f of fieldList) {
962
- if (f && typeof f === "object") {
963
- const fname = f.name ?? "?";
964
- for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
965
- check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
966
- }
1005
+ const fieldList = Array.isArray(fields) ? fields.filter((f) => !!f && typeof f === "object").map((f) => [typeof f.name === "string" ? f.name : "?", f]) : fields && typeof fields === "object" ? Object.entries(fields).filter(([, def]) => !!def && typeof def === "object").map(([n, def]) => [n, def]) : [];
1006
+ const masters = masterDetailCount(obj);
1007
+ for (const [fname, f] of fieldList) {
1008
+ for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
1009
+ check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
1010
+ }
1011
+ const roWhenSource = celSourceOf(f.readonlyWhen);
1012
+ if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
1013
+ issues.push({
1014
+ where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
1015
+ message: `\`readonlyWhen\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and the field would be locked on every write. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
1016
+ source: roWhenSource,
1017
+ severity: "error"
1018
+ });
967
1019
  }
968
- if (f && typeof f === "object" && f.formula) {
969
- const res = (0, import_formula.validateExpression)(
1020
+ checkNullGuards(
1021
+ `object '${objectName}' \xB7 field '${fname}' requiredWhen`,
1022
+ `field '${fname}' requiredWhen`,
1023
+ f.requiredWhen,
1024
+ objectName,
1025
+ "fail-open"
1026
+ );
1027
+ if (f.expression) {
1028
+ const res = (0, import_formula2.validateExpression)(
970
1029
  "value",
971
- f.formula,
1030
+ f.expression,
972
1031
  objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
973
1032
  );
974
- const fieldWhere = `object '${objectName}' \xB7 field '${f.name ?? "?"}' formula`;
1033
+ const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
975
1034
  for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
976
1035
  for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
977
1036
  }
@@ -979,7 +1038,7 @@ function validateStackExpressions(stack) {
979
1038
  }
980
1039
  const seenActions = /* @__PURE__ */ new Set();
981
1040
  const checkAction = (where, action, objectName) => {
982
- const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0) ?? (typeof action.object === "string" ? action.object : void 0);
1041
+ const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0);
983
1042
  const name = typeof action.name === "string" ? action.name : "?";
984
1043
  const key = `${obj ?? ""}:${name}`;
985
1044
  if (seenActions.has(key)) return;
@@ -998,10 +1057,10 @@ function validateStackExpressions(stack) {
998
1057
  checkAction(`object '${objectName}'`, action, objectName);
999
1058
  }
1000
1059
  }
1001
- for (const rule of asArray2(stack.sharingRules)) {
1002
- const ruleObj = typeof rule.object === "string" ? rule.object : void 0;
1003
- const where = `sharingRule '${rule.name ?? "?"}'${ruleObj ? ` (${ruleObj})` : ""} condition`;
1004
- check(where, rule.condition ?? rule.criteria ?? rule.predicate, ruleObj, "record");
1060
+ for (const sharingRule of asArray2(stack.sharingRules)) {
1061
+ const ruleObj = typeof sharingRule.object === "string" ? sharingRule.object : void 0;
1062
+ const where = `sharingRule '${sharingRule.name ?? "?"}'${ruleObj ? ` (${ruleObj})` : ""} condition`;
1063
+ check(where, sharingRule.condition, ruleObj, "record");
1005
1064
  }
1006
1065
  for (const hook of asArray2(stack.hooks)) {
1007
1066
  const hookName = hook.name ?? "?";
@@ -1180,9 +1239,12 @@ function validateFunctionalCompleteness(stack) {
1180
1239
  }
1181
1240
 
1182
1241
  // src/validate-flow-trigger-readiness.ts
1242
+ var import_automation2 = require("@objectstack/spec/automation");
1183
1243
  var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
1184
1244
  var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
1185
1245
  var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
1246
+ var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
1247
+ var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
1186
1248
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
1187
1249
  function asArray4(v) {
1188
1250
  if (Array.isArray(v)) return v;
@@ -1194,6 +1256,12 @@ function asArray4(v) {
1194
1256
  }
1195
1257
  return [];
1196
1258
  }
1259
+ function renderNonObject(v) {
1260
+ const t = typeof v;
1261
+ if (t === "string" || t === "number" || t === "boolean") return `${JSON.stringify(v)} (a ${t})`;
1262
+ if (t === "bigint") return `${String(v)}n (a bigint)`;
1263
+ return `a ${t}`;
1264
+ }
1197
1265
  function startNodeOf(flow) {
1198
1266
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
1199
1267
  const index = nodes.findIndex((n) => n?.type === "start");
@@ -1241,10 +1309,35 @@ function validateFlowTriggerReadiness(stack) {
1241
1309
  hint: `Object names match exactly. Check config.timeRelative.object against the object's registered name. If the object comes from another installed package, this warning can be ignored.`
1242
1310
  });
1243
1311
  }
1312
+ const parsed = import_automation2.TimeRelativeTriggerSchema.safeParse(tr);
1313
+ if (!parsed.success) {
1314
+ const problems = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message.replace(/\s+/g, " ").trim()}`).join("; ");
1315
+ findings.push({
1316
+ // `error` (#5762): the verdict is `TimeRelativeTriggerSchema`'s, and it
1317
+ // is the same schema the trigger safeParses at bind time. A descriptor
1318
+ // it refuses is refused at bind too — the sweep is never installed, on
1319
+ // every deployment, with no installed package able to change the
1320
+ // answer. Nothing is left for the author to weigh.
1321
+ severity: "error",
1322
+ rule: FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
1323
+ where: `flow "${flowName}" \u203A start node`,
1324
+ path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
1325
+ message: `has a config.timeRelative descriptor the time-relative trigger REFUSES at bind time, so the sweep is never installed \u2014 the flow declares a time-relative trigger and then never runs (the only trace is one warn in the server log). ${problems}`,
1326
+ hint: `Those messages are TimeRelativeTriggerSchema's own \u2014 the same schema the trigger safeParses at bind time, so a descriptor that satisfies them binds. An unrecognized key names the declared key it was probably meant to be; see content/docs/references/automation/time-relative-trigger.mdx.`
1327
+ });
1328
+ }
1244
1329
  }
1245
1330
  if (start && isRecordTriggered2 && !VALID_RECORD_TRIGGER.test((triggerType ?? "").trim())) {
1246
1331
  findings.push({
1247
- severity: "warning",
1332
+ // `error` (#5762). The token grammar is CLOSED and local: the engine
1333
+ // routes any `record-`-prefixed string to the record-change trigger by a
1334
+ // hardcoded prefix test (no registry lookup, so installing a package
1335
+ // cannot claim a new `record-*` token), and that trigger maps the token
1336
+ // with `triggerTypeToHookEvents` — the same regex this file's
1337
+ // `VALID_RECORD_TRIGGER` mirrors. Off-grammar means zero hook events,
1338
+ // which means bound-to-nothing on every deployment. Unlike an object
1339
+ // name, there is no other-package reading that rescues it.
1340
+ severity: "error",
1248
1341
  rule: FLOW_TRIGGER_UNKNOWN_EVENT,
1249
1342
  where: `flow "${flowName}" \u203A start node`,
1250
1343
  path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
@@ -1254,7 +1347,14 @@ function validateFlowTriggerReadiness(stack) {
1254
1347
  }
1255
1348
  if (start && isArrayRecordTriggered) {
1256
1349
  findings.push({
1257
- severity: "warning",
1350
+ // `error` (#5762), same id and same reason as 1c: an array maps to no
1351
+ // hook event either. The engine routes it to the record-change trigger
1352
+ // for the express purpose of making it loud, and its own comment names
1353
+ // THIS rule as the primary catch — a primary catch that only warns is
1354
+ // the "declared ≠ enforced" shape the registry's tier exists to close.
1355
+ // Multi-event arrays are deferred, not unsupported-by-accident (#3457),
1356
+ // so if they land the grammar widens here in the same commit.
1357
+ severity: "error",
1258
1358
  rule: FLOW_TRIGGER_UNKNOWN_EVENT,
1259
1359
  where: `flow "${flowName}" \u203A start node`,
1260
1360
  path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
@@ -1262,6 +1362,25 @@ function validateFlowTriggerReadiness(stack) {
1262
1362
  hint: `Use one triggerType string. For "created or updated" use record-after-write (one flow, both events, #3427). For any other combination, author one flow per event \u2014 multi-event arrays are deferred (#3457).`
1263
1363
  });
1264
1364
  }
1365
+ if (start && config.timeRelative != null && typeof config.timeRelative !== "object") {
1366
+ const fallback = isRecordTriggered2 || isArrayRecordTriggered ? "its record-change trigger" : config.schedule != null || flow.type === "schedule" ? "its plain `config.schedule` cadence" : triggerType === "api" || flow.type === "api" ? "its api trigger" : void 0;
1367
+ const consequence2 = fallback ? `The flow still binds through ${fallback}, so the descriptor is silently DROPPED \u2014 it fires on that trigger's terms (once per firing, with no record on the context) instead of once per matching record, and nothing anywhere reports the difference.` : `Nothing else on this start node declares a trigger either, so the flow binds to NOTHING and never fires \u2014 with zero diagnostics at any layer, not even the one bind-time warn a descriptor that IS an object gets when the trigger refuses it.`;
1368
+ findings.push({
1369
+ // `error` (#5762). The criterion IS the engine's routing predicate, so a
1370
+ // value that fails it is not routed to the time-relative trigger by any
1371
+ // deployment — the strongest verdict in this file, and the one case with
1372
+ // no runtime channel to fall back on (not even the bind-time warn 1b-ii
1373
+ // moves earlier). Note the two consequences below are both defects: one
1374
+ // never fires, the other silently drops the descriptor. Neither is a
1375
+ // shape the author can have meant, so both gate.
1376
+ severity: "error",
1377
+ rule: FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
1378
+ where: `flow "${flowName}" \u203A start node`,
1379
+ path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
1380
+ message: `has config.timeRelative = ${renderNonObject(config.timeRelative)}, which is not the descriptor OBJECT this slot takes \u2014 the engine routes a flow to the time-relative sweep only when config.timeRelative is an object, so this one is never routed there and the sweep is never installed. ${consequence2}`,
1381
+ hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
1382
+ });
1383
+ }
1265
1384
  if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
1266
1385
  findings.push({
1267
1386
  severity: "warning",
@@ -1277,7 +1396,7 @@ function validateFlowTriggerReadiness(stack) {
1277
1396
  }
1278
1397
 
1279
1398
  // src/flow-walk.ts
1280
- var import_automation2 = require("@objectstack/spec/automation");
1399
+ var import_automation3 = require("@objectstack/spec/automation");
1281
1400
  function isRec2(v) {
1282
1401
  return !!v && typeof v === "object" && !Array.isArray(v);
1283
1402
  }
@@ -1285,9 +1404,9 @@ function strName(v) {
1285
1404
  return typeof v === "string" && v.length > 0 ? v : void 0;
1286
1405
  }
1287
1406
  var REGION_SLOTS = new Map(
1288
- [...import_automation2.FLOW_REGION_SLOTS_BY_TYPE].map(([type, slots]) => [type, slots.map((s) => s.key)])
1407
+ [...import_automation3.FLOW_REGION_SLOTS_BY_TYPE].map(([type, slots]) => [type, slots.map((s) => s.key)])
1289
1408
  );
1290
- var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
1409
+ var REGION_CONFIG_KEYS = import_automation3.FLOW_REGION_CONFIG_KEYS;
1291
1410
  var MAX_REGION_DEPTH = 16;
1292
1411
  function flowNodeLabel(node, index) {
1293
1412
  return strName(node.label) ?? strName(node.id) ?? `#${index}`;
@@ -2287,13 +2406,13 @@ function validateSearchableFields(stack) {
2287
2406
  for (let vi = 0; vi < views.length; vi++) {
2288
2407
  const view = views[vi];
2289
2408
  if (!isRec3(view)) continue;
2290
- const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
2409
+ const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
2291
2410
  const viewObject = strName2(view.objectName) ?? strName2(view.object);
2292
2411
  if (isRec3(view.list)) {
2293
2412
  check(
2294
2413
  view.list.searchableFields,
2295
2414
  listViewObject(view.list) ?? viewObject,
2296
- `view "${viewLabel}" \u203A list`,
2415
+ `view "${viewLabel2}" \u203A list`,
2297
2416
  `views[${vi}].list.searchableFields`,
2298
2417
  "list-view searchableFields",
2299
2418
  "narrowing"
@@ -2305,7 +2424,7 @@ function validateSearchableFields(stack) {
2305
2424
  check(
2306
2425
  lv.searchableFields,
2307
2426
  listViewObject(lv) ?? viewObject,
2308
- `view "${viewLabel}" \u203A listViews.${key}`,
2427
+ `view "${viewLabel2}" \u203A listViews.${key}`,
2309
2428
  `views[${vi}].listViews.${key}.searchableFields`,
2310
2429
  "list-view searchableFields",
2311
2430
  "narrowing"
@@ -2429,8 +2548,11 @@ function sortFieldRefs(value, basePath) {
2429
2548
  }
2430
2549
  var COMPONENT_FIELD_SPECS = {
2431
2550
  "record:highlights": { props: ["fields"] },
2432
- // `sections`/`hideFields` are not in RecordDetailsProps, but every real page
2433
- // authors them (they survive because `properties` is unvalidated).
2551
+ // `sections` (object form) and `hideFields` are what every real page authors,
2552
+ // and since #5611 they are what `RecordDetailsProps` declares — this model and
2553
+ // the spec agree. (Before that, `sections` was declared as an ID `string[]`
2554
+ // and `hideFields` not at all; both survived only because `properties` is
2555
+ // unvalidated.)
2434
2556
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2435
2557
  "record:path": { props: ["statusField"] },
2436
2558
  "element:number": { props: ["field"] },
@@ -2493,7 +2615,7 @@ function indexObjectFields(stack) {
2493
2615
  }
2494
2616
  return objectFields;
2495
2617
  }
2496
- function checkFieldRefs(refs, objectName, objectFields, where, consequence = "skipped") {
2618
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
2497
2619
  const findings = [];
2498
2620
  if (!objectName) return findings;
2499
2621
  const known = objectFields.get(objectName);
@@ -2502,11 +2624,11 @@ function checkFieldRefs(refs, objectName, objectFields, where, consequence = "sk
2502
2624
  if (ref.name.includes(".")) continue;
2503
2625
  if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
2504
2626
  findings.push({
2505
- severity: consequence === "queried" ? "error" : "warning",
2627
+ severity: consequence2 === "queried" ? "error" : "warning",
2506
2628
  rule: PAGE_FIELD_UNKNOWN,
2507
2629
  where,
2508
2630
  path: ref.path,
2509
- message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (consequence === "queried" ? 'it is used in a QUERY, so the predicate can never match: the surface renders an empty result that looks exactly like "there is no data".' : "the component silently skips it, so it never renders."),
2631
+ message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (consequence2 === "queried" ? 'it is used in a QUERY, so the predicate can never match: the surface renders an empty result that looks exactly like "there is no data".' : "the component silently skips it, so it never renders."),
2510
2632
  hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
2511
2633
  });
2512
2634
  }
@@ -2560,6 +2682,42 @@ function validatePageFieldBindings(stack) {
2560
2682
  return findings;
2561
2683
  }
2562
2684
 
2685
+ // src/zod-issue-format.ts
2686
+ var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2687
+ var valueAtPath = (root, path) => {
2688
+ let cur = root;
2689
+ for (const key of path) {
2690
+ if (!isRec6(cur) && !Array.isArray(cur)) return void 0;
2691
+ cur = cur[key];
2692
+ }
2693
+ return cur;
2694
+ };
2695
+ var preview = (value) => {
2696
+ let text;
2697
+ try {
2698
+ text = JSON.stringify(value) ?? String(value);
2699
+ } catch {
2700
+ text = String(value);
2701
+ }
2702
+ return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
2703
+ };
2704
+ function describeIssue(issue, root, depth = 0) {
2705
+ const value = depth === 0 ? valueAtPath(root, issue.path) : void 0;
2706
+ const seen = depth > 0 || issue.code === "custom" || issue.message.includes("received ") ? "" : value === void 0 ? " (nothing is set there)" : ` (received ${preview(value)})`;
2707
+ const armIssues = issue.code === "invalid_union" ? issue.errors : void 0;
2708
+ if (!armIssues || armIssues.length === 0) {
2709
+ return `${issue.message}${seen}`;
2710
+ }
2711
+ const arms = armIssues.map(
2712
+ (arm) => arm.map((inner) => {
2713
+ const where = inner.path.length ? `${inner.path.join(".")} \u2014 ` : "";
2714
+ return `${where}${describeIssue(inner, root, depth + 1)}`;
2715
+ }).join("; ")
2716
+ ).filter((text) => text.length > 0);
2717
+ if (arms.length === 0) return `${issue.message}${seen}`;
2718
+ return `${issue.message}${seen} \u2014 no accepted form matched: ` + arms.map((text, i) => `(${i + 1}) ${text}`).join(" ");
2719
+ }
2720
+
2563
2721
  // src/validate-react-page-props.ts
2564
2722
  var import_meta2 = {};
2565
2723
  var cachedTs = null;
@@ -2665,35 +2823,78 @@ function filterAttrValue(tsc, sf, attr) {
2665
2823
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
2666
2824
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
2667
2825
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
2668
- var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
2669
- var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2826
+ var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
2827
+ function checkChartDrillDown(raw, push2) {
2828
+ if (raw === void 0 || raw === NOT_STATIC) return;
2829
+ if (!isRec7(raw)) {
2830
+ push2(
2831
+ "error",
2832
+ REACT_CHART_DRILLDOWN_INVALID,
2833
+ `drillDown must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
2834
+ "Write drillDown={{ \u2026 }} \u2014 or, to turn the drill on with all defaults, drillDown={{}}. Omit the prop entirely to leave drill off."
2835
+ );
2836
+ return;
2837
+ }
2838
+ const parsed = import_ui2.ChartDrillDownSchema.safeParse(raw);
2839
+ if (parsed.success) return;
2840
+ for (const issue of parsed.error.issues) {
2841
+ const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
2842
+ push2(
2843
+ "error",
2844
+ REACT_CHART_DRILLDOWN_INVALID,
2845
+ `${at}: ${issue.message}`,
2846
+ "The drill config is declared by ChartDrillDownSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
2847
+ );
2848
+ }
2849
+ }
2850
+ function checkChartAggregate(raw, push2) {
2851
+ if (raw === void 0 || raw === NOT_STATIC) return;
2852
+ if (!isRec7(raw)) {
2853
+ push2(
2854
+ "error",
2855
+ REACT_CHART_AGGREGATE_INVALID,
2856
+ `aggregate must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
2857
+ 'Write aggregate={{ function: "count", groupBy: "<field>" }} \u2014 or bind data={\u2026} instead to chart precomputed rows.'
2858
+ );
2859
+ return;
2860
+ }
2861
+ const groupByAbsent = raw.groupBy === void 0;
2862
+ if (groupByAbsent) {
2863
+ push2(
2864
+ "warning",
2865
+ REACT_CHART_AGGREGATE_INVALID,
2866
+ "aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
2867
+ "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. Deliberate single-value charts are tolerated at warning level for now: ChartAggregateSchema declares groupBy required while ObjectChart honours its absence by falling back to xAxisKey \u2014 objectstack#5583 decides which of the two moves."
2868
+ );
2869
+ }
2870
+ const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
2871
+ if (parsed.success) return;
2872
+ for (const issue of parsed.error.issues) {
2873
+ if (groupByAbsent && issue.path[0] === "groupBy") continue;
2874
+ const at = issue.path.length ? `aggregate.${issue.path.join(".")}` : "aggregate";
2875
+ push2(
2876
+ "error",
2877
+ REACT_CHART_AGGREGATE_INVALID,
2878
+ `${at}: ${describeIssue(issue, raw)}`,
2879
+ "The aggregate is declared by ChartAggregateSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
2880
+ );
2881
+ }
2882
+ }
2883
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2670
2884
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
2671
2885
  function checkObjectChart(attrs, objectFields, findings) {
2672
2886
  const { values, where, path } = attrs;
2673
2887
  const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2888
+ checkChartDrillDown(values.get("drillDown"), push2);
2674
2889
  if (values.has("data")) return;
2675
2890
  const aggregate = values.get("aggregate");
2891
+ checkChartAggregate(aggregate, push2);
2676
2892
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
2677
- if (!isRec6(aggregate)) return;
2893
+ if (!isRec7(aggregate)) return;
2678
2894
  const fn = strOf(aggregate.function);
2679
2895
  const field = strOf(aggregate.field);
2680
2896
  const groupBy = aggregate.groupBy;
2681
- const groupByField = strOf(groupBy) ?? (isRec6(groupBy) ? strOf(groupBy.field) : void 0);
2682
- if (fn && !CHART_FUNCTIONS.includes(fn)) {
2683
- push2(
2684
- "error",
2685
- REACT_CHART_AGGREGATE_INVALID,
2686
- `aggregate.function "${fn}" is not an aggregation this chart can run.`,
2687
- `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
2688
- );
2689
- } else if (fn && fn !== "count" && !field) {
2690
- push2(
2691
- "error",
2692
- REACT_CHART_AGGREGATE_INVALID,
2693
- `aggregate.function "${fn}" has no "field" to aggregate.`,
2694
- 'Add aggregate.field, or use function "count" (the only one that may omit it).'
2695
- );
2696
- }
2897
+ const groupByField = strOf(groupBy) ?? (isRec7(groupBy) ? strOf(groupBy.field) : void 0);
2697
2898
  const objectName = strOf(values.get("objectName"));
2698
2899
  const known = objectName ? objectFields.get(objectName) : void 0;
2699
2900
  if (objectName && known) {
@@ -2726,18 +2927,18 @@ function checkObjectChart(attrs, objectFields, findings) {
2726
2927
  );
2727
2928
  };
2728
2929
  const xAxisRaw = values.get("xAxis");
2729
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec6(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2930
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec7(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2730
2931
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
2731
2932
  axisRef(categoryAxis, categoryProp);
2732
2933
  const yAxisRaw = values.get("yAxis");
2733
2934
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
2734
2935
  for (const a of yAxisList) {
2735
- axisRef(strOf(a) ?? (isRec6(a) ? strOf(a.field) : void 0), "yAxis[].field");
2936
+ axisRef(strOf(a) ?? (isRec7(a) ? strOf(a.field) : void 0), "yAxis[].field");
2736
2937
  }
2737
2938
  const series = values.get("series");
2738
2939
  if (Array.isArray(series)) {
2739
2940
  for (const s of series) {
2740
- if (!isRec6(s)) continue;
2941
+ if (!isRec7(s)) continue;
2741
2942
  const dataKey = strOf(s.dataKey);
2742
2943
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
2743
2944
  }
@@ -2793,7 +2994,7 @@ function subformFieldRefs(value, basePath) {
2793
2994
  if (!Array.isArray(value)) return { child, parent };
2794
2995
  for (let i = 0; i < value.length; i++) {
2795
2996
  const sub = value[i];
2796
- if (!isRec6(sub)) continue;
2997
+ if (!isRec7(sub)) continue;
2797
2998
  const at = (key) => `${basePath}[${i}].${key}`;
2798
2999
  child.push({
2799
3000
  objectName: strOf(sub.childObject),
@@ -2838,20 +3039,20 @@ function reactFieldRefs(spec, values, basePath) {
2838
3039
  }
2839
3040
  for (const key of spec.nestedFields ?? []) {
2840
3041
  const v = readable(key);
2841
- if (isRec6(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
3042
+ if (isRec7(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2842
3043
  }
2843
3044
  for (const key of spec.sections ?? []) {
2844
3045
  const v = readable(key);
2845
3046
  if (!Array.isArray(v)) continue;
2846
3047
  for (let i = 0; i < v.length; i++) {
2847
3048
  const section = v[i];
2848
- if (!isRec6(section)) continue;
3049
+ if (!isRec7(section)) continue;
2849
3050
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
2850
3051
  }
2851
3052
  }
2852
3053
  for (const key of spec.keyedByField ?? []) {
2853
3054
  const v = readable(key);
2854
- if (!isRec6(v)) continue;
3055
+ if (!isRec7(v)) continue;
2855
3056
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
2856
3057
  }
2857
3058
  for (const key of spec.filterArrays ?? []) {
@@ -3122,7 +3323,7 @@ function validateSemanticRoles(stack) {
3122
3323
  const where = `object "${objName}"`;
3123
3324
  const path = `objects[${i}]`;
3124
3325
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
3125
- const fieldNames = new Set(Object.keys(fields));
3326
+ const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
3126
3327
  const declaredGroups = new Set(
3127
3328
  (Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
3128
3329
  );
@@ -3505,9 +3706,9 @@ function validateCapabilityReferences(stack) {
3505
3706
  }
3506
3707
 
3507
3708
  // src/validate-approval-approvers.ts
3508
- var import_automation3 = require("@objectstack/spec/automation");
3709
+ var import_automation4 = require("@objectstack/spec/automation");
3509
3710
  var import_spec = require("@objectstack/spec");
3510
- var import_formula2 = require("@objectstack/formula");
3711
+ var import_formula3 = require("@objectstack/formula");
3511
3712
  var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
3512
3713
  var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
3513
3714
  var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
@@ -3538,7 +3739,7 @@ function validateApprovalApprovers(stack) {
3538
3739
  const findings = [];
3539
3740
  if (!stack || typeof stack !== "object") return findings;
3540
3741
  const flows = asArray19(stack.flows);
3541
- const validTypes = new Set(import_automation3.ApproverType.options);
3742
+ const validTypes = new Set(import_automation4.ApproverType.options);
3542
3743
  for (let fi = 0; fi < flows.length; fi++) {
3543
3744
  const flow = flows[fi];
3544
3745
  if (!flow || typeof flow !== "object") continue;
@@ -3546,7 +3747,7 @@ function validateApprovalApprovers(stack) {
3546
3747
  const walked = walkFlowNodes(flow, `flows[${fi}]`);
3547
3748
  for (let ni = 0; ni < walked.length; ni++) {
3548
3749
  const { node, path: nodePath } = walked[ni];
3549
- if (!node || node.type !== import_automation3.APPROVAL_NODE_TYPE) continue;
3750
+ if (!node || node.type !== import_automation4.APPROVAL_NODE_TYPE) continue;
3550
3751
  const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
3551
3752
  const cfg = node.config ?? {};
3552
3753
  const approvers = Array.isArray(cfg.approvers) ? cfg.approvers : [];
@@ -3564,12 +3765,12 @@ function validateApprovalApprovers(stack) {
3564
3765
  rule: APPROVAL_APPROVER_TYPE_UNKNOWN,
3565
3766
  where,
3566
3767
  path: `${path}.type`,
3567
- message: `approver type '${type}' is not an ApproverType (${import_automation3.ApproverType.options.join(" | ")}).`,
3768
+ message: `approver type '${type}' is not an ApproverType (${import_automation4.ApproverType.options.join(" | ")}).`,
3568
3769
  hint: fix ? `Use the spec value: { type: '${fix}', value: '${value}' }.` : `Pick one of the spec values; unmapped types degrade to an inert '${type}:${value}' literal at runtime.`
3569
3770
  });
3570
3771
  continue;
3571
3772
  }
3572
- const canonical = (0, import_automation3.canonicalApproverType)(type);
3773
+ const canonical = (0, import_automation4.canonicalApproverType)(type);
3573
3774
  if (canonical === "expression") {
3574
3775
  const source = value.trim();
3575
3776
  if (!source) {
@@ -3582,7 +3783,7 @@ function validateApprovalApprovers(stack) {
3582
3783
  hint: `Write a CEL expression over current.* (the record's live state at node entry), trigger.* (the submit-time snapshot) or vars.* (flow variables), e.g. current.approvers_dynamic or vars.approval_lead.picked_departments.`
3583
3784
  });
3584
3785
  } else {
3585
- const parsed = (0, import_formula2.collectCelRootIdentifiers)(source);
3786
+ const parsed = (0, import_formula3.collectCelRootIdentifiers)(source);
3586
3787
  if (!parsed.ok) {
3587
3788
  findings.push({
3588
3789
  severity: "error",
@@ -3626,8 +3827,8 @@ function validateApprovalApprovers(stack) {
3626
3827
  message: `approver { type: '${type}', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: ${MEMBERSHIP_TIER_LIST}) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
3627
3828
  hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type 'org_membership_level' only for membership tiers (${MEMBERSHIP_TIER_LIST}) \u2014 the vocabulary is closed (ADR-0108), so a business role is always a position.`
3628
3829
  });
3629
- } else if (type in import_automation3.DEPRECATED_APPROVER_TYPES) {
3630
- const fix = (0, import_automation3.canonicalApproverType)(type);
3830
+ } else if (type in import_automation4.DEPRECATED_APPROVER_TYPES) {
3831
+ const fix = (0, import_automation4.canonicalApproverType)(type);
3631
3832
  findings.push({
3632
3833
  severity: "warning",
3633
3834
  rule: APPROVAL_APPROVER_TYPE_DEPRECATED,
@@ -3636,7 +3837,7 @@ function validateApprovalApprovers(stack) {
3636
3837
  message: `approver type '${type}' is the deprecated spelling of '${fix}' (ADR-0090 D3) and is removed in the next major.`,
3637
3838
  hint: `Author { type: '${fix}', value: '${value}' }. It resolves identically today.`
3638
3839
  });
3639
- } else if (import_automation3.APPROVER_VALUE_BINDINGS[canonical]?.source === "unsupported") {
3840
+ } else if (import_automation4.APPROVER_VALUE_BINDINGS[canonical]?.source === "unsupported") {
3640
3841
  findings.push({
3641
3842
  severity: "warning",
3642
3843
  rule: APPROVAL_APPROVER_TYPE_UNSUPPORTED,
@@ -3647,7 +3848,7 @@ function validateApprovalApprovers(stack) {
3647
3848
  });
3648
3849
  }
3649
3850
  const declaredOrg = a.organization;
3650
- if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" && import_automation3.ApproverType.options.includes(canonical) && !(0, import_automation3.approverTypeIsOrgScoped)(canonical)) {
3851
+ if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" && import_automation4.ApproverType.options.includes(canonical) && !(0, import_automation4.approverTypeIsOrgScoped)(canonical)) {
3651
3852
  findings.push({
3652
3853
  severity: "error",
3653
3854
  rule: APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED,
@@ -3661,7 +3862,7 @@ function validateApprovalApprovers(stack) {
3661
3862
  const routable = approvers.filter(
3662
3863
  (a) => a && typeof a === "object" && typeof a.type === "string"
3663
3864
  );
3664
- if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0, import_automation3.canonicalApproverType)(String(a.type))))) {
3865
+ if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0, import_automation4.canonicalApproverType)(String(a.type))))) {
3665
3866
  const locks = cfg.lockRecord !== false;
3666
3867
  findings.push({
3667
3868
  severity: "info",
@@ -3673,7 +3874,7 @@ function validateApprovalApprovers(stack) {
3673
3874
  });
3674
3875
  }
3675
3876
  const hasExpression = approvers.some(
3676
- (a) => a && typeof a === "object" && (0, import_automation3.canonicalApproverType)(String(a.type ?? "")) === "expression"
3877
+ (a) => a && typeof a === "object" && (0, import_automation4.canonicalApproverType)(String(a.type ?? "")) === "expression"
3677
3878
  );
3678
3879
  if (hasExpression && cfg.onEmptyApprovers == null) {
3679
3880
  findings.push({
@@ -3685,7 +3886,7 @@ function validateApprovalApprovers(stack) {
3685
3886
  hint: `Declare the empty-slate policy explicitly: onEmptyApprovers: 'admin_rescue' (hold for admin takeover), 'fail' (fail the node \u2014 config bug), or 'auto_approve' (wave through, output.autoApproved = true).`
3686
3887
  });
3687
3888
  }
3688
- const declaredOutputs = (0, import_automation3.normalizeDecisionOutputs)(cfg.decisionOutputs).map((d) => d.key);
3889
+ const declaredOutputs = (0, import_automation4.normalizeDecisionOutputs)(cfg.decisionOutputs).map((d) => d.key);
3689
3890
  const reserved = declaredOutputs.filter((k) => RESERVED_OUTPUT_KEYS.has(k));
3690
3891
  if (reserved.length) {
3691
3892
  findings.push({
@@ -3840,7 +4041,7 @@ function asArray20(v) {
3840
4041
  return [];
3841
4042
  }
3842
4043
  function owdOf(obj) {
3843
- return obj.sharingModel ?? obj.security?.sharingModel;
4044
+ return obj.sharingModel;
3844
4045
  }
3845
4046
  function isSystemObject(obj) {
3846
4047
  return obj.isSystem === true || String(obj.name ?? "").startsWith("sys_");
@@ -3854,7 +4055,7 @@ function labelHasRoleWord(label2) {
3854
4055
  return /\brole(s)?\b/i.test(label2);
3855
4056
  }
3856
4057
  function refOf(def) {
3857
- const r = def.reference ?? def.reference_to;
4058
+ const r = def.reference;
3858
4059
  return typeof r === "string" && r ? r : void 0;
3859
4060
  }
3860
4061
  function firstMasterDetailField(obj) {
@@ -4147,6 +4348,7 @@ function validateSecurityPosture(stack, opts) {
4147
4348
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
4148
4349
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
4149
4350
  var ORG_PARENT_FIELD = "parent_organization_id";
4351
+ var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
4150
4352
  function asArray21(v) {
4151
4353
  if (Array.isArray(v)) return v;
4152
4354
  if (v && typeof v === "object") {
@@ -4157,6 +4359,15 @@ function asArray21(v) {
4157
4359
  function str(v) {
4158
4360
  return typeof v === "string" ? v : "";
4159
4361
  }
4362
+ function expressionText(v) {
4363
+ if (typeof v === "string") return v;
4364
+ if (v && typeof v === "object") {
4365
+ const rec = v;
4366
+ if (typeof rec.source === "string") return rec.source;
4367
+ if (rec.ast !== void 0) return JSON.stringify(rec.ast) ?? "";
4368
+ }
4369
+ return "";
4370
+ }
4160
4371
  function isTenancyDisabled(object) {
4161
4372
  const tenancy = object.tenancy;
4162
4373
  if (tenancy && typeof tenancy === "object" && tenancy.enabled === false) return true;
@@ -4168,7 +4379,7 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
4168
4379
  function validateOrgAxisRedLines(stack) {
4169
4380
  const findings = [];
4170
4381
  const cfg = stack ?? {};
4171
- const permissionSets = asArray21(cfg.permissions ?? cfg.permissionSets);
4382
+ const permissionSets = asArray21(cfg.permissions);
4172
4383
  permissionSets.forEach((ps, psIndex) => {
4173
4384
  asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
4174
4385
  for (const clause of ["using", "check"]) {
@@ -4184,53 +4395,167 @@ function validateOrgAxisRedLines(stack) {
4184
4395
  }
4185
4396
  });
4186
4397
  });
4187
- const objects = asArray21(cfg.objects);
4188
- objects.forEach((object, oIndex) => {
4189
- const objectName = str(object.name) || String(oIndex);
4190
- asArray21(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
4191
- for (const clause of ["using", "check"]) {
4192
- if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
4193
- findings.push({
4194
- severity: "error",
4195
- rule: ORG_AXIS_PERMISSION_INHERITANCE,
4196
- where: `object "${objectName}" policy "${str(policy.name) || pIndex}"`,
4197
- path: `objects[${oIndex}].rowLevelSecurity[${pIndex}].${clause}`,
4198
- message: `RLS ${clause} reads \`${ORG_PARENT_FIELD}\`, which builds a permission hierarchy along the organization axis. ADR-0105 D6 forbids it: the org tree is a REPORTING dimension only.`,
4199
- hint: INHERITANCE_HINT
4200
- });
4201
- }
4202
- });
4203
- });
4204
- asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
4205
- const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
4206
- const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
4207
- if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
4398
+ asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
4399
+ const slots = [
4400
+ { key: "condition", text: expressionText(rule.condition) },
4401
+ { key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
4402
+ ];
4403
+ for (const slot of slots) {
4404
+ if (!slot.text.includes(ORG_PARENT_FIELD)) continue;
4208
4405
  findings.push({
4209
4406
  severity: "error",
4210
4407
  rule: ORG_AXIS_PERMISSION_INHERITANCE,
4211
4408
  where: `sharing rule "${str(rule.name) || rIndex}"`,
4212
- path: `sharingRules[${rIndex}]`,
4213
- message: `Sharing rule reads \`${ORG_PARENT_FIELD}\`, granting access by walking the organization tree. ADR-0105 D6 forbids permission inheritance along the org axis.`,
4409
+ path: `sharingRules[${rIndex}].${slot.key}`,
4410
+ message: `Sharing rule ${slot.key} reads \`${ORG_PARENT_FIELD}\`, granting access by walking the organization tree. ADR-0105 D6 forbids permission inheritance along the org axis.`,
4214
4411
  hint: INHERITANCE_HINT
4215
4412
  });
4216
4413
  }
4217
4414
  });
4218
4415
  const tenancyDisabledObjects = new Set(
4219
- objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
4416
+ asArray21(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
4220
4417
  );
4221
- asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
4222
- const target = str(rule.object ?? rule.objectName);
4418
+ asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
4419
+ const target = str(rule.object);
4223
4420
  if (!target || !tenancyDisabledObjects.has(target)) return;
4224
- const sharedTo = rule.sharedTo ?? rule.recipient;
4225
- const recipientType = str(sharedTo?.type);
4226
- if (recipientType !== "business_unit") return;
4421
+ const sharedWith = rule.sharedWith;
4422
+ const recipientType = str(sharedWith?.type);
4423
+ if (!BU_TREE_RECIPIENT_TYPES.has(recipientType)) return;
4424
+ const reach = recipientType === "unit_and_subordinates" ? "a business unit AND every descendant unit" : "a business unit";
4227
4425
  findings.push({
4228
4426
  severity: "error",
4229
4427
  rule: ORG_AXIS_CROSS_ORG_BU_GRANT,
4230
4428
  where: `sharing rule "${str(rule.name) || rIndex}" on object "${target}"`,
4231
- path: `sharingRules[${rIndex}].sharedTo`,
4232
- message: `A business-unit sharing rule targets "${target}", which opted out of tenancy (\`tenancy.enabled: false\`). Platform-global objects carry no organization column, so this grant spans EVERY organization \u2014 a cross-organization business-unit grant, which ADR-0105 D6 forbids (BU trees are org-internal).`,
4233
- hint: `Either scope the object to organizations (drop \`tenancy.enabled: false\` so Layer 0 walls it), or share it to a position / permission-set audience instead of a business unit. A platform-global catalog that everyone should read wants an OWD of \`public_read\`, not a BU grant.`
4429
+ path: `sharingRules[${rIndex}].sharedWith`,
4430
+ message: `Sharing rule recipient \`${recipientType}\` (${reach}) targets "${target}", which opted out of tenancy (\`tenancy.enabled: false\`). Platform-global objects carry no organization column, so this grant spans EVERY organization \u2014 a cross-organization business-unit grant, which ADR-0105 D6 forbids (BU trees are org-internal).`,
4431
+ hint: `Either scope the object to organizations (drop \`tenancy.enabled: false\` so Layer 0 walls it), or share it to a \`user\` / \`team\` / \`position\` audience instead \u2014 those expand flat, with no business-unit tree to resolve. A platform-global catalog that everyone should read wants an OWD of \`public_read\`, not a BU grant.`
4432
+ });
4433
+ });
4434
+ return findings;
4435
+ }
4436
+
4437
+ // src/validate-sharing-rule-enforceability.ts
4438
+ var import_formula4 = require("@objectstack/formula");
4439
+ var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
4440
+ var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
4441
+ function asArray22(v) {
4442
+ if (Array.isArray(v)) return v;
4443
+ if (v && typeof v === "object") {
4444
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4445
+ }
4446
+ return [];
4447
+ }
4448
+ function str2(v) {
4449
+ return typeof v === "string" ? v : "";
4450
+ }
4451
+ function toCompilerInput(condition) {
4452
+ if (typeof condition === "string") return condition.trim() ? condition : null;
4453
+ if (condition && typeof condition === "object") {
4454
+ const source = condition.source;
4455
+ if (typeof source === "string" && source.trim()) return { source };
4456
+ }
4457
+ return null;
4458
+ }
4459
+ function sourceOf(condition) {
4460
+ const input = toCompilerInput(condition);
4461
+ if (typeof input === "string") return input;
4462
+ return str2(input?.source);
4463
+ }
4464
+ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column `record.<field>` paths (ADR-0058 D2).";
4465
+ function validateSharingRuleEnforceability(stack) {
4466
+ const findings = [];
4467
+ const cfg = stack ?? {};
4468
+ asArray22(cfg.sharingRules).forEach((rule, index) => {
4469
+ const input = toCompilerInput(rule.condition);
4470
+ if (input === null) return;
4471
+ const result = (0, import_formula4.compileCelToFilter)(input, { variables: {} });
4472
+ if (result.ok) return;
4473
+ if (result.reason === "parse-error") return;
4474
+ const name = str2(rule.name) || String(index);
4475
+ const object = str2(rule.object);
4476
+ const where = `sharing rule "${name}"${object ? ` on object "${object}"` : ""}`;
4477
+ const path = `sharingRules[${index}].condition`;
4478
+ const source = sourceOf(rule.condition);
4479
+ const skipped = "so `bootstrapDeclaredSharingRules` SKIPS the rule at boot: it is never written to `sys_sharing_rule`, no `sys_record_share` grant is ever materialised, and the only signal is one WARN line in the boot log. The rule is declared and grants nothing (ADR-0049: an unlowerable condition is never seeded as a permissive match-all).";
4480
+ if (result.reason === "unresolved-variable") {
4481
+ findings.push({
4482
+ severity: "error",
4483
+ rule: SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
4484
+ where,
4485
+ path,
4486
+ message: `Sharing-rule condition \`${source}\` reads a runtime variable (${result.detail}), ` + skipped,
4487
+ hint: "A criteria sharing rule is MATERIALISED: the seeder compiles ONE static `criteria_json` per rule and the evaluator writes `sys_record_share` rows from it, so there is no \"current user\" for the condition to read. Express per-user access with the mechanism that runs per request instead \u2014 an RLS policy on a permission set (`rowLevelSecurity[].using`, where `current_user.*` IS resolved), or the record-ownership path. Keep this rule for the part of the predicate that is a property of the RECORD (e.g. `record.stage == 'closed_won'`) and name the audience through `sharedWith`."
4488
+ });
4489
+ return;
4490
+ }
4491
+ findings.push({
4492
+ severity: "error",
4493
+ rule: SHARING_RULE_UNLOWERABLE_CONDITION,
4494
+ where,
4495
+ path,
4496
+ message: `Sharing-rule condition \`${source}\` is outside the pushdown subset the runtime can compile (${result.detail}), ` + skipped,
4497
+ hint: "Rewrite the predicate inside the lowerable subset. " + PUSHDOWN_SUBSET + " Two traps in particular: (1) `has(record.x)` is correct in an object VALIDATION rule, which is INTERPRETED, and wrong here, where the condition is COMPILED \u2014 write the null test as `record.x != null`; (2) a related-record path (`record.account.region`) is a join, which the compiler refuses by design (ADR-0055) \u2014 denormalise the value onto this object (a formula/rollup field) and test that column, or share the related object instead."
4498
+ });
4499
+ });
4500
+ return findings;
4501
+ }
4502
+
4503
+ // src/validate-rls-predicate-enforceability.ts
4504
+ var import_formula5 = require("@objectstack/formula");
4505
+ var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
4506
+ var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
4507
+ function asArray23(v) {
4508
+ if (Array.isArray(v)) return v;
4509
+ if (v && typeof v === "object") {
4510
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4511
+ }
4512
+ return [];
4513
+ }
4514
+ function str3(v) {
4515
+ return typeof v === "string" ? v : "";
4516
+ }
4517
+ var PUSHDOWN_SUBSET2 = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column field paths (ADR-0058 D2), compared against a literal or a `current_user.*` value.";
4518
+ function consequence(clause) {
4519
+ const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
4520
+ return clause === "using" ? dropped + "When it is the only applicable policy for that object and operation, `compileFilter` returns the `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause: every select / update / delete on the object matches ZERO rows. When other policies also apply, this one just vanishes from the OR and grants none of the access it appears to." : dropped + "On the ADR-0058 D4 write path that leaves the post-image `check` as the `RLS_DENY_FILTER` sentinel, which no record can satisfy: every insert / update the policy governs fails with `PermissionDeniedError`. The policy reads as a write rule and behaves as a blanket refusal.";
4521
+ }
4522
+ function validateRlsPredicateEnforceability(stack) {
4523
+ const findings = [];
4524
+ const cfg = stack ?? {};
4525
+ asArray23(cfg.permissions).forEach((ps, psIndex) => {
4526
+ asArray23(ps.rowLevelSecurity).forEach((policy, pIndex) => {
4527
+ for (const clause of ["using", "check"]) {
4528
+ const source = str3(policy[clause]);
4529
+ if (!source.trim()) continue;
4530
+ if ((0, import_formula5.isSupportedRlsExpression)(source)) continue;
4531
+ const why = (0, import_formula5.isPushdownableCel)((0, import_formula5.sqlPredicateToCel)(source));
4532
+ const detail = why.ok ? "" : why.detail;
4533
+ const parseError = !why.ok && why.reason === "parse-error";
4534
+ const psName = str3(ps.name) || String(psIndex);
4535
+ const policyName = str3(policy.name) || String(pIndex);
4536
+ const object = str3(policy.object);
4537
+ const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
4538
+ const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
4539
+ if (parseError) {
4540
+ findings.push({
4541
+ severity: "error",
4542
+ rule: RLS_PREDICATE_UNPARSEABLE,
4543
+ where,
4544
+ path,
4545
+ message: `RLS ${clause} \`${source}\` does not parse as CEL even after the legacy SQL bridge (\`=\` \u2192 \`==\`, \`IN\` \u2192 \`in\`) has been applied (${detail}), ` + consequence(clause),
4546
+ hint: "Author the predicate in canonical CEL (ADR-0058 D1). The bridge covers only the historic SQL subset \u2014 a bare `=` and `IN` \u2014 so everything else must already be CEL: combine with `&&` / `||` rather than SQL `AND` / `OR`, negate with `!`, and use `startsWith` / `endsWith` / `contains` rather than `LIKE`. A subquery has no CEL spelling at all: RLS cannot join (ADR-0055), so pre-resolve the set into a membership key the runtime exposes (`field in current_user.<key>`, ADR-0105 D11) or denormalise the value onto this object."
4547
+ });
4548
+ continue;
4549
+ }
4550
+ findings.push({
4551
+ severity: "error",
4552
+ rule: RLS_PREDICATE_UNENFORCEABLE,
4553
+ where,
4554
+ path,
4555
+ message: `RLS ${clause} \`${source}\` is outside the pushdown subset the runtime can compile (${detail}), ` + consequence(clause),
4556
+ hint: "Rewrite the predicate inside the lowerable subset. " + PUSHDOWN_SUBSET2 + " Three traps in particular: (1) a function call \u2014 `size(record.tags) > 0`, `has(record.x)` \u2014 is correct in an object VALIDATION rule, which is INTERPRETED, and wrong here, where the predicate is COMPILED to a filter; write the null test as `field != null`. (2) A related-record path (`record.account.region`) is a join, which the compiler refuses by design (ADR-0055) \u2014 denormalise the value onto this object (a formula/rollup field) and test that column. (3) Arithmetic on a column (`amount * 2 > 100`) never lowers \u2014 precompute it into a field, or compare the column against the literal directly."
4557
+ });
4558
+ }
4234
4559
  });
4235
4560
  });
4236
4561
  return findings;
@@ -4239,7 +4564,7 @@ function validateOrgAxisRedLines(stack) {
4239
4564
  // src/validate-dashboard-action-refs.ts
4240
4565
  var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
4241
4566
  var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
4242
- function asArray22(v) {
4567
+ function asArray24(v) {
4243
4568
  if (Array.isArray(v)) return v;
4244
4569
  if (v && typeof v === "object") {
4245
4570
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4273,14 +4598,14 @@ function collectKnownTargets(stack) {
4273
4598
  const pages = /* @__PURE__ */ new Set();
4274
4599
  const views = /* @__PURE__ */ new Set();
4275
4600
  const collectNames = (v, into, name) => {
4276
- for (const item of asArray22(v)) {
4601
+ for (const item of asArray24(v)) {
4277
4602
  if (!item || typeof item !== "object") continue;
4278
4603
  const n = name(item);
4279
4604
  if (n) into.add(n);
4280
4605
  }
4281
4606
  };
4282
4607
  collectNames(stack.actions, actions, (a) => strName5(a.name));
4283
- for (const obj of asArray22(stack.objects)) {
4608
+ for (const obj of asArray24(stack.objects)) {
4284
4609
  if (!obj || typeof obj !== "object") continue;
4285
4610
  const n = strName5(obj.name);
4286
4611
  if (n) objects.add(n);
@@ -4320,7 +4645,7 @@ function resolveUrlRoute(target, known) {
4320
4645
  function validateDashboardActionRefs(stack) {
4321
4646
  const findings = [];
4322
4647
  if (!stack || typeof stack !== "object") return findings;
4323
- const dashboards = asArray22(stack.dashboards);
4648
+ const dashboards = asArray24(stack.dashboards);
4324
4649
  if (dashboards.length === 0) return findings;
4325
4650
  const known = collectKnownTargets(stack);
4326
4651
  const checkOne = (action, where, path) => {
@@ -4360,7 +4685,7 @@ function validateDashboardActionRefs(stack) {
4360
4685
  if (!dash || typeof dash !== "object") continue;
4361
4686
  const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
4362
4687
  const dashPath = `dashboards[${di}]`;
4363
- const headerActions = asArray22(dash.header?.actions);
4688
+ const headerActions = asArray24(dash.header?.actions);
4364
4689
  for (let ai = 0; ai < headerActions.length; ai++) {
4365
4690
  const action = headerActions[ai];
4366
4691
  if (!action || typeof action !== "object") continue;
@@ -4371,18 +4696,6 @@ function validateDashboardActionRefs(stack) {
4371
4696
  `${dashPath}.header.actions[${ai}].actionUrl`
4372
4697
  );
4373
4698
  }
4374
- const widgets = asArray22(dash.widgets);
4375
- for (let wi = 0; wi < widgets.length; wi++) {
4376
- const widget = widgets[wi];
4377
- if (!widget || typeof widget !== "object") continue;
4378
- if (!strName5(widget.actionUrl)) continue;
4379
- const widgetId = strName5(widget.id) ?? `#${wi}`;
4380
- checkOne(
4381
- { actionType: widget.actionType, actionUrl: widget.actionUrl },
4382
- `dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
4383
- `${dashPath}.widgets[${wi}].actionUrl`
4384
- );
4385
- }
4386
4699
  }
4387
4700
  return findings;
4388
4701
  }
@@ -4391,7 +4704,7 @@ function validateDashboardActionRefs(stack) {
4391
4704
  var import_data6 = require("@objectstack/spec/data");
4392
4705
  var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
4393
4706
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
4394
- function asArray23(v) {
4707
+ function asArray25(v) {
4395
4708
  if (Array.isArray(v)) return v;
4396
4709
  if (v && typeof v === "object") {
4397
4710
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4460,7 +4773,7 @@ function validateFilterTokens(stack) {
4460
4773
  ["apps", "app"]
4461
4774
  ];
4462
4775
  for (const [key, kind] of surfaces) {
4463
- const items = asArray23(stack[key]);
4776
+ const items = asArray25(stack[key]);
4464
4777
  items.forEach((item, i) => {
4465
4778
  const name = label(item.name ?? item.id, `#${i}`);
4466
4779
  if (kind === "dashboard") {
@@ -4490,7 +4803,7 @@ var import_system2 = require("@objectstack/spec/system");
4490
4803
  var PLATFORM_NAMES = [...import_system2.PLATFORM_PROVIDED_OBJECT_NAMES];
4491
4804
  var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
4492
4805
  var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
4493
- function asArray24(v) {
4806
+ function asArray26(v) {
4494
4807
  if (Array.isArray(v)) return v;
4495
4808
  if (v && typeof v === "object") {
4496
4809
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4537,7 +4850,7 @@ function distance2(a, b) {
4537
4850
  function validateObjectReferences(stack) {
4538
4851
  const findings = [];
4539
4852
  if (!stack || typeof stack !== "object") return findings;
4540
- const objects = asArray24(stack.objects);
4853
+ const objects = asArray26(stack.objects);
4541
4854
  const ownObjects = /* @__PURE__ */ new Set();
4542
4855
  for (const obj of objects) {
4543
4856
  const n = strName6(obj.name);
@@ -4570,7 +4883,7 @@ function validateObjectReferences(stack) {
4570
4883
  });
4571
4884
  };
4572
4885
  const checkActionParams2 = (action, actionPath, actionLabel) => {
4573
- const params = asArray24(action.params);
4886
+ const params = asArray26(action.params);
4574
4887
  for (let pi = 0; pi < params.length; pi++) {
4575
4888
  const param = params[pi];
4576
4889
  if (!param || typeof param !== "object") continue;
@@ -4592,7 +4905,7 @@ function validateObjectReferences(stack) {
4592
4905
  );
4593
4906
  }
4594
4907
  };
4595
- const globalActions = asArray24(stack.actions);
4908
+ const globalActions = asArray26(stack.actions);
4596
4909
  for (let ai = 0; ai < globalActions.length; ai++) {
4597
4910
  const action = globalActions[ai];
4598
4911
  if (!action || typeof action !== "object") continue;
@@ -4602,7 +4915,7 @@ function validateObjectReferences(stack) {
4602
4915
  const obj = objects[oi];
4603
4916
  if (!obj || typeof obj !== "object") continue;
4604
4917
  const objName = strName6(obj.name) ?? `#${oi}`;
4605
- const objActions = asArray24(obj.actions);
4918
+ const objActions = asArray26(obj.actions);
4606
4919
  for (let ai = 0; ai < objActions.length; ai++) {
4607
4920
  const action = objActions[ai];
4608
4921
  if (!action || typeof action !== "object") continue;
@@ -4613,12 +4926,12 @@ function validateObjectReferences(stack) {
4613
4926
  );
4614
4927
  }
4615
4928
  }
4616
- const dashboards = asArray24(stack.dashboards);
4929
+ const dashboards = asArray26(stack.dashboards);
4617
4930
  for (let di = 0; di < dashboards.length; di++) {
4618
4931
  const dash = dashboards[di];
4619
4932
  if (!dash || typeof dash !== "object") continue;
4620
4933
  const dashName = strName6(dash.name) ?? `#${di}`;
4621
- const filters = asArray24(dash.globalFilters);
4934
+ const filters = asArray26(dash.globalFilters);
4622
4935
  for (let fi = 0; fi < filters.length; fi++) {
4623
4936
  const filter = filters[fi];
4624
4937
  if (!filter || typeof filter !== "object") continue;
@@ -4633,13 +4946,13 @@ function validateObjectReferences(stack) {
4633
4946
  );
4634
4947
  }
4635
4948
  }
4636
- const apps = asArray24(stack.apps);
4949
+ const apps = asArray26(stack.apps);
4637
4950
  for (let ai = 0; ai < apps.length; ai++) {
4638
4951
  const app = apps[ai];
4639
4952
  if (!app || typeof app !== "object") continue;
4640
4953
  const appName = strName6(app.name) ?? `#${ai}`;
4641
4954
  const walkNav = (items, basePath) => {
4642
- const navItems = asArray24(items);
4955
+ const navItems = asArray26(items);
4643
4956
  for (let ni = 0; ni < navItems.length; ni++) {
4644
4957
  const nav = navItems[ni];
4645
4958
  if (!nav || typeof nav !== "object") continue;
@@ -4666,7 +4979,7 @@ function validateObjectReferences(stack) {
4666
4979
  }
4667
4980
  };
4668
4981
  walkNav(app.navigation, `apps[${ai}].navigation`);
4669
- const areas = asArray24(app.areas);
4982
+ const areas = asArray26(app.areas);
4670
4983
  for (let ri = 0; ri < areas.length; ri++) {
4671
4984
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4672
4985
  }
@@ -4676,10 +4989,10 @@ function validateObjectReferences(stack) {
4676
4989
 
4677
4990
  // src/validate-nav-target-refs.ts
4678
4991
  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 });
4992
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4993
+ function asArray27(v) {
4994
+ if (Array.isArray(v)) return v.filter(isRec8);
4995
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
4683
4996
  return [];
4684
4997
  }
4685
4998
  function strName7(v) {
@@ -4693,7 +5006,7 @@ var NAV_TARGETS = [
4693
5006
  ];
4694
5007
  function namesOf(collection) {
4695
5008
  const out = /* @__PURE__ */ new Set();
4696
- for (const entry of asArray25(collection)) {
5009
+ for (const entry of asArray27(collection)) {
4697
5010
  const n = strName7(entry.name);
4698
5011
  if (n) out.add(n);
4699
5012
  }
@@ -4701,8 +5014,8 @@ function namesOf(collection) {
4701
5014
  }
4702
5015
  function validateNavTargetRefs(stack) {
4703
5016
  const findings = [];
4704
- if (!isRec7(stack)) return findings;
4705
- const apps = asArray25(stack.apps);
5017
+ if (!isRec8(stack)) return findings;
5018
+ const apps = asArray27(stack.apps);
4706
5019
  if (apps.length === 0) return findings;
4707
5020
  const declared = /* @__PURE__ */ new Map();
4708
5021
  for (const [, , collection] of NAV_TARGETS) {
@@ -4713,7 +5026,7 @@ function validateNavTargetRefs(stack) {
4713
5026
  const walk = (items, basePath) => {
4714
5027
  if (!Array.isArray(items)) return;
4715
5028
  for (const [ni, raw] of items.entries()) {
4716
- if (!isRec7(raw)) continue;
5029
+ if (!isRec8(raw)) continue;
4717
5030
  const nav = raw;
4718
5031
  const navPath = `${basePath}[${ni}]`;
4719
5032
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
@@ -4736,7 +5049,7 @@ function validateNavTargetRefs(stack) {
4736
5049
  }
4737
5050
  };
4738
5051
  walk(app.navigation, `apps[${ai}].navigation`);
4739
- for (const [ari, area] of asArray25(app.areas).entries()) {
5052
+ for (const [ari, area] of asArray27(app.areas).entries()) {
4740
5053
  walk(area.items, `apps[${ai}].areas[${ari}].items`);
4741
5054
  walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
4742
5055
  }
@@ -4746,7 +5059,7 @@ function validateNavTargetRefs(stack) {
4746
5059
 
4747
5060
  // src/validate-action-name-refs.ts
4748
5061
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
4749
- function asArray26(v) {
5062
+ function asArray28(v) {
4750
5063
  if (Array.isArray(v)) return v;
4751
5064
  if (v && typeof v === "object") {
4752
5065
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4790,13 +5103,13 @@ function suggest4(target, known) {
4790
5103
  }
4791
5104
  function collectActionNames(stack) {
4792
5105
  const names = /* @__PURE__ */ new Set();
4793
- for (const action of asArray26(stack.actions)) {
5106
+ for (const action of asArray28(stack.actions)) {
4794
5107
  const n = strName8(action?.name);
4795
5108
  if (n) names.add(n);
4796
5109
  }
4797
- for (const obj of asArray26(stack.objects)) {
5110
+ for (const obj of asArray28(stack.objects)) {
4798
5111
  if (!obj || typeof obj !== "object") continue;
4799
- for (const action of asArray26(obj.actions)) {
5112
+ for (const action of asArray28(obj.actions)) {
4800
5113
  const n = strName8(action?.name);
4801
5114
  if (n) names.add(n);
4802
5115
  }
@@ -4851,7 +5164,7 @@ function validateActionNameRefs(stack) {
4851
5164
  );
4852
5165
  }
4853
5166
  };
4854
- const views = asArray26(stack.views);
5167
+ const views = asArray28(stack.views);
4855
5168
  for (let vi = 0; vi < views.length; vi++) {
4856
5169
  const view = views[vi];
4857
5170
  if (!view || typeof view !== "object") continue;
@@ -4865,7 +5178,7 @@ function validateActionNameRefs(stack) {
4865
5178
  }
4866
5179
  }
4867
5180
  }
4868
- const objects = asArray26(stack.objects);
5181
+ const objects = asArray28(stack.objects);
4869
5182
  for (let oi = 0; oi < objects.length; oi++) {
4870
5183
  const obj = objects[oi];
4871
5184
  if (!obj || typeof obj !== "object") continue;
@@ -4876,7 +5189,7 @@ function validateActionNameRefs(stack) {
4876
5189
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
4877
5190
  }
4878
5191
  }
4879
- const pages = asArray26(stack.pages);
5192
+ const pages = asArray28(stack.pages);
4880
5193
  for (let pi = 0; pi < pages.length; pi++) {
4881
5194
  const page = pages[pi];
4882
5195
  if (!page || typeof page !== "object") continue;
@@ -4895,13 +5208,13 @@ function validateActionNameRefs(stack) {
4895
5208
  }
4896
5209
  }
4897
5210
  }
4898
- const apps = asArray26(stack.apps);
5211
+ const apps = asArray28(stack.apps);
4899
5212
  for (let ai = 0; ai < apps.length; ai++) {
4900
5213
  const app = apps[ai];
4901
5214
  if (!app || typeof app !== "object") continue;
4902
5215
  const appName = strName8(app.name) ?? `#${ai}`;
4903
5216
  const walkNav = (items, basePath) => {
4904
- const navItems = asArray26(items);
5217
+ const navItems = asArray28(items);
4905
5218
  for (let ni = 0; ni < navItems.length; ni++) {
4906
5219
  const nav = navItems[ni];
4907
5220
  if (!nav || typeof nav !== "object") continue;
@@ -4920,7 +5233,7 @@ function validateActionNameRefs(stack) {
4920
5233
  }
4921
5234
  };
4922
5235
  walkNav(app.navigation, `apps[${ai}].navigation`);
4923
- const areas = asArray26(app.areas);
5236
+ const areas = asArray28(app.areas);
4924
5237
  for (let ri = 0; ri < areas.length; ri++) {
4925
5238
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4926
5239
  }
@@ -4930,7 +5243,7 @@ function validateActionNameRefs(stack) {
4930
5243
 
4931
5244
  // src/validate-action-locations.ts
4932
5245
  var ACTION_NO_PLACEMENT = "action-no-placement";
4933
- function asArray27(v) {
5246
+ function asArray29(v) {
4934
5247
  if (Array.isArray(v)) return v;
4935
5248
  if (v && typeof v === "object") {
4936
5249
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4951,7 +5264,7 @@ function collectNamePlacedActions(stack) {
4951
5264
  for (const key of ["rowActions", "bulkActions"]) {
4952
5265
  for (const n of strList2(list3[key])) placed.add(n);
4953
5266
  }
4954
- for (const def of asArray27(list3.bulkActionDefs)) {
5267
+ for (const def of asArray29(list3.bulkActionDefs)) {
4955
5268
  const n = strName9(def?.name);
4956
5269
  if (n) placed.add(n);
4957
5270
  }
@@ -4960,12 +5273,12 @@ function collectNamePlacedActions(stack) {
4960
5273
  if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
4961
5274
  for (const lv of Object.values(listViews)) harvest(lv);
4962
5275
  };
4963
- for (const view of asArray27(stack.views)) {
5276
+ for (const view of asArray29(stack.views)) {
4964
5277
  if (!view || typeof view !== "object") continue;
4965
5278
  harvest(view.list);
4966
5279
  harvestListViews(view.listViews);
4967
5280
  }
4968
- for (const obj of asArray27(stack.objects)) {
5281
+ for (const obj of asArray29(stack.objects)) {
4969
5282
  if (!obj || typeof obj !== "object") continue;
4970
5283
  harvestListViews(obj.listViews);
4971
5284
  }
@@ -4990,37 +5303,121 @@ function validateActionLocations(stack) {
4990
5303
  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
5304
  });
4992
5305
  };
4993
- const actions = asArray27(stack.actions);
5306
+ const actions = asArray29(stack.actions);
4994
5307
  for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
4995
- const objects = asArray27(stack.objects);
5308
+ const objects = asArray29(stack.objects);
4996
5309
  for (let oi = 0; oi < objects.length; oi++) {
4997
5310
  const obj = objects[oi];
4998
5311
  if (!obj || typeof obj !== "object") continue;
4999
- const own = asArray27(obj.actions);
5312
+ const own = asArray29(obj.actions);
5000
5313
  for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
5001
5314
  }
5002
5315
  return findings;
5003
5316
  }
5004
5317
 
5318
+ // src/validate-component-props.ts
5319
+ var import_ui3 = require("@objectstack/spec/ui");
5320
+ var import_spec2 = require("@objectstack/spec");
5321
+ var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5322
+ var COMPONENT_PROPS_INVALID = "component-props-invalid";
5323
+ function isRec9(v) {
5324
+ return !!v && typeof v === "object" && !Array.isArray(v);
5325
+ }
5326
+ function strName10(v) {
5327
+ return typeof v === "string" && v.length > 0 ? v : void 0;
5328
+ }
5329
+ function asArray30(v) {
5330
+ if (Array.isArray(v)) return v;
5331
+ if (v && typeof v === "object") {
5332
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5333
+ }
5334
+ return [];
5335
+ }
5336
+ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
5337
+ var DATASOURCE_SUPPLIED_PROP = "object";
5338
+ function suppliedByDataSource(issue, component) {
5339
+ if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
5340
+ const dataSource = isRec9(component.dataSource) ? component.dataSource : void 0;
5341
+ return strName10(dataSource?.object) !== void 0;
5342
+ }
5343
+ function validateComponentProps(stack) {
5344
+ const findings = [];
5345
+ if (!isRec9(stack)) return findings;
5346
+ const pages = asArray30(stack.pages);
5347
+ for (let pi = 0; pi < pages.length; pi++) {
5348
+ const page = pages[pi];
5349
+ if (!isRec9(page)) continue;
5350
+ const pageName = strName10(page.name) ?? `#${pi}`;
5351
+ for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5352
+ const type = strName10(component.type);
5353
+ if (!type) continue;
5354
+ const schema = PROPS_SCHEMAS[type];
5355
+ if (!schema) continue;
5356
+ const props = isRec9(component.properties) ? component.properties : void 0;
5357
+ if (!props) continue;
5358
+ const where = `page "${pageName}" \xB7 ${type}`;
5359
+ const base = `${path}.properties`;
5360
+ for (const f of (0, import_spec2.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
5361
+ findings.push({
5362
+ severity: "warning",
5363
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
5364
+ where,
5365
+ path: f.path,
5366
+ message: `\`${f.key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui), so nothing verifies it: \`properties\` is an untyped bag, the renderer spreads whatever it carries, and a key it does not read is ignored in silence.` + (f.suggestion ? ` Did you mean \`${f.suggestion}\`?` : ""),
5367
+ hint: f.guidance ?? (f.suggestion ? `Rename \`${f.key}\` \u2192 \`${f.suggestion}\`.` : `Remove \`${f.key}\`, or \u2014 if the component really does honour it \u2014 declare it on \`${type}\`'s props schema so the declaration and the renderer agree.`)
5368
+ });
5369
+ }
5370
+ const parsed = schema.safeParse(props);
5371
+ if (parsed.success) continue;
5372
+ for (const issue of parsed.error?.issues ?? []) {
5373
+ if (suppliedByDataSource(issue, component)) continue;
5374
+ const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
5375
+ if (issue.code === "unrecognized_keys") {
5376
+ for (const key of issue.keys ?? []) {
5377
+ findings.push({
5378
+ severity: "warning",
5379
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
5380
+ where,
5381
+ path: `${at}.${key}`,
5382
+ message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${issue.message}`,
5383
+ hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
5384
+ });
5385
+ }
5386
+ continue;
5387
+ }
5388
+ findings.push({
5389
+ severity: "warning",
5390
+ rule: COMPONENT_PROPS_INVALID,
5391
+ where,
5392
+ path: at,
5393
+ message: `${at.slice(base.length + 1) || "properties"}: ${describeIssue(issue, props)}`,
5394
+ hint: `\`${type}\`'s props are declared by ComponentPropsMap (@objectstack/spec/ui) \u2014 the rejection above carries the fix. Advisory for now: the props bag is not parsed on the storage path either, so nothing rejects this today (objectstack#5068).`
5395
+ });
5396
+ }
5397
+ }
5398
+ }
5399
+ return findings;
5400
+ }
5401
+
5005
5402
  // src/validate-chart-bindings.ts
5006
5403
  var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
5007
5404
  var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
5008
5405
  var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
5009
5406
  var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
5010
- function asArray28(v) {
5407
+ function asArray31(v) {
5011
5408
  if (Array.isArray(v)) return v;
5012
5409
  if (v && typeof v === "object") {
5013
5410
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5014
5411
  }
5015
5412
  return [];
5016
5413
  }
5017
- function strName10(v) {
5414
+ function strName11(v) {
5018
5415
  return typeof v === "string" && v.length > 0 ? v : void 0;
5019
5416
  }
5020
5417
  function strList3(v) {
5021
5418
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
5022
5419
  }
5023
- function isRec8(v) {
5420
+ function isRec10(v) {
5024
5421
  return !!v && typeof v === "object" && !Array.isArray(v);
5025
5422
  }
5026
5423
  function distance4(a, b) {
@@ -5058,17 +5455,17 @@ function list2(names) {
5058
5455
  }
5059
5456
  function indexDatasets(stack) {
5060
5457
  const out = /* @__PURE__ */ new Map();
5061
- for (const ds of asArray28(stack.datasets)) {
5062
- const name = strName10(ds.name);
5458
+ for (const ds of asArray31(stack.datasets)) {
5459
+ const name = strName11(ds.name);
5063
5460
  if (!name) continue;
5064
5461
  const dimensions = /* @__PURE__ */ new Set();
5065
- for (const d of asArray28(ds.dimensions)) {
5066
- const n = strName10(d.name);
5462
+ for (const d of asArray31(ds.dimensions)) {
5463
+ const n = strName11(d.name);
5067
5464
  if (n) dimensions.add(n);
5068
5465
  }
5069
5466
  const measures = /* @__PURE__ */ new Set();
5070
- for (const m of asArray28(ds.measures)) {
5071
- const n = strName10(m.name);
5467
+ for (const m of asArray31(ds.measures)) {
5468
+ const n = strName11(m.name);
5072
5469
  if (n) measures.add(n);
5073
5470
  }
5074
5471
  out.set(name, { dimensions, measures });
@@ -5146,29 +5543,29 @@ function validateChartBindings(stack) {
5146
5543
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
5147
5544
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
5148
5545
  };
5149
- const reports = asArray28(stack.reports);
5546
+ const reports = asArray31(stack.reports);
5150
5547
  for (let ri = 0; ri < reports.length; ri++) {
5151
5548
  const report = reports[ri];
5152
- if (!isRec8(report)) continue;
5153
- const reportName = strName10(report.name) ?? `#${ri}`;
5549
+ if (!isRec10(report)) continue;
5550
+ const reportName = strName11(report.name) ?? `#${ri}`;
5154
5551
  const checkReportChart = (chart, dataset, values, where, path) => {
5155
- if (!isRec8(chart)) return;
5552
+ if (!isRec10(chart)) return;
5156
5553
  check({
5157
5554
  dataset,
5158
5555
  // `values` is the report's measure SELECTION, not a chart ref; feeding
5159
5556
  // it in lets the yAxis "declared but not selected" check work without
5160
5557
  // reporting the selection itself twice.
5161
5558
  values: { names: values, path: `${path}.values` },
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),
5559
+ xAxis: strName11(chart.xAxis) ? { name: strName11(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
5560
+ yAxis: strName11(chart.yAxis) ? { name: strName11(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
5561
+ series: asArray31(chart.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
5165
5562
  where,
5166
5563
  path: `${path}.chart`
5167
5564
  });
5168
5565
  };
5169
5566
  checkReportChart(
5170
5567
  report.chart,
5171
- strName10(report.dataset),
5568
+ strName11(report.dataset),
5172
5569
  strList3(report.values),
5173
5570
  `report "${reportName}" \xB7 chart`,
5174
5571
  `reports[${ri}]`
@@ -5176,45 +5573,45 @@ function validateChartBindings(stack) {
5176
5573
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
5177
5574
  for (let bi = 0; bi < blocks.length; bi++) {
5178
5575
  const block = blocks[bi];
5179
- if (!isRec8(block)) continue;
5576
+ if (!isRec10(block)) continue;
5180
5577
  checkReportChart(
5181
5578
  block.chart,
5182
- strName10(block.dataset),
5579
+ strName11(block.dataset),
5183
5580
  strList3(block.values),
5184
- `report "${reportName}" \xB7 block "${strName10(block.name) ?? `#${bi}`}" chart`,
5581
+ `report "${reportName}" \xB7 block "${strName11(block.name) ?? `#${bi}`}" chart`,
5185
5582
  `reports[${ri}].blocks[${bi}]`
5186
5583
  );
5187
5584
  }
5188
5585
  }
5189
5586
  const checkListChart = (container, where, path) => {
5190
- if (!isRec8(container)) return;
5587
+ if (!isRec10(container)) return;
5191
5588
  const chart = container.chart;
5192
- if (!isRec8(chart)) return;
5589
+ if (!isRec10(chart)) return;
5193
5590
  check({
5194
- dataset: strName10(chart.dataset),
5591
+ dataset: strName11(chart.dataset),
5195
5592
  dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
5196
5593
  values: { names: strList3(chart.values), path: `${path}.chart.values` },
5197
5594
  where,
5198
5595
  path: `${path}.chart`
5199
5596
  });
5200
5597
  };
5201
- const views = asArray28(stack.views);
5598
+ const views = asArray31(stack.views);
5202
5599
  for (let vi = 0; vi < views.length; vi++) {
5203
5600
  const view = views[vi];
5204
- if (!isRec8(view)) continue;
5205
- const viewName = strName10(view.name) ?? strName10(view.objectName) ?? `#${vi}`;
5601
+ if (!isRec10(view)) continue;
5602
+ const viewName = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
5206
5603
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
5207
- if (isRec8(view.listViews)) {
5604
+ if (isRec10(view.listViews)) {
5208
5605
  for (const [key, lv] of Object.entries(view.listViews)) {
5209
5606
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
5210
5607
  }
5211
5608
  }
5212
5609
  }
5213
- const objects = asArray28(stack.objects);
5610
+ const objects = asArray31(stack.objects);
5214
5611
  for (let oi = 0; oi < objects.length; oi++) {
5215
5612
  const obj = objects[oi];
5216
- if (!isRec8(obj) || !isRec8(obj.listViews)) continue;
5217
- const objName = strName10(obj.name) ?? `#${oi}`;
5613
+ if (!isRec10(obj) || !isRec10(obj.listViews)) continue;
5614
+ const objName = strName11(obj.name) ?? `#${oi}`;
5218
5615
  for (const [key, lv] of Object.entries(obj.listViews)) {
5219
5616
  checkListChart(
5220
5617
  lv,
@@ -5223,22 +5620,22 @@ function validateChartBindings(stack) {
5223
5620
  );
5224
5621
  }
5225
5622
  }
5226
- const pages = asArray28(stack.pages);
5623
+ const pages = asArray31(stack.pages);
5227
5624
  for (let pi = 0; pi < pages.length; pi++) {
5228
5625
  const page = pages[pi];
5229
- if (!isRec8(page)) continue;
5230
- const pageName = strName10(page.name) ?? `#${pi}`;
5626
+ if (!isRec10(page)) continue;
5627
+ const pageName = strName11(page.name) ?? `#${pi}`;
5231
5628
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
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);
5629
+ const props = isRec10(component.properties) ? component.properties : void 0;
5630
+ if (!props || !strName11(props.dataset)) continue;
5631
+ const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName11(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
5632
+ const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
5236
5633
  check({
5237
- dataset: strName10(props.dataset),
5634
+ dataset: strName11(props.dataset),
5238
5635
  dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
5239
5636
  values: { names: strList3(props.values), path: `${path}.properties.values` },
5240
5637
  series: [...axisRefs, ...seriesRefs],
5241
- where: `page "${pageName}" \xB7 ${strName10(component.type) ?? "chart"}`,
5638
+ where: `page "${pageName}" \xB7 ${strName11(component.type) ?? "chart"}`,
5242
5639
  path: `${path}.properties`
5243
5640
  });
5244
5641
  }
@@ -5246,12 +5643,265 @@ function validateChartBindings(stack) {
5246
5643
  return findings;
5247
5644
  }
5248
5645
 
5249
- // src/validate-nav-access.ts
5250
- var import_system3 = require("@objectstack/spec/system");
5251
-
5252
- // src/build-access-matrix.ts
5253
- function asArray29(v) {
5254
- if (Array.isArray(v)) return v;
5646
+ // src/validate-rule-compilability.ts
5647
+ var import_node_module3 = require("module");
5648
+ var import_meta3 = {};
5649
+ var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
5650
+ var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
5651
+ var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
5652
+ var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5653
+ function asArray32(v) {
5654
+ if (Array.isArray(v)) return v.filter(isRec11);
5655
+ if (isRec11(v)) {
5656
+ return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ name, ...def }));
5657
+ }
5658
+ return [];
5659
+ }
5660
+ var cachedAjv = null;
5661
+ var cachedAddFormats = null;
5662
+ function loadAjv() {
5663
+ if (cachedAjv) return cachedAjv;
5664
+ const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5665
+ let mod;
5666
+ try {
5667
+ mod = (0, import_node_module3.createRequire)(anchor)("ajv");
5668
+ } catch (err) {
5669
+ throw new Error(
5670
+ `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
5671
+ );
5672
+ }
5673
+ const ctor = isRec11(mod) && "default" in mod ? mod.default : mod;
5674
+ cachedAjv = ctor;
5675
+ return ctor;
5676
+ }
5677
+ function loadAddFormats() {
5678
+ if (cachedAddFormats) return cachedAddFormats;
5679
+ const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5680
+ let mod;
5681
+ try {
5682
+ mod = (0, import_node_module3.createRequire)(anchor)("ajv-formats");
5683
+ } catch (err) {
5684
+ throw new Error(
5685
+ `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
5686
+ );
5687
+ }
5688
+ const plugin = isRec11(mod) && "default" in mod ? mod.default : mod;
5689
+ cachedAddFormats = plugin;
5690
+ return plugin;
5691
+ }
5692
+ function createRuntimeAjv() {
5693
+ const instance = new (loadAjv())(RUNTIME_AJV_OPTIONS);
5694
+ loadAddFormats()(instance);
5695
+ return instance;
5696
+ }
5697
+ function registeredFormatNames() {
5698
+ const names = Object.keys(createRuntimeAjv().formats).sort();
5699
+ if (names.length === 0) {
5700
+ throw new Error(
5701
+ `@objectstack/lint: the runtime-parity ajv instance has no \`format\` registered. "ajv-formats" loaded but added nothing, so the set of legitimate format names is unknown \u2014 refusing to judge format names against an empty vocabulary, which would report every \`format\` in the stack as misspelled. Check that the installed "ajv-formats" is the real package and matches the version @objectstack/lint declares.`
5702
+ );
5703
+ }
5704
+ return names;
5705
+ }
5706
+ function errorText(err) {
5707
+ return err instanceof Error ? err.message : String(err);
5708
+ }
5709
+ var MAX_RULE_NESTING_DEPTH = 16;
5710
+ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
5711
+ const name = typeof rule.name === "string" && rule.name ? rule.name : "?";
5712
+ const label2 = labelTrail ? `${labelTrail} \u2192 '${name}'` : `'${name}'`;
5713
+ const path = pathTrail ? `${pathTrail}.${name}` : name;
5714
+ const out = [{ rule, label: label2, path }];
5715
+ if (depth >= MAX_RULE_NESTING_DEPTH) return out;
5716
+ for (const branch of ["then", "otherwise"]) {
5717
+ const nested = rule[branch];
5718
+ if (isRec11(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
5719
+ }
5720
+ return out;
5721
+ }
5722
+ function walkObjectValidationRules(stack) {
5723
+ const walked = [];
5724
+ if (!isRec11(stack)) return walked;
5725
+ for (const obj of asArray32(stack.objects)) {
5726
+ const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
5727
+ const validations = obj.validations;
5728
+ for (const authored of asArray32(validations)) {
5729
+ for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
5730
+ walked.push({
5731
+ rule,
5732
+ objectName,
5733
+ label: label2,
5734
+ where: `object '${objectName}' \xB7 validation ${label2}`,
5735
+ basePath: `objects.${objectName}.validations.${path}`
5736
+ });
5737
+ }
5738
+ }
5739
+ }
5740
+ return walked;
5741
+ }
5742
+ function validateRuleCompilability(stack) {
5743
+ const findings = [];
5744
+ for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
5745
+ if (rule.type === "format" && typeof rule.regex === "string" && rule.regex !== "") {
5746
+ try {
5747
+ new RegExp(rule.regex);
5748
+ } catch (err) {
5749
+ findings.push({
5750
+ severity: "error",
5751
+ rule: VALIDATION_RULE_REGEX_UNCOMPILABLE,
5752
+ where,
5753
+ path: `${basePath}.regex`,
5754
+ message: `\`format\` validation ${label2} on object '${objectName}' declares a \`regex\` that does not compile: ${errorText(err)}. The write path builds it with \`new RegExp(rule.regex)\` and SKIPS the rule when that throws (rule-validator.ts \`checkFormat\`), so the rule is declared, listed in the metadata, and enforces nothing on any record.`,
5755
+ hint: `Fix the pattern so \`new RegExp('${rule.regex}')\` compiles \u2014 a literal \`(\`, \`[\` or \`\\\` must be escaped (\`\\\\(\`, \`\\\\[\`, \`\\\\\\\\\`), and the source is a STRING, so a backslash is written twice in TypeScript ('^\\\\d{2}-\\\\d{7}$'). Or drop \`regex\` and use a named \`format\` ('email' | 'url' | 'phone' | 'json').`
5756
+ });
5757
+ }
5758
+ }
5759
+ if (rule.type === "json_schema" && isRec11(rule.schema)) {
5760
+ try {
5761
+ createRuntimeAjv().compile(rule.schema);
5762
+ } catch (err) {
5763
+ findings.push({
5764
+ severity: "error",
5765
+ rule: VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
5766
+ where,
5767
+ path: `${basePath}.schema`,
5768
+ message: `\`json_schema\` validation ${label2} on object '${objectName}' declares a \`schema\` ajv cannot compile: ${errorText(err)}. The write path compiles it with the same ajv (\`new Ajv({ allErrors: true, strict: false })\` + \`ajv-formats\`) and SKIPS the rule when that throws (rule-validator.ts \`checkJsonSchema\`), so the rule is declared and enforces nothing on any record.`,
5769
+ hint: `Correct the schema so ajv compiles it \u2014 the message above names the offending keyword. \`type\` must be one of null|boolean|object|array|number|string|integer (or an array of those), \`required\` an array of strings, and every \`$ref\` must resolve. Vendor keywords are fine (the runtime runs \`strict: false\`); a MALFORMED standard keyword is not.`
5770
+ });
5771
+ }
5772
+ }
5773
+ }
5774
+ return findings;
5775
+ }
5776
+
5777
+ // src/validate-rule-schema-formats.ts
5778
+ var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
5779
+ var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5780
+ var SUBSCHEMA_KEYS = [
5781
+ "additionalItems",
5782
+ "additionalProperties",
5783
+ "contains",
5784
+ "propertyNames",
5785
+ "if",
5786
+ "then",
5787
+ "else",
5788
+ "not",
5789
+ "unevaluatedItems",
5790
+ "unevaluatedProperties"
5791
+ ];
5792
+ var SUBSCHEMA_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
5793
+ var SUBSCHEMA_MAP_KEYS = [
5794
+ "properties",
5795
+ "patternProperties",
5796
+ "$defs",
5797
+ "definitions",
5798
+ "dependentSchemas"
5799
+ ];
5800
+ var MAX_SCHEMA_WALK_DEPTH = 32;
5801
+ var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
5802
+ function collectFormatUses(schema, pointer, out, depth) {
5803
+ if (!isRec12(schema)) return;
5804
+ if (typeof schema.format === "string") {
5805
+ out.push({ pointer: `${pointer}/format`, name: schema.format });
5806
+ }
5807
+ if (depth >= MAX_SCHEMA_WALK_DEPTH) return;
5808
+ for (const key of SUBSCHEMA_KEYS) {
5809
+ if (key in schema) {
5810
+ collectFormatUses(schema[key], `${pointer}/${escapePointerSegment(key)}`, out, depth + 1);
5811
+ }
5812
+ }
5813
+ for (const key of SUBSCHEMA_LIST_KEYS) {
5814
+ const value = schema[key];
5815
+ if (!Array.isArray(value)) continue;
5816
+ value.forEach((entry, index) => {
5817
+ collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${index}`, out, depth + 1);
5818
+ });
5819
+ }
5820
+ for (const key of SUBSCHEMA_MAP_KEYS) {
5821
+ const value = schema[key];
5822
+ if (!isRec12(value)) continue;
5823
+ for (const [name, entry] of Object.entries(value)) {
5824
+ collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
5825
+ }
5826
+ }
5827
+ const items = schema.items;
5828
+ if (Array.isArray(items)) {
5829
+ items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
5830
+ } else if (isRec12(items)) {
5831
+ collectFormatUses(items, `${pointer}/items`, out, depth + 1);
5832
+ }
5833
+ const dependencies = schema.dependencies;
5834
+ if (isRec12(dependencies)) {
5835
+ for (const [name, entry] of Object.entries(dependencies)) {
5836
+ if (!isRec12(entry)) continue;
5837
+ collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
5838
+ }
5839
+ }
5840
+ }
5841
+ function editDistance2(a, b) {
5842
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
5843
+ for (let i = 1; i <= a.length; i++) {
5844
+ const current = [i];
5845
+ for (let j = 1; j <= b.length; j++) {
5846
+ current[j] = Math.min(
5847
+ previous[j] + 1,
5848
+ current[j - 1] + 1,
5849
+ previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
5850
+ );
5851
+ }
5852
+ previous = current;
5853
+ }
5854
+ return previous[b.length];
5855
+ }
5856
+ function nearestRegisteredFormat(name, registered) {
5857
+ const budget = Math.min(3, Math.floor(name.length / 2));
5858
+ if (budget < 1) return null;
5859
+ const authored = name.toLowerCase();
5860
+ let best = null;
5861
+ let bestDistance = Number.POSITIVE_INFINITY;
5862
+ for (const candidate of [...registered].sort()) {
5863
+ const distance7 = editDistance2(authored, candidate);
5864
+ if (distance7 < bestDistance) {
5865
+ bestDistance = distance7;
5866
+ best = candidate;
5867
+ }
5868
+ }
5869
+ return bestDistance <= budget ? best : null;
5870
+ }
5871
+ function validateRuleSchemaFormats(stack) {
5872
+ const findings = [];
5873
+ const pending = [];
5874
+ for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
5875
+ if (rule.type !== "json_schema" || !isRec12(rule.schema)) continue;
5876
+ const uses = [];
5877
+ collectFormatUses(rule.schema, "", uses, 0);
5878
+ for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
5879
+ }
5880
+ if (pending.length === 0) return findings;
5881
+ const registered = registeredFormatNames();
5882
+ const known = new Set(registered);
5883
+ for (const { use, where, label: label2, objectName, basePath } of pending) {
5884
+ if (known.has(use.name)) continue;
5885
+ const suggestion = nearestRegisteredFormat(use.name, registered);
5886
+ const pointer = `#${use.pointer}`;
5887
+ findings.push({
5888
+ severity: "error",
5889
+ rule: VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
5890
+ where,
5891
+ path: `${basePath}.schema${pointer}`,
5892
+ message: `\`json_schema\` validation ${label2} on object '${objectName}' names \`format: '${use.name}'\` at \`${pointer}\`, which is not a registered format. ajv logs \`unknown format "${use.name}" ignored\` once at compile time and DROPS the keyword \u2014 in the write path (rule-validator.ts, \`strict: false\`) and in the publish gate alike \u2014 so the schema compiles, the rule ships and runs on every write, its \`type\`/\`required\` keywords are enforced, and this constraint is enforced on no record, ever. The record is ACCEPTED, so nothing downstream reports the gap either.`,
5893
+ hint: (suggestion ? `Did you mean \`format: '${suggestion}'\`? ` : "") + `The registered names are: ${registered.join(", ")} \u2014 the default \`ajv-formats\` set, the one \`rule-validator.ts\` registers (#5029). Names are case-sensitive and hyphenated (\`date-time\`, not \`datetime\`). If you meant a constraint ajv has no format for, express it with \`pattern\` instead \u2014 a regex is enforced, an unknown format name is not.`
5894
+ });
5895
+ }
5896
+ return findings;
5897
+ }
5898
+
5899
+ // src/validate-nav-access.ts
5900
+ var import_system3 = require("@objectstack/spec/system");
5901
+
5902
+ // src/build-access-matrix.ts
5903
+ function asArray33(v) {
5904
+ if (Array.isArray(v)) return v;
5255
5905
  if (v && typeof v === "object") {
5256
5906
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5257
5907
  }
@@ -5261,13 +5911,13 @@ function buildAccessMatrix(stack) {
5261
5911
  const entries = [];
5262
5912
  if (!stack || typeof stack !== "object") return { version: 1, entries };
5263
5913
  const owdByObject = /* @__PURE__ */ new Map();
5264
- for (const obj of asArray29(stack.objects)) {
5914
+ for (const obj of asArray33(stack.objects)) {
5265
5915
  const name = typeof obj.name === "string" ? obj.name : "";
5266
5916
  if (!name) continue;
5267
5917
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
5268
5918
  if (typeof owd === "string") owdByObject.set(name, owd);
5269
5919
  }
5270
- for (const ps of asArray29(stack.permissions)) {
5920
+ for (const ps of asArray33(stack.permissions)) {
5271
5921
  const psName = typeof ps.name === "string" ? ps.name : "";
5272
5922
  if (!psName) continue;
5273
5923
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -5340,34 +5990,34 @@ function diffAccessMatrix(before, after) {
5340
5990
 
5341
5991
  // src/validate-nav-access.ts
5342
5992
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
5343
- function asArray30(v) {
5993
+ function asArray34(v) {
5344
5994
  if (Array.isArray(v)) return v;
5345
5995
  if (v && typeof v === "object") {
5346
5996
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5347
5997
  }
5348
5998
  return [];
5349
5999
  }
5350
- function strName11(v) {
6000
+ function strName12(v) {
5351
6001
  return typeof v === "string" && v.length > 0 ? v : void 0;
5352
6002
  }
5353
6003
  function collectNavExposures(stack) {
5354
6004
  const out = [];
5355
- const apps = asArray30(stack.apps);
6005
+ const apps = asArray34(stack.apps);
5356
6006
  for (let ai = 0; ai < apps.length; ai++) {
5357
6007
  const app = apps[ai];
5358
6008
  if (!app || typeof app !== "object") continue;
5359
- const appName = strName11(app.name) ?? `#${ai}`;
6009
+ const appName = strName12(app.name) ?? `#${ai}`;
5360
6010
  const walk = (items, basePath) => {
5361
- const navItems = asArray30(items);
6011
+ const navItems = asArray34(items);
5362
6012
  for (let ni = 0; ni < navItems.length; ni++) {
5363
6013
  const nav = navItems[ni];
5364
6014
  if (!nav || typeof nav !== "object") continue;
5365
6015
  const navPath = `${basePath}[${ni}]`;
5366
- const objectName = strName11(nav.objectName);
6016
+ const objectName = strName12(nav.objectName);
5367
6017
  if (nav.type === "object" && objectName) {
5368
6018
  out.push({
5369
6019
  objectName,
5370
- where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
6020
+ where: `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
5371
6021
  path: `${navPath}.objectName`
5372
6022
  });
5373
6023
  }
@@ -5375,7 +6025,7 @@ function collectNavExposures(stack) {
5375
6025
  }
5376
6026
  };
5377
6027
  walk(app.navigation, `apps[${ai}].navigation`);
5378
- const areas = asArray30(app.areas);
6028
+ const areas = asArray34(app.areas);
5379
6029
  for (let ri = 0; ri < areas.length; ri++) {
5380
6030
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
5381
6031
  }
@@ -5385,13 +6035,13 @@ function collectNavExposures(stack) {
5385
6035
  function validateNavAccess(stack) {
5386
6036
  const findings = [];
5387
6037
  if (!stack || typeof stack !== "object") return findings;
5388
- const permissionSets = asArray30(stack.permissions);
6038
+ const permissionSets = asArray34(stack.permissions);
5389
6039
  if (permissionSets.length === 0) return findings;
5390
6040
  const exposures = collectNavExposures(stack);
5391
6041
  if (exposures.length === 0) return findings;
5392
6042
  const ownObjects = /* @__PURE__ */ new Set();
5393
- for (const obj of asArray30(stack.objects)) {
5394
- const n = strName11(obj.name);
6043
+ for (const obj of asArray34(stack.objects)) {
6044
+ const n = strName12(obj.name);
5395
6045
  if (n) ownObjects.add(n);
5396
6046
  }
5397
6047
  const readable = /* @__PURE__ */ new Set();
@@ -5423,15 +6073,15 @@ function validateNavAccess(stack) {
5423
6073
  var import_system4 = require("@objectstack/spec/system");
5424
6074
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
5425
6075
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
5426
- function isRec9(v) {
6076
+ function isRec13(v) {
5427
6077
  return !!v && typeof v === "object" && !Array.isArray(v);
5428
6078
  }
5429
- function asArray31(v) {
6079
+ function asArray35(v) {
5430
6080
  if (Array.isArray(v)) return v;
5431
- if (isRec9(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec9(def) ? def : {} }));
6081
+ if (isRec13(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec13(def) ? def : {} }));
5432
6082
  return [];
5433
6083
  }
5434
- function strName12(v) {
6084
+ function strName13(v) {
5435
6085
  return typeof v === "string" && v.length > 0 ? v : void 0;
5436
6086
  }
5437
6087
  function distance5(a, b) {
@@ -5489,35 +6139,32 @@ function collectViewRecord(view, factsFor) {
5489
6139
  const addView = (objectName, name) => {
5490
6140
  if (objectName && name) factsFor(objectName).views.add(name);
5491
6141
  };
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));
6142
+ const addSections = (container, binding) => {
6143
+ if (!binding) return;
6144
+ for (const section of asArray35(container.sections)) {
6145
+ const sectionName = strName13(section.name);
6146
+ if (sectionName) factsFor(binding).sections.add(sectionName);
6147
+ }
6148
+ };
6149
+ const listBinding = isRec13(view.list) ? bindingOf(view.list) : void 0;
6150
+ if (isRec13(view.list)) addView(listBinding, strName13(view.list.name));
6151
+ addView(recordObject ?? listBinding, strName13(view.name));
5495
6152
  for (const key of ["listViews", "formViews"]) {
5496
6153
  const container = view[key];
5497
- if (!isRec9(container)) continue;
6154
+ if (!isRec13(container)) continue;
5498
6155
  for (const [subKey, sub] of Object.entries(container)) {
5499
- if (!isRec9(sub)) continue;
6156
+ if (!isRec13(sub)) continue;
5500
6157
  const binding = bindingOf(sub) ?? listBinding;
5501
6158
  addView(binding, subKey);
5502
- addView(binding, strName12(sub.name));
5503
- if (binding) {
5504
- for (const section of asArray31(sub.sections)) {
5505
- const sectionName = strName12(section.name);
5506
- if (sectionName) factsFor(binding).sections.add(sectionName);
5507
- }
5508
- }
5509
- }
5510
- }
5511
- const sectionBinding = recordObject ?? listBinding;
5512
- if (sectionBinding) {
5513
- for (const section of asArray31(view.sections)) {
5514
- const sectionName = strName12(section.name);
5515
- if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
6159
+ addView(binding, strName13(sub.name));
6160
+ addSections(sub, binding);
5516
6161
  }
5517
6162
  }
6163
+ if (isRec13(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
6164
+ addSections(view, recordObject ?? listBinding);
5518
6165
  }
5519
6166
  function viewObjectName(view) {
5520
- return strName12(view.objectName) ?? strName12(view.object) ?? (isRec9(view.data) ? strName12(view.data.object) : void 0);
6167
+ return strName13(view.objectName) ?? strName13(view.object) ?? (isRec13(view.data) ? strName13(view.data.object) : void 0);
5521
6168
  }
5522
6169
  function readOptions(field) {
5523
6170
  const raw = field.options;
@@ -5529,14 +6176,14 @@ function readOptions(field) {
5529
6176
  values.add(opt);
5530
6177
  continue;
5531
6178
  }
5532
- if (!isRec9(opt)) continue;
5533
- const value = strName12(opt.value);
6179
+ if (!isRec13(opt)) continue;
6180
+ const value = strName13(opt.value);
5534
6181
  if (!value) continue;
5535
6182
  values.add(value);
5536
- const label2 = strName12(opt.label);
6183
+ const label2 = strName13(opt.label);
5537
6184
  if (label2) byLabel.set(label2.toLowerCase(), value);
5538
6185
  }
5539
- } else if (isRec9(raw)) {
6186
+ } else if (isRec13(raw)) {
5540
6187
  for (const [value, label2] of Object.entries(raw)) {
5541
6188
  values.add(value);
5542
6189
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -5556,48 +6203,48 @@ function buildUniverse(stack) {
5556
6203
  }
5557
6204
  return facts;
5558
6205
  };
5559
- for (const obj of asArray31(stack.objects)) {
5560
- const objectName = strName12(obj.name);
6206
+ for (const obj of asArray35(stack.objects)) {
6207
+ const objectName = strName13(obj.name);
5561
6208
  if (!objectName) continue;
5562
6209
  const facts = factsFor(objectName);
5563
- for (const field of asArray31(obj.fields)) {
5564
- const fieldName = strName12(field.name);
6210
+ for (const field of asArray35(obj.fields)) {
6211
+ const fieldName = strName13(field.name);
5565
6212
  if (fieldName) facts.fields.set(fieldName, field);
5566
6213
  }
5567
- for (const action of asArray31(obj.actions)) {
5568
- const actionName = strName12(action.name);
6214
+ for (const action of asArray35(obj.actions)) {
6215
+ const actionName = strName13(action.name);
5569
6216
  if (actionName) facts.actions.set(actionName, action);
5570
6217
  }
5571
- for (const view of asArray31(obj.views)) {
5572
- collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
6218
+ for (const view of asArray35(obj.views)) {
6219
+ collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
5573
6220
  }
5574
6221
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
5575
- for (const group of asArray31(obj.fieldGroups)) {
5576
- const key = strName12(group.key) ?? strName12(group.name);
6222
+ for (const group of asArray35(obj.fieldGroups)) {
6223
+ const key = strName13(group.key) ?? strName13(group.name);
5577
6224
  if (key) facts.sections.add(key);
5578
6225
  }
5579
6226
  }
5580
- for (const view of asArray31(stack.views)) {
6227
+ for (const view of asArray35(stack.views)) {
5581
6228
  collectViewRecord(view, factsFor);
5582
6229
  }
5583
- const pages = asArray31(stack.pages);
6230
+ const pages = asArray35(stack.pages);
5584
6231
  for (let pi = 0; pi < pages.length; pi++) {
5585
6232
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
5586
6233
  if (!walked.objectName) continue;
5587
- const props = isRec9(walked.component.properties) ? walked.component.properties : void 0;
6234
+ const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
5588
6235
  if (!props) continue;
5589
- for (const section of asArray31(props.sections)) {
5590
- const sectionName = strName12(section.name);
6236
+ for (const section of asArray35(props.sections)) {
6237
+ const sectionName = strName13(section.name);
5591
6238
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
5592
6239
  }
5593
6240
  }
5594
6241
  }
5595
6242
  const globalActions = /* @__PURE__ */ new Map();
5596
6243
  const actionOwners = /* @__PURE__ */ new Map();
5597
- for (const action of asArray31(stack.actions)) {
5598
- const actionName = strName12(action.name);
6244
+ for (const action of asArray35(stack.actions)) {
6245
+ const actionName = strName13(action.name);
5599
6246
  if (!actionName) continue;
5600
- const owner = strName12(action.objectName) ?? strName12(action.object);
6247
+ const owner = strName13(action.objectName) ?? strName13(action.object);
5601
6248
  if (owner) {
5602
6249
  factsFor(owner).actions.set(actionName, action);
5603
6250
  actionOwners.set(actionName, owner);
@@ -5611,41 +6258,41 @@ function buildUniverse(stack) {
5611
6258
  }
5612
6259
  }
5613
6260
  const apps = /* @__PURE__ */ new Map();
5614
- for (const app of asArray31(stack.apps)) {
5615
- const appName = strName12(app.name);
6261
+ for (const app of asArray35(stack.apps)) {
6262
+ const appName = strName13(app.name);
5616
6263
  if (!appName) continue;
5617
6264
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
5618
6265
  const walkNav = (items) => {
5619
- for (const item of asArray31(items)) {
5620
- const id = strName12(item.id);
6266
+ for (const item of asArray35(items)) {
6267
+ const id = strName13(item.id);
5621
6268
  if (id) navIds.add(id);
5622
6269
  if (item.children) walkNav(item.children);
5623
6270
  }
5624
6271
  };
5625
6272
  walkNav(app.navigation);
5626
- for (const area of asArray31(app.areas)) {
5627
- const areaId = strName12(area.id);
6273
+ for (const area of asArray35(app.areas)) {
6274
+ const areaId = strName13(area.id);
5628
6275
  if (areaId) navIds.add(areaId);
5629
6276
  walkNav(area.navigation);
5630
6277
  }
5631
6278
  apps.set(appName, navIds);
5632
6279
  }
5633
6280
  const dashboards = /* @__PURE__ */ new Map();
5634
- for (const dash of asArray31(stack.dashboards)) {
5635
- const dashName = strName12(dash.name);
6281
+ for (const dash of asArray35(stack.dashboards)) {
6282
+ const dashName = strName13(dash.name);
5636
6283
  if (!dashName) continue;
5637
6284
  const widgets = /* @__PURE__ */ new Set();
5638
- for (const widget of asArray31(dash.widgets)) {
5639
- const id = strName12(widget.id) ?? strName12(widget.name);
6285
+ for (const widget of asArray35(dash.widgets)) {
6286
+ const id = strName13(widget.id) ?? strName13(widget.name);
5640
6287
  if (id) widgets.add(id);
5641
6288
  }
5642
6289
  const actions = /* @__PURE__ */ new Set();
5643
6290
  const headerActions = [
5644
- ...asArray31(isRec9(dash.header) ? dash.header.actions : void 0),
5645
- ...asArray31(dash.actions)
6291
+ ...asArray35(isRec13(dash.header) ? dash.header.actions : void 0),
6292
+ ...asArray35(dash.actions)
5646
6293
  ];
5647
6294
  for (const action of headerActions) {
5648
- const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
6295
+ const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
5649
6296
  if (key) actions.add(key);
5650
6297
  }
5651
6298
  dashboards.set(dashName, { widgets, actions });
@@ -5657,7 +6304,7 @@ function localePath(bundleIndex, locale) {
5657
6304
  }
5658
6305
  function validateTranslationReferences(stack) {
5659
6306
  const findings = [];
5660
- if (!isRec9(stack)) return findings;
6307
+ if (!isRec13(stack)) return findings;
5661
6308
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
5662
6309
  if (bundles.length === 0) return findings;
5663
6310
  const universe = buildUniverse(stack);
@@ -5666,13 +6313,13 @@ function validateTranslationReferences(stack) {
5666
6313
  };
5667
6314
  for (let bi = 0; bi < bundles.length; bi++) {
5668
6315
  const bundle = bundles[bi];
5669
- if (!isRec9(bundle)) continue;
6316
+ if (!isRec13(bundle)) continue;
5670
6317
  for (const [locale, rawData] of Object.entries(bundle)) {
5671
- if (!isRec9(rawData)) continue;
6318
+ if (!isRec13(rawData)) continue;
5672
6319
  const base = localePath(bi, locale);
5673
6320
  const inLocale = `locale "${locale}"`;
5674
6321
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
5675
- if (!isRec9(rawNode)) continue;
6322
+ if (!isRec13(rawNode)) continue;
5676
6323
  const objPath = `${base}.objects.${objectName}`;
5677
6324
  const facts = universe.objects.get(objectName);
5678
6325
  if (!facts) {
@@ -5698,7 +6345,7 @@ function validateTranslationReferences(stack) {
5698
6345
  );
5699
6346
  continue;
5700
6347
  }
5701
- if (!isRec9(rawField)) continue;
6348
+ if (!isRec13(rawField)) continue;
5702
6349
  checkOptionKeys(findings, {
5703
6350
  optionMap: rawField.options,
5704
6351
  field,
@@ -5780,7 +6427,7 @@ function validateTranslationReferences(stack) {
5780
6427
  );
5781
6428
  continue;
5782
6429
  }
5783
- if (!isRec9(rawApp)) continue;
6430
+ if (!isRec13(rawApp)) continue;
5784
6431
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
5785
6432
  if (navIds.has(navId)) continue;
5786
6433
  orphan(
@@ -5803,7 +6450,7 @@ function validateTranslationReferences(stack) {
5803
6450
  );
5804
6451
  continue;
5805
6452
  }
5806
- if (!isRec9(rawDash)) continue;
6453
+ if (!isRec13(rawDash)) continue;
5807
6454
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
5808
6455
  if (dash.widgets.has(widgetId)) continue;
5809
6456
  orphan(
@@ -5828,7 +6475,7 @@ function validateTranslationReferences(stack) {
5828
6475
  return findings;
5829
6476
  }
5830
6477
  function asRecord(v) {
5831
- return isRec9(v) ? v : {};
6478
+ return isRec13(v) ? v : {};
5832
6479
  }
5833
6480
  function checkOptionKeys(findings, ctx) {
5834
6481
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -5840,7 +6487,7 @@ function checkOptionKeys(findings, ctx) {
5840
6487
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
5841
6488
  where: ctx.where,
5842
6489
  path: ctx.path,
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.`,
6490
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName13(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
5844
6491
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
5845
6492
  });
5846
6493
  return;
@@ -5859,11 +6506,11 @@ function checkOptionKeys(findings, ctx) {
5859
6506
  }
5860
6507
  }
5861
6508
  function checkActionParams(findings, ctx) {
5862
- const rawParams = Object.keys(asRecord(isRec9(ctx.rawAction) ? ctx.rawAction.params : void 0));
6509
+ const rawParams = Object.keys(asRecord(isRec13(ctx.rawAction) ? ctx.rawAction.params : void 0));
5863
6510
  if (rawParams.length === 0) return;
5864
6511
  const declared = /* @__PURE__ */ new Set();
5865
- for (const param of asArray31(ctx.action.params)) {
5866
- const name = strName12(param.name) ?? strName12(param.field);
6512
+ for (const param of asArray35(ctx.action.params)) {
6513
+ const name = strName13(param.name) ?? strName13(param.field);
5867
6514
  if (name) declared.add(name);
5868
6515
  }
5869
6516
  for (const paramName of rawParams) {
@@ -5879,16 +6526,161 @@ function checkActionParams(findings, ctx) {
5879
6526
  }
5880
6527
  }
5881
6528
 
6529
+ // src/validate-translatable-sections.ts
6530
+ var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
6531
+ function isRec14(v) {
6532
+ return !!v && typeof v === "object" && !Array.isArray(v);
6533
+ }
6534
+ function strName14(v) {
6535
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6536
+ }
6537
+ function viewObjectName2(view) {
6538
+ return strName14(view.objectName) ?? strName14(view.object) ?? (isRec14(view.data) ? strName14(view.data.object) : void 0);
6539
+ }
6540
+ function collectionEntries(v, base) {
6541
+ if (Array.isArray(v)) {
6542
+ const out = [];
6543
+ for (let i = 0; i < v.length; i++) {
6544
+ if (isRec14(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
6545
+ }
6546
+ return out;
6547
+ }
6548
+ if (isRec14(v)) {
6549
+ return Object.entries(v).filter(([, def]) => isRec14(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
6550
+ }
6551
+ return [];
6552
+ }
6553
+ function viewLabel(view) {
6554
+ const name = strName14(view.name);
6555
+ return name ? `view "${name}"` : "";
6556
+ }
6557
+ function joinWhere(...parts) {
6558
+ return parts.filter((p) => p.length > 0).join(" \xB7 ");
6559
+ }
6560
+ function collectViewSites(view, basePath, label2, sites) {
6561
+ const recordObject = viewObjectName2(view);
6562
+ const listBinding = isRec14(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
6563
+ const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
6564
+ sites.push({
6565
+ path: `${basePath}.sections`,
6566
+ surface: label2,
6567
+ objectName: recordObject ?? listBinding,
6568
+ sections: view.sections
6569
+ });
6570
+ if (isRec14(view.form)) {
6571
+ sites.push({
6572
+ path: `${basePath}.form.sections`,
6573
+ surface: joinWhere(label2, "form"),
6574
+ objectName: bindingOf(view.form) ?? listBinding,
6575
+ sections: view.form.sections
6576
+ });
6577
+ }
6578
+ for (const key of ["listViews", "formViews"]) {
6579
+ const container = view[key];
6580
+ if (!isRec14(container)) continue;
6581
+ for (const [subKey, sub] of Object.entries(container)) {
6582
+ if (!isRec14(sub)) continue;
6583
+ sites.push({
6584
+ path: `${basePath}.${key}.${subKey}.sections`,
6585
+ surface: joinWhere(label2, `${key}.${subKey}`),
6586
+ objectName: bindingOf(sub) ?? listBinding,
6587
+ sections: sub.sections
6588
+ });
6589
+ }
6590
+ }
6591
+ }
6592
+ function translatedObjectNames(stack) {
6593
+ const out = /* @__PURE__ */ new Set();
6594
+ const bundles = Array.isArray(stack.translations) ? stack.translations : [];
6595
+ for (const bundle of bundles) {
6596
+ if (!isRec14(bundle)) continue;
6597
+ for (const data of Object.values(bundle)) {
6598
+ if (!isRec14(data) || !isRec14(data.objects)) continue;
6599
+ for (const [objectName, node] of Object.entries(data.objects)) {
6600
+ if (isRec14(node)) out.add(objectName);
6601
+ }
6602
+ }
6603
+ }
6604
+ return out;
6605
+ }
6606
+ function suggestedName(label2) {
6607
+ const slug = label2.toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
6608
+ return slug.length > 0 ? slug : void 0;
6609
+ }
6610
+ function validateTranslatableSections(stack) {
6611
+ const findings = [];
6612
+ if (!isRec14(stack)) return findings;
6613
+ const translated = translatedObjectNames(stack);
6614
+ if (translated.size === 0) return findings;
6615
+ const sites = [];
6616
+ for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
6617
+ const objectName = strName14(obj.name);
6618
+ if (!objectName) continue;
6619
+ for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
6620
+ collectViewSites(
6621
+ { ...view, object: strName14(view.object) ?? objectName },
6622
+ path,
6623
+ viewLabel(view),
6624
+ sites
6625
+ );
6626
+ }
6627
+ if (isRec14(obj.listViews)) {
6628
+ collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
6629
+ }
6630
+ }
6631
+ for (const { rec: view, path } of collectionEntries(stack.views, "views")) {
6632
+ collectViewSites(view, path, viewLabel(view), sites);
6633
+ }
6634
+ for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
6635
+ const pageName = strName14(page.name);
6636
+ const pageLabel = pageName ? `page "${pageName}"` : "";
6637
+ for (const walked of walkPageComponents(page, pagePath)) {
6638
+ if (!walked.objectName) continue;
6639
+ const props = isRec14(walked.component.properties) ? walked.component.properties : void 0;
6640
+ if (!props) continue;
6641
+ const type = strName14(walked.component.type) ?? "component";
6642
+ sites.push({
6643
+ path: `${walked.path}.properties.sections`,
6644
+ surface: joinWhere(pageLabel, type),
6645
+ objectName: walked.objectName,
6646
+ sections: props.sections
6647
+ });
6648
+ }
6649
+ }
6650
+ for (const site of sites) {
6651
+ const objectName = site.objectName;
6652
+ if (!objectName || !translated.has(objectName)) continue;
6653
+ if (!Array.isArray(site.sections)) continue;
6654
+ for (let i = 0; i < site.sections.length; i++) {
6655
+ const section = site.sections[i];
6656
+ if (!isRec14(section)) continue;
6657
+ if (strName14(section.name)) continue;
6658
+ const heading = strName14(section.label) ?? strName14(section.title);
6659
+ if (!heading) continue;
6660
+ const slug = suggestedName(heading);
6661
+ findings.push({
6662
+ severity: "warning",
6663
+ rule: TRANSLATION_SECTION_NAME_MISSING,
6664
+ where: joinWhere(`object "${objectName}"`, site.surface, `section "${heading}"`),
6665
+ path: `${site.path}[${i}]`,
6666
+ message: `Section "${heading}" declares a label but no \`name\`. Headings resolve through \`objects.${objectName}._sections.<name>.label\`, so a section with no name has no key a bundle can carry \u2014 this heading can never be translated and renders in the source locale in EVERY locale. Object "${objectName}" IS translated, which is what makes the hole invisible: every neighbouring label resolves and only the heading stays behind. The i18n coverage report cannot see it either \u2014 it walks \`sections[].name\`, and a nameless section contributes nothing to walk.`,
6667
+ hint: `Give the section a stable \`name\` (snake_case)` + (slug ? `, e.g. \`name: '${slug}'\`` : "") + `, then translate it as \`objects.${objectName}._sections.${slug ?? "<name>"}.label\` in each locale bundle. The renderers look the heading up by name only \u2014 the name above is a suggestion to write down, never a key derived from the label, so renaming the heading later cannot break the lookup.`
6668
+ });
6669
+ }
6670
+ }
6671
+ return findings;
6672
+ }
6673
+
5882
6674
  // src/validate-ai-surface-affinity.ts
5883
6675
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
5884
- function asArray32(v) {
6676
+ function asArray36(v) {
5885
6677
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5886
6678
  if (v && typeof v === "object") {
5887
6679
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5888
6680
  }
5889
6681
  return [];
5890
6682
  }
5891
- function strName13(v) {
6683
+ function strName15(v) {
5892
6684
  return typeof v === "string" && v.length > 0 ? v : void 0;
5893
6685
  }
5894
6686
  function surfaceOf(v) {
@@ -5898,18 +6690,18 @@ function validateAiSurfaceAffinity(stack) {
5898
6690
  const findings = [];
5899
6691
  if (!stack || typeof stack !== "object") return findings;
5900
6692
  const skillsByName = /* @__PURE__ */ new Map();
5901
- for (const skill of asArray32(stack.skills)) {
5902
- const n = strName13(skill.name);
6693
+ for (const skill of asArray36(stack.skills)) {
6694
+ const n = strName15(skill.name);
5903
6695
  if (n) skillsByName.set(n, skill);
5904
6696
  }
5905
- const agents = asArray32(stack.agents);
6697
+ const agents = asArray36(stack.agents);
5906
6698
  for (let ai = 0; ai < agents.length; ai++) {
5907
6699
  const agent = agents[ai];
5908
- const agentName = strName13(agent.name) ?? `#${ai}`;
6700
+ const agentName = strName15(agent.name) ?? `#${ai}`;
5909
6701
  const agentSurface = surfaceOf(agent.surface);
5910
6702
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
5911
6703
  for (let si = 0; si < skillRefs.length; si++) {
5912
- const ref = strName13(skillRefs[si]);
6704
+ const ref = strName15(skillRefs[si]);
5913
6705
  if (!ref) continue;
5914
6706
  const skill = skillsByName.get(ref);
5915
6707
  if (!skill) continue;
@@ -5931,14 +6723,14 @@ function validateAiSurfaceAffinity(stack) {
5931
6723
  // src/validate-ai-tool-references.ts
5932
6724
  var import_system5 = require("@objectstack/spec/system");
5933
6725
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
5934
- function asArray33(v) {
6726
+ function asArray37(v) {
5935
6727
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5936
6728
  if (v && typeof v === "object") {
5937
6729
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5938
6730
  }
5939
6731
  return [];
5940
6732
  }
5941
- function strName14(v) {
6733
+ function strName16(v) {
5942
6734
  return typeof v === "string" && v.length > 0 ? v : void 0;
5943
6735
  }
5944
6736
  function distance6(a, b) {
@@ -5979,26 +6771,26 @@ function materialisesAsTool(action) {
5979
6771
  if (!ai || typeof ai !== "object") return false;
5980
6772
  const aiRec = ai;
5981
6773
  if (aiRec.exposed !== true) return false;
5982
- if (!strName14(aiRec.description)) return false;
5983
- const type = strName14(action.type);
6774
+ if (!strName16(aiRec.description)) return false;
6775
+ const type = strName16(action.type);
5984
6776
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
5985
6777
  if (type === "script") return Boolean(action.target || action.body);
5986
6778
  return Boolean(action.target);
5987
6779
  }
5988
6780
  function collectToolUniverse(stack) {
5989
6781
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
5990
- for (const tool of asArray33(stack.tools)) {
5991
- const n = strName14(tool.name);
6782
+ for (const tool of asArray37(stack.tools)) {
6783
+ const n = strName16(tool.name);
5992
6784
  if (n) universe.add(n);
5993
6785
  }
5994
6786
  const addActionFamily = (actions) => {
5995
- for (const action of asArray33(actions)) {
5996
- const n = strName14(action.name);
6787
+ for (const action of asArray37(actions)) {
6788
+ const n = strName16(action.name);
5997
6789
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
5998
6790
  }
5999
6791
  };
6000
6792
  addActionFamily(stack.actions);
6001
- for (const obj of asArray33(stack.objects)) {
6793
+ for (const obj of asArray37(stack.objects)) {
6002
6794
  addActionFamily(obj.actions);
6003
6795
  }
6004
6796
  return universe;
@@ -6006,13 +6798,13 @@ function collectToolUniverse(stack) {
6006
6798
  function collectUnexposedActionNames(stack) {
6007
6799
  const names = /* @__PURE__ */ new Set();
6008
6800
  const scan = (actions) => {
6009
- for (const action of asArray33(actions)) {
6010
- const n = strName14(action.name);
6801
+ for (const action of asArray37(actions)) {
6802
+ const n = strName16(action.name);
6011
6803
  if (n && !materialisesAsTool(action)) names.add(n);
6012
6804
  }
6013
6805
  };
6014
6806
  scan(stack.actions);
6015
- for (const obj of asArray33(stack.objects)) scan(obj.actions);
6807
+ for (const obj of asArray37(stack.objects)) scan(obj.actions);
6016
6808
  return names;
6017
6809
  }
6018
6810
  function validateAiToolReferences(stack) {
@@ -6030,13 +6822,13 @@ function validateAiToolReferences(stack) {
6030
6822
  }
6031
6823
  return universe.has(ref);
6032
6824
  };
6033
- const skills = asArray33(stack.skills);
6825
+ const skills = asArray37(stack.skills);
6034
6826
  for (let si = 0; si < skills.length; si++) {
6035
6827
  const skill = skills[si];
6036
- const skillName = strName14(skill.name) ?? `#${si}`;
6828
+ const skillName = strName16(skill.name) ?? `#${si}`;
6037
6829
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
6038
6830
  for (let ti = 0; ti < refs.length; ti++) {
6039
- const ref = strName14(refs[ti]);
6831
+ const ref = strName16(refs[ti]);
6040
6832
  if (!ref || resolves(ref)) continue;
6041
6833
  const isPattern = ref.endsWith("*");
6042
6834
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -6055,24 +6847,24 @@ function validateAiToolReferences(stack) {
6055
6847
 
6056
6848
  // src/validate-ai-agent-authoring.ts
6057
6849
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
6058
- function asArray34(v) {
6850
+ function asArray38(v) {
6059
6851
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
6060
6852
  if (v && typeof v === "object") {
6061
6853
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6062
6854
  }
6063
6855
  return [];
6064
6856
  }
6065
- function strName15(v) {
6857
+ function strName17(v) {
6066
6858
  return typeof v === "string" && v.length > 0 ? v : void 0;
6067
6859
  }
6068
6860
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
6069
6861
  function validateAiAgentAuthoring(stack) {
6070
6862
  const findings = [];
6071
6863
  if (!stack || typeof stack !== "object") return findings;
6072
- const agents = asArray34(stack.agents);
6864
+ const agents = asArray38(stack.agents);
6073
6865
  for (let ai = 0; ai < agents.length; ai++) {
6074
6866
  const agent = agents[ai];
6075
- const name = strName15(agent.name) ?? `#${ai}`;
6867
+ const name = strName17(agent.name) ?? `#${ai}`;
6076
6868
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
6077
6869
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
6078
6870
  findings.push({
@@ -6088,15 +6880,15 @@ function validateAiAgentAuthoring(stack) {
6088
6880
  }
6089
6881
 
6090
6882
  // src/validate-hook-body-writes.ts
6091
- var import_node_module3 = require("module");
6883
+ var import_node_module4 = require("module");
6092
6884
  var import_shared = require("@objectstack/spec/shared");
6093
- var import_meta3 = {};
6885
+ var import_meta4 = {};
6094
6886
  var cachedTs2 = null;
6095
6887
  function loadTypeScript2() {
6096
6888
  if (cachedTs2) return cachedTs2;
6097
- const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
6889
+ const anchor = typeof import_meta4 !== "undefined" && import_meta4.url ? import_meta4.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
6098
6890
  try {
6099
- cachedTs2 = (0, import_node_module3.createRequire)(anchor)("typescript");
6891
+ cachedTs2 = (0, import_node_module4.createRequire)(anchor)("typescript");
6100
6892
  } catch (err) {
6101
6893
  throw new Error(
6102
6894
  `@objectstack/lint: checking an L2 (language:'js') hook body requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a hook with a JS body is validated.`
@@ -6177,24 +6969,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
6177
6969
  "owner",
6178
6970
  "record_type"
6179
6971
  ]);
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)) {
6972
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6973
+ function asArray39(v) {
6974
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
6975
+ if (isRec15(v)) {
6184
6976
  return Object.entries(v).map(([name, def]) => ({
6185
6977
  name,
6186
- ...isRec10(def) ? def : {}
6978
+ ...isRec15(def) ? def : {}
6187
6979
  }));
6188
6980
  }
6189
6981
  return [];
6190
6982
  }
6191
6983
  function indexObjectFields2(stack) {
6192
6984
  const out = /* @__PURE__ */ new Map();
6193
- for (const obj of asArray35(stack.objects)) {
6985
+ for (const obj of asArray39(stack.objects)) {
6194
6986
  const name = typeof obj.name === "string" ? obj.name : void 0;
6195
6987
  if (!name) continue;
6196
6988
  const names = /* @__PURE__ */ new Set();
6197
- for (const f of asArray35(obj.fields)) {
6989
+ for (const f of asArray39(obj.fields)) {
6198
6990
  if (typeof f.name === "string" && f.name) names.add(f.name);
6199
6991
  }
6200
6992
  out.set(name, names);
@@ -6324,12 +7116,12 @@ ${source}
6324
7116
  }
6325
7117
  function validateHookBodyWrites(stack) {
6326
7118
  const findings = [];
6327
- const hooks = asArray35(stack.hooks);
7119
+ const hooks = asArray39(stack.hooks);
6328
7120
  if (hooks.length === 0) return findings;
6329
7121
  let objectFields = null;
6330
7122
  hooks.forEach((hook, hookIndex) => {
6331
7123
  const body = hook.body;
6332
- if (!isRec10(body) || body.language !== "js") return;
7124
+ if (!isRec15(body) || body.language !== "js") return;
6333
7125
  const source = body.source;
6334
7126
  if (typeof source !== "string" || source.trim() === "") return;
6335
7127
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -6410,13 +7202,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
6410
7202
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
6411
7203
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
6412
7204
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
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)) {
7205
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7206
+ function asArray40(v) {
7207
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
7208
+ if (isRec16(v)) {
6417
7209
  return Object.entries(v).map(([name, def]) => ({
6418
7210
  name,
6419
- ...isRec11(def) ? def : {}
7211
+ ...isRec16(def) ? def : {}
6420
7212
  }));
6421
7213
  }
6422
7214
  return [];
@@ -6430,11 +7222,11 @@ function collectActionBodies(stack) {
6430
7222
  const sites = [];
6431
7223
  const seen = /* @__PURE__ */ new Set();
6432
7224
  const collect = (actions, pathPrefix, parentObject) => {
6433
- asArray36(actions).forEach((action, index) => {
7225
+ asArray40(actions).forEach((action, index) => {
6434
7226
  const type = typeof action.type === "string" ? action.type : "script";
6435
7227
  if (type !== "script") return;
6436
7228
  const body = action.body;
6437
- if (!isRec11(body) || body.language !== "js") return;
7229
+ if (!isRec16(body) || body.language !== "js") return;
6438
7230
  const source = body.source;
6439
7231
  if (typeof source !== "string" || source.trim() === "") return;
6440
7232
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -6445,7 +7237,7 @@ function collectActionBodies(stack) {
6445
7237
  });
6446
7238
  };
6447
7239
  collect(stack.actions, "actions");
6448
- asArray36(stack.objects).forEach((obj, objIndex) => {
7240
+ asArray40(stack.objects).forEach((obj, objIndex) => {
6449
7241
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
6450
7242
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
6451
7243
  });
@@ -6453,7 +7245,7 @@ function collectActionBodies(stack) {
6453
7245
  }
6454
7246
  function validateActionBodyWrites(stack) {
6455
7247
  const findings = [];
6456
- if (!isRec11(stack)) return findings;
7248
+ if (!isRec16(stack)) return findings;
6457
7249
  const sites = collectActionBodies(stack);
6458
7250
  if (sites.length === 0) return findings;
6459
7251
  let objectFields = null;
@@ -6512,13 +7304,13 @@ var import_shared3 = require("@objectstack/spec/shared");
6512
7304
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
6513
7305
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
6514
7306
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
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)) {
7307
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7308
+ function asArray41(v) {
7309
+ if (Array.isArray(v)) return v.filter((x) => isRec17(x));
7310
+ if (isRec17(v)) {
6519
7311
  return Object.entries(v).map(([name, def]) => ({
6520
7312
  name,
6521
- ...isRec12(def) ? def : {}
7313
+ ...isRec17(def) ? def : {}
6522
7314
  }));
6523
7315
  }
6524
7316
  return [];
@@ -6531,8 +7323,8 @@ function readLiteralObjectName2(config) {
6531
7323
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
6532
7324
  function validateFlowNodeWrites(stack) {
6533
7325
  const findings = [];
6534
- if (!isRec12(stack)) return findings;
6535
- const flows = asArray37(stack.flows);
7326
+ if (!isRec17(stack)) return findings;
7327
+ const flows = asArray41(stack.flows);
6536
7328
  if (flows.length === 0) return findings;
6537
7329
  let objectFields = null;
6538
7330
  flows.forEach((flow, flowIndex) => {
@@ -6540,10 +7332,10 @@ function validateFlowNodeWrites(stack) {
6540
7332
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
6541
7333
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
6542
7334
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
6543
- const config = isRec12(node.config) ? node.config : void 0;
7335
+ const config = isRec17(node.config) ? node.config : void 0;
6544
7336
  if (!config) return;
6545
7337
  const fields = config.fields;
6546
- if (!isRec12(fields)) return;
7338
+ if (!isRec17(fields)) return;
6547
7339
  const written = Object.keys(fields);
6548
7340
  if (written.length === 0) return;
6549
7341
  const objectName = readLiteralObjectName2(config);
@@ -6590,6 +7382,14 @@ var REFERENCE_INTEGRITY_RULES = [
6590
7382
  // `component` (an unregistered ref renders a named diagnostic, not silence).
6591
7383
  { name: "validateNavTargetRefs", run: validateNavTargetRefs },
6592
7384
  { name: "validateTranslationReferences", run: validateTranslationReferences },
7385
+ // The same family from the other end (#5417). Its sibling above asks "does
7386
+ // this bundle key resolve?"; this one asks "is there a key at all?" — a form
7387
+ // section authored with a `label` and no `name` renders a heading that
7388
+ // `_sections` (keyed by name) can never address, so neither the orphan check
7389
+ // nor the coverage walk can see it. A reference that cannot be written is
7390
+ // still a reference question, and warning-only for the same reason its
7391
+ // sibling is: one heading stays in the source locale, nothing breaks.
7392
+ { name: "validateTranslatableSections", run: validateTranslatableSections },
6593
7393
  { name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
6594
7394
  { name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
6595
7395
  { name: "validateAiToolReferences", run: validateAiToolReferences },
@@ -6675,7 +7475,8 @@ function validateReferenceIntegrity(stack) {
6675
7475
  }
6676
7476
 
6677
7477
  // src/lint-flow-patterns.ts
6678
- function asArray38(v) {
7478
+ var import_automation5 = require("@objectstack/spec/automation");
7479
+ function asArray42(v) {
6679
7480
  if (Array.isArray(v)) return v;
6680
7481
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6681
7482
  return [];
@@ -6698,6 +7499,7 @@ var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
6698
7499
  var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
6699
7500
  var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
6700
7501
  var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
7502
+ var FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED = "flow-approval-revise-target-not-service-owned";
6701
7503
  var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
6702
7504
  var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
6703
7505
  var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
@@ -6705,6 +7507,7 @@ var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
6705
7507
  var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
6706
7508
  var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
6707
7509
  var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
7510
+ var FLOW_MULTI_WRITE_UNFILTERED = "flow-multi-write-unfiltered";
6708
7511
  var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
6709
7512
  "decision",
6710
7513
  "assignment",
@@ -6727,6 +7530,36 @@ var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
6727
7530
  "end"
6728
7531
  ]);
6729
7532
  var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
7533
+ var RUNAS_EFFECTIVE_IDENTITY = "`runAs:'user'` (the default when none is declared)";
7534
+ function findDataNodeAnywhere(nodes, edges) {
7535
+ for (const graph of (0, import_automation5.collectFlowGraphs)({
7536
+ nodes,
7537
+ edges
7538
+ })) {
7539
+ for (const node of graph.nodes) {
7540
+ if (DATA_NODE_TYPES.has(typeof node.type === "string" ? node.type : "")) {
7541
+ return { node, scope: graph.scope };
7542
+ }
7543
+ }
7544
+ }
7545
+ return null;
7546
+ }
7547
+ var BULK_WRITE_CONSEQUENCE = /* @__PURE__ */ new Map([
7548
+ ["delete_record", {
7549
+ verb: "deleted",
7550
+ engineCall: "driver.deleteMany",
7551
+ // The delete dispatch is the one that is EXTRACTED and case-set-pinned
7552
+ // (`engine-delete-dispatch.ts`), so it can be cited by name.
7553
+ dispatchNote: "the engine's delete-dispatch case-set lists `multi with no predicate at all` as a legal `multi` call"
7554
+ }],
7555
+ ["update_record", {
7556
+ verb: "overwritten",
7557
+ engineCall: "driver.updateMany",
7558
+ // Update has no extracted dispatch module, so the branch itself is the
7559
+ // authority — and its refusal fires only WITHOUT the declaration.
7560
+ dispatchNote: "the engine takes its bulk branch on `options.multi` alone (`Update requires an ID or options.multi=true` is refused only when the declaration is absent)"
7561
+ }]
7562
+ ]);
6730
7563
  var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
6731
7564
  var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
6732
7565
  function isScheduleTriggered(flow, startCfg) {
@@ -6811,7 +7644,7 @@ function collectTemplateStrings(value, key, out) {
6811
7644
  function edgeLabelOf(e) {
6812
7645
  return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
6813
7646
  }
6814
- function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
7647
+ function scanErrorLabelledEdges(at, nodes, edges, findings) {
6815
7648
  const typeById = /* @__PURE__ */ new Map();
6816
7649
  for (const n of nodes) {
6817
7650
  if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
@@ -6824,14 +7657,14 @@ function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
6824
7657
  const src = typeof e.source === "string" ? e.source : "";
6825
7658
  if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
6826
7659
  findings.push({
6827
- where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
7660
+ where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6828
7661
  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
7662
  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
7663
  rule: FLOW_ERROR_LABEL_NOT_FAULT
6831
7664
  });
6832
7665
  }
6833
7666
  }
6834
- function scanBranchRouting(flowName, nodes, edges, findings) {
7667
+ function scanBranchRouting(at, nodes, edges, findings) {
6835
7668
  const outEdgesBySource = /* @__PURE__ */ new Map();
6836
7669
  for (const e of edges) {
6837
7670
  if (e.type === "fault") continue;
@@ -6844,7 +7677,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6844
7677
  for (const e of outs) {
6845
7678
  if (e.isDefault === true && e.condition) {
6846
7679
  findings.push({
6847
- where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
7680
+ where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6848
7681
  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
7682
  hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
6850
7683
  rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
@@ -6857,7 +7690,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6857
7690
  const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
6858
7691
  if (defaults.length > 1) {
6859
7692
  findings.push({
6860
- where: `flow '${flowName}' \xB7 node '${src}'`,
7693
+ where: `${at} \xB7 node '${src}'`,
6861
7694
  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
7695
  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
7696
  rule: FLOW_MULTIPLE_DEFAULT_EDGES
@@ -6870,7 +7703,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6870
7703
  const cfg = node.config ?? {};
6871
7704
  if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
6872
7705
  findings.push({
6873
- where: `flow '${flowName}' \xB7 node '${String(node.id)}' (${nodeType})`,
7706
+ where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
6874
7707
  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
7708
  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
7709
  rule: FLOW_INERT_NODE_CONDITION
@@ -6890,7 +7723,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6890
7723
  const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
6891
7724
  if (unclaimed.length > 0) {
6892
7725
  findings.push({
6893
- where: `flow '${flowName}' \xB7 decision '${nid}'`,
7726
+ where: `${at} \xB7 decision '${nid}'`,
6894
7727
  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
7728
  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
7729
  rule: FLOW_BRANCH_LABEL_UNMATCHED,
@@ -6906,7 +7739,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6906
7739
  );
6907
7740
  if (ungated.length > 0) {
6908
7741
  findings.push({
6909
- where: `flow '${flowName}' \xB7 decision '${nid}'`,
7742
+ where: `${at} \xB7 decision '${nid}'`,
6910
7743
  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
7744
  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
7745
  rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
@@ -6914,10 +7747,39 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6914
7747
  }
6915
7748
  }
6916
7749
  }
6917
- function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6918
- const approvals = nodes.filter((n) => n.type === "approval");
7750
+ function filterCarriesNoCondition(filter) {
7751
+ if (filter === void 0 || filter === null) return true;
7752
+ if (typeof filter !== "object" || Array.isArray(filter)) return false;
7753
+ return Object.keys(filter).length === 0;
7754
+ }
7755
+ function scanUnboundedBulkWrites(at, nodes, findings) {
7756
+ for (const node of nodes) {
7757
+ const nodeType = typeof node.type === "string" ? node.type : "";
7758
+ const consequence2 = BULK_WRITE_CONSEQUENCE.get(nodeType);
7759
+ if (!consequence2) continue;
7760
+ const cfg = node.config ?? {};
7761
+ if (cfg.multi !== true) continue;
7762
+ if (!filterCarriesNoCondition(cfg.filter)) continue;
7763
+ const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
7764
+ const filterState = cfg.filter === void 0 || cfg.filter === null ? "no `filter` key" : "an EMPTY `filter`";
7765
+ findings.push({
7766
+ where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
7767
+ message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards \`where: {}\` plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` with no predicate. Nothing refuses it at run time, so the only feedback is the step's \`acted\` row count \u2014 reported AFTER the rows are gone.`,
7768
+ hint: `Write the constraint you mean into \`filter\` (e.g. \`{ status: 'closed' }\` \u2014 see examples/app-showcase \`showcase_inquiry_purge\`, bulk intent bounded by a predicate). If emptying '${objectName}' really is the intent, keep it: this is a warning, not a gate, and the run-time path stays open. Distinct from the #3810 erased-condition guard, which REFUSES this node at run time when a condition you WROTE interpolated to nothing \u2014 that guard is keyed on "a written condition is gone" and deliberately not on "the filter is empty", which is the fact this rule judges at authoring time. (#5482, #5393)`,
7769
+ // Warning, not `error`: see the severity policy at the top of this file.
7770
+ // The shape has a legitimate reading the engine grants on purpose, so it is
7771
+ // not provably wrong — unlike the gating members of this family.
7772
+ rule: FLOW_MULTI_WRITE_UNFILTERED
7773
+ });
7774
+ }
7775
+ }
7776
+ function scanApprovalReviseLoops(at, nodes, edges, findings) {
7777
+ const approvals = nodes.filter((n) => n.type === import_automation5.APPROVAL_NODE_TYPE);
6919
7778
  if (approvals.length === 0) return;
6920
7779
  const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
7780
+ const nodeTypeById = new Map(
7781
+ nodes.filter((n) => typeof n.id === "string").map((n) => [n.id, typeof n.type === "string" ? n.type : ""])
7782
+ );
6921
7783
  const outEdges = /* @__PURE__ */ new Map();
6922
7784
  for (const e of edges) {
6923
7785
  const src = typeof e.source === "string" ? e.source : "";
@@ -6930,7 +7792,18 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6930
7792
  if (!aid) continue;
6931
7793
  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
7794
  if (reviseTargets.length === 0) continue;
6933
- const where = `flow '${flowName}' \xB7 approval '${aid}'`;
7795
+ const where = `${at} \xB7 approval '${aid}'`;
7796
+ for (const target of reviseTargets) {
7797
+ const targetType = nodeTypeById.get(target) ?? "";
7798
+ if (targetType === import_automation5.APPROVAL_REVISE_NODE_TYPE) continue;
7799
+ findings.push({
7800
+ where,
7801
+ severity: "error",
7802
+ message: `has a 'revise' out-edge into node '${target}' of type '${targetType || "(untyped)"}' \u2014 the revise window must be an '${import_automation5.APPROVAL_REVISE_NODE_TYPE}' node. Send-back parks the run there while the record is unlocked, and only the approvals service may continue it (submitter-only, audited, and refusing a colliding pending request); \`sendBack\` refuses any other target, so this flow's revise branch cannot run.`,
7803
+ hint: `Set node '${target}' to \`type: '${import_automation5.APPROVAL_REVISE_NODE_TYPE}'\` (drop any \`waitEventConfig\` \u2014 the window is ended by POST /api/v1/approvals/requests/:id/resubmit, not by a signal). ADR-0044 D3 originally said 'wait' here; its 2026-07-28 amendment reversed that, because a 'wait' is resumable by anyone with the run id (#3823, #3801).`,
7804
+ rule: FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED
7805
+ });
7806
+ }
6934
7807
  const cfg = a.config ?? {};
6935
7808
  if (cfg.maxRevisions === 0) {
6936
7809
  findings.push({
@@ -6958,7 +7831,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6958
7831
  findings.push({
6959
7832
  where,
6960
7833
  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.`,
7834
+ hint: `Close the loop: the 'revise' edge should reach an '${import_automation5.APPROVAL_REVISE_NODE_TYPE}' node whose resubmit edge returns to '${aid}' marked \`type: 'back'\` (ADR-0044). See examples/app-showcase showcase_budget_approval.`,
6962
7835
  rule: FLOW_APPROVAL_REVISE_DEAD_END
6963
7836
  });
6964
7837
  } else if (!returnEdges.some((e) => e.type === "back")) {
@@ -6973,7 +7846,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6973
7846
  }
6974
7847
  function lintFlowPatterns(stack) {
6975
7848
  const findings = [];
6976
- for (const flow of asArray38(stack.flows)) {
7849
+ for (const flow of asArray42(stack.flows)) {
6977
7850
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
6978
7851
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
6979
7852
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
@@ -6994,75 +7867,91 @@ function lintFlowPatterns(stack) {
6994
7867
  const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
6995
7868
  const userLessKind = userLessTriggerKind(flow, startCfg);
6996
7869
  if (userLessKind && runAs !== "system") {
6997
- const dataNode = nodes.find((n) => DATA_NODE_TYPES.has(typeof n.type === "string" ? n.type : ""));
7870
+ const dataNode = findDataNodeAnywhere(nodes, edges);
6998
7871
  if (dataNode) {
6999
- const declared = typeof flow.runAs === "string" ? `\`runAs:'${runAs}'\`` : `the default \`runAs:'user'\``;
7872
+ const at = dataNode.scope ? `, in ${dataNode.scope},` : "";
7000
7873
  findings.push({
7001
7874
  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.`,
7875
+ message: `${userLessKind}-triggered flow runs under ${RUNAS_EFFECTIVE_IDENTITY}, but a ${userLessKind} run has no trigger user \u2014 so its data node '${dataNode.node.id}' (${dataNode.node.type})${at} has no identity to scope to and will be REFUSED at run time.`,
7003
7876
  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
7877
  rule: FLOW_RUNAS_UNSCOPED,
7005
7878
  severity: "error"
7006
7879
  });
7007
7880
  }
7008
7881
  }
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
- });
7882
+ for (const graph of (0, import_automation5.collectFlowGraphs)({
7883
+ // A cast, not a parse. `FlowNodeSchema.config` is an open `z.record`, so a
7884
+ // region's contents arrive as raw authored records even in a parsed stack —
7885
+ // a nested edge `condition` may still be a bare string where a top-level
7886
+ // one is an Expression envelope. Every rule below reads both
7887
+ // (`conditionSource`), and the walk itself only touches `type` / `config`.
7888
+ // The already-guarded arrays are passed rather than `flow` itself so a
7889
+ // non-array `nodes` still cannot throw: this function promises it never does.
7890
+ nodes,
7891
+ edges
7892
+ })) {
7893
+ const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
7894
+ const graphNodes = graph.nodes;
7895
+ const graphEdges = graph.edges;
7896
+ for (const node of graphNodes) {
7897
+ const nodeWhere = `${at} \xB7 node '${node.id}' (${node.type})`;
7898
+ const cfg = node.config ?? {};
7899
+ if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
7900
+ for (const key of Object.keys(cfg)) {
7901
+ if (PHANTOM_AGG_KEYS.has(key)) {
7902
+ findings.push({
7903
+ where: nodeWhere,
7904
+ 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.`,
7905
+ 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)`,
7906
+ rule: FLOW_PHANTOM_AGGREGATION
7907
+ });
7908
+ }
7033
7909
  }
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
- });
7910
+ const strings = [];
7911
+ collectTemplateStrings(stripRegions(node.config), void 0, strings);
7912
+ for (const str4 of strings) {
7913
+ if (DOUBLE_BRACE.test(str4)) {
7914
+ findings.push({
7915
+ where: nodeWhere,
7916
+ message: `double-brace interpolation \`${str4.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
7917
+ hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
7918
+ rule: FLOW_DOUBLE_BRACE_INTERP
7919
+ });
7920
+ }
7921
+ if (BARE_DOLLAR_REF.test(str4)) {
7922
+ findings.push({
7923
+ where: nodeWhere,
7924
+ message: `\`${str4.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
7925
+ hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
7926
+ rule: FLOW_BARE_DOLLAR_REF
7927
+ });
7928
+ }
7041
7929
  }
7042
7930
  }
7931
+ scanApprovalReviseLoops(at, graphNodes, graphEdges, findings);
7932
+ scanErrorLabelledEdges(at, graphNodes, graphEdges, findings);
7933
+ scanBranchRouting(at, graphNodes, graphEdges, findings);
7934
+ scanUnboundedBulkWrites(at, graphNodes, findings);
7043
7935
  }
7044
- scanApprovalReviseLoops(flowName, nodes, edges, findings);
7045
- scanErrorLabelledEdges(flowName, nodes, edges, findings);
7046
- scanBranchRouting(flowName, nodes, edges, findings);
7047
7936
  }
7048
7937
  return findings;
7049
7938
  }
7050
7939
 
7051
7940
  // src/lint-liveness-properties.ts
7052
- var import_node_module4 = require("module");
7941
+ var import_node_module5 = require("module");
7053
7942
  var import_node_path = require("path");
7054
7943
  var import_node_fs = require("fs");
7055
- var import_meta4 = {};
7944
+ var import_meta5 = {};
7056
7945
  var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
7057
7946
  var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
7058
- function asArray39(v) {
7947
+ function asArray43(v) {
7059
7948
  if (Array.isArray(v)) return v;
7060
7949
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7061
7950
  return [];
7062
7951
  }
7063
7952
  function resolveLivenessDir() {
7064
7953
  try {
7065
- const require2 = (0, import_node_module4.createRequire)(import_meta4.url);
7954
+ const require2 = (0, import_node_module5.createRequire)(import_meta5.url);
7066
7955
  const pkgJson = require2.resolve("@objectstack/spec/package.json");
7067
7956
  const dir = (0, import_node_path.join)((0, import_node_path.dirname)(pkgJson), "liveness");
7068
7957
  return (0, import_node_fs.existsSync)(dir) ? dir : null;
@@ -7168,7 +8057,16 @@ var TYPE_COLLECTIONS = [
7168
8057
  { type: "job", key: "jobs" },
7169
8058
  { type: "email_template", key: "emailTemplates" },
7170
8059
  { type: "mapping", key: "mappings" },
7171
- { type: "translation", key: "translations" }
8060
+ { type: "translation", key: "translations" },
8061
+ // #4956 — dashboard joins the list the moment its ledger first warns on
8062
+ // anything, which is exactly the rule the comment above states. Drilling
8063
+ // `widgets` produced five warned keys (`colorVariant`, `actionUrl`,
8064
+ // `actionType`, `actionIcon`, `aria`), all under `widgets[]`; `getNested`
8065
+ // fans a dotted path out over an array level, so `widgets.colorVariant`
8066
+ // checks every widget on the dashboard. Registering it here is not optional
8067
+ // bookkeeping: without it the ledger would be newly correct and newly
8068
+ // silent, which is the shape this lint exists to prevent.
8069
+ { type: "dashboard", key: "dashboards" }
7172
8070
  ];
7173
8071
  function lintLivenessProperties(stack) {
7174
8072
  const dir = resolveLivenessDir();
@@ -7176,11 +8074,11 @@ function lintLivenessProperties(stack) {
7176
8074
  const findings = [];
7177
8075
  const objectWarn = loadWarnMap(dir, "object");
7178
8076
  const fieldWarn = loadWarnMap(dir, "field");
7179
- for (const obj of asArray39(stack.objects)) {
8077
+ for (const obj of asArray43(stack.objects)) {
7180
8078
  const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7181
8079
  if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
7182
8080
  if (fieldWarn.size > 0) {
7183
- for (const field of asArray39(obj.fields)) {
8081
+ for (const field of asArray43(obj.fields)) {
7184
8082
  const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
7185
8083
  checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
7186
8084
  }
@@ -7189,7 +8087,7 @@ function lintLivenessProperties(stack) {
7189
8087
  for (const { type, key } of TYPE_COLLECTIONS) {
7190
8088
  const warnMap = loadWarnMap(dir, type);
7191
8089
  if (warnMap.size === 0) continue;
7192
- for (const item of asArray39(stack[key])) {
8090
+ for (const item of asArray43(stack[key])) {
7193
8091
  const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
7194
8092
  checkItem(type, item, `${type} '${name}'`, warnMap, findings);
7195
8093
  }
@@ -7203,7 +8101,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7203
8101
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7204
8102
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
7205
8103
  var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
7206
- function asArray40(v) {
8104
+ function asArray44(v) {
7207
8105
  if (Array.isArray(v)) return v;
7208
8106
  if (v && typeof v === "object") {
7209
8107
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7212,9 +8110,9 @@ function asArray40(v) {
7212
8110
  }
7213
8111
  function lintAutonumberFormats(stack) {
7214
8112
  const findings = [];
7215
- for (const obj of asArray40(stack.objects)) {
8113
+ for (const obj of asArray44(stack.objects)) {
7216
8114
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7217
- const fields = asArray40(obj.fields);
8115
+ const fields = asArray44(obj.fields);
7218
8116
  const fieldMeta = /* @__PURE__ */ new Map();
7219
8117
  for (const f of fields) {
7220
8118
  if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
@@ -7279,8 +8177,8 @@ function lintAutonumberFormats(stack) {
7279
8177
  }
7280
8178
 
7281
8179
  // src/lint-view-refs.ts
7282
- var import_spec2 = require("@objectstack/spec");
7283
- function asArray41(v) {
8180
+ var import_spec3 = require("@objectstack/spec");
8181
+ function asArray45(v) {
7284
8182
  if (Array.isArray(v)) return v;
7285
8183
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7286
8184
  return [];
@@ -7308,16 +8206,16 @@ function lintViewRefs(stack) {
7308
8206
  s.add(kind);
7309
8207
  };
7310
8208
  const containers = [];
7311
- for (const v of asArray41(stack.views)) {
8209
+ for (const v of asArray45(stack.views)) {
7312
8210
  if (v.viewKind) {
7313
8211
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7314
8212
  continue;
7315
8213
  }
7316
- if (!(0, import_spec2.isAggregatedViewContainer)(v)) continue;
8214
+ if (!(0, import_spec3.isAggregatedViewContainer)(v)) continue;
7317
8215
  const object = viewContainerObjectName(v);
7318
8216
  if (object) containers.push({ object, container: v });
7319
8217
  }
7320
- for (const obj of asArray41(stack.objects)) {
8218
+ for (const obj of asArray45(stack.objects)) {
7321
8219
  const object = typeof obj.name === "string" ? obj.name : void 0;
7322
8220
  if (!object) continue;
7323
8221
  if (obj.list || obj.form || obj.listViews || obj.formViews) {
@@ -7325,7 +8223,7 @@ function lintViewRefs(stack) {
7325
8223
  }
7326
8224
  }
7327
8225
  for (const { object, container } of containers) {
7328
- const { items, collisions } = (0, import_spec2.expandViewContainerWithDiagnostics)(object, container);
8226
+ const { items, collisions } = (0, import_spec3.expandViewContainerWithDiagnostics)(object, container);
7329
8227
  for (const it of items) indexKind(it.name, it.viewKind);
7330
8228
  for (const col of collisions) {
7331
8229
  findings.push({
@@ -7371,11 +8269,11 @@ function lintViewRefs(stack) {
7371
8269
  });
7372
8270
  }
7373
8271
  };
7374
- for (const obj of asArray41(stack.objects)) {
8272
+ for (const obj of asArray45(stack.objects)) {
7375
8273
  const object = typeof obj.name === "string" ? obj.name : void 0;
7376
- for (const action of asArray41(obj.actions)) checkAction(action, object);
8274
+ for (const action of asArray45(obj.actions)) checkAction(action, object);
7377
8275
  }
7378
- for (const action of asArray41(stack.actions)) checkAction(action);
8276
+ for (const action of asArray45(stack.actions)) checkAction(action);
7379
8277
  return findings;
7380
8278
  }
7381
8279
 
@@ -7427,8 +8325,43 @@ function refOf2(def) {
7427
8325
  return def?.reference || def?.reference_to;
7428
8326
  }
7429
8327
  var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
8328
+ var UNIQUE_UNSCOPED_DECLARED_INDEX = "unique/unscoped-declared-index";
8329
+ var UNIQUE_LEGACY_ORGANIZATION_COMPOSITE = "unique/legacy-organization-composite";
8330
+ function authoredTenantColumn(obj) {
8331
+ const declared = obj?.tenancy?.tenantField;
8332
+ return typeof declared === "string" && declared.trim() ? declared.trim() : "organization_id";
8333
+ }
7430
8334
  function uniqueDeclared(u) {
7431
- return u === true || u === "global";
8335
+ return u === true || u === "global" || u === "organization";
8336
+ }
8337
+ function fieldUniqueScope(u) {
8338
+ return u === "global" ? "global" : "organization";
8339
+ }
8340
+ function indexUniqueScope(u) {
8341
+ return u === "organization" ? "organization" : "global";
8342
+ }
8343
+ function lintUnscopedDeclaredIndexes(objects) {
8344
+ const issues = [];
8345
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
8346
+ for (let i = 0; i < objects.length; i++) {
8347
+ const obj = objects[i];
8348
+ if (!obj?.name) continue;
8349
+ const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
8350
+ for (let j = 0; j < declaredIndexes.length; j++) {
8351
+ const idx = declaredIndexes[j];
8352
+ if (idx?.unique !== true) continue;
8353
+ const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
8354
+ const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
8355
+ issues.push({
8356
+ severity: "warning",
8357
+ rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
8358
+ message: `"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` \u2014 a unique index whose scope is unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; an author who meant "unique per organization" gets no per-organization constraint and no error. Protocol 18 rejects this spelling (#5082).`,
8359
+ path: `objects[${i}].indexes[${j}]`,
8360
+ fix: `State the scope: \`unique: 'global'\` (installation-wide \u2014 exactly today's behavior) or \`unique: 'organization'\` (one holder per organization \u2014 the driver prepends the NULL-safe organization key part at registration).`
8361
+ });
8362
+ }
8363
+ }
8364
+ return issues;
7432
8365
  }
7433
8366
  function lintUniqueDeclarations(objects) {
7434
8367
  const issues = [];
@@ -7448,23 +8381,70 @@ function lintUniqueDeclarations(objects) {
7448
8381
  if (singleColumnUniqueIndexes.size === 0) continue;
7449
8382
  for (const { name, def } of fieldEntries2(obj.fields)) {
7450
8383
  if (!uniqueDeclared(def?.unique)) continue;
7451
- if (def.unique === "global") continue;
7452
8384
  const idx = singleColumnUniqueIndexes.get(name);
7453
8385
  if (!idx) continue;
8386
+ const fScope = fieldUniqueScope(def.unique);
8387
+ const iScope = indexUniqueScope(idx.unique);
7454
8388
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
8389
+ const fieldSpelling = `\`unique: ${typeof def.unique === "string" ? `'${def.unique}'` : def.unique}\``;
8390
+ const indexSpelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
8391
+ let message;
8392
+ let fix;
8393
+ if (fScope === iScope) {
8394
+ const boundary = fScope === "global" ? "installation-wide" : "per-organization";
8395
+ message = `"${obj.name}.${name}" declares field-level ${fieldSpelling} AND a single-column unique index${indexLabel} (${indexSpelling}) on the same column. Both ask for the same ${boundary} boundary \u2014 the same unique index declared twice (ADR-0120 D5b). Redundant, not contradictory: drop one so the intent has a single home.`;
8396
+ fix = fScope === "global" ? `Keep ONE spelling of installation-wide uniqueness: \`unique: 'global'\` on '${name}', or the declared index \u2014 not both.` : `Keep ONE spelling of per-organization uniqueness: \`unique: 'organization'\` on '${name}' (preferred), or the declared \`'organization'\` index \u2014 not both.`;
8397
+ } else {
8398
+ const globalSide = fScope === "global" ? `field-level ${fieldSpelling}` : `declared index${indexLabel} (${indexSpelling})`;
8399
+ const orgSide = fScope === "global" ? `declared index${indexLabel} (${indexSpelling})` : `field-level ${fieldSpelling}`;
8400
+ message = `"${obj.name}.${name}" declares an installation-wide unique (${globalSide}) AND a per-organization unique (${orgSide}) on the same column \u2014 the two scopes CONTRADICT (ADR-0120 D5b). The installation-wide index is physically stricter and wins; the per-organization constraint can never be tripped, so one of the two intents you wrote is silently dead.`;
8401
+ fix = `Pick ONE scope and say it once: for installation-wide uniqueness keep \`unique: 'global'\` and drop the per-organization declaration; for per-organization uniqueness set \`unique: 'organization'\` (field-level on '${name}', or on the declared index) and drop the installation-wide one.`;
8402
+ }
7455
8403
  issues.push({
7456
8404
  severity: "warning",
7457
8405
  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.`,
8406
+ message,
7459
8407
  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.`
8408
+ fix
8409
+ });
8410
+ }
8411
+ }
8412
+ return issues;
8413
+ }
8414
+ function lintLegacyOrganizationComposites(objects) {
8415
+ const issues = [];
8416
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
8417
+ for (let i = 0; i < objects.length; i++) {
8418
+ const obj = objects[i];
8419
+ if (!obj?.name) continue;
8420
+ const tenantColumn = authoredTenantColumn(obj);
8421
+ const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
8422
+ for (let j = 0; j < declaredIndexes.length; j++) {
8423
+ const idx = declaredIndexes[j];
8424
+ if (!uniqueDeclared(idx?.unique) || idx.unique === "organization") continue;
8425
+ const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
8426
+ if (cols.length < 2) continue;
8427
+ if (!cols.includes(tenantColumn)) continue;
8428
+ const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
8429
+ const spelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
8430
+ const rest = cols.filter((c) => c !== tenantColumn);
8431
+ issues.push({
8432
+ severity: "warning",
8433
+ rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
8434
+ message: `"${obj.name}" declares index${indexLabel} [${cols.join(", ")}] with ${spelling} and lists the organization column '${tenantColumn}' itself \u2014 the hand-written per-organization composite that predates the scope vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) \u2014 which on a single-organization deployment is every row.`,
8435
+ path: `objects[${i}].indexes[${j}]`,
8436
+ fix: `State the scope instead: \`unique: 'organization'\` on this index (keep \`fields\` exactly as they are \u2014 the driver makes the listed '${tenantColumn}' NULL-safe in place rather than prepending a second organization key part). ${rest.length > 0 ? `The constraint then really is "one ${rest.join(" + ")} per organization". ` : ""}Opting in is a physical tightening: it surfaces as a \`recreate_index\` drift op gated by the duplicate pre-flight probe (ADR-0120 D4), so pre-existing duplicate NULL-organization rows block it with a report rather than failing a boot. Leaving it as-is stays valid indefinitely and forces no drift.`
7461
8437
  });
7462
8438
  }
7463
8439
  }
7464
8440
  return issues;
7465
8441
  }
7466
8442
  function lintDataModel(objects) {
7467
- const issues = lintUniqueDeclarations(objects);
8443
+ const issues = [
8444
+ ...lintUnscopedDeclaredIndexes(objects),
8445
+ ...lintUniqueDeclarations(objects),
8446
+ ...lintLegacyOrganizationComposites(objects)
8447
+ ];
7468
8448
  if (!Array.isArray(objects) || objects.length === 0) return issues;
7469
8449
  const childrenByParent = {};
7470
8450
  for (const child of objects) {
@@ -7731,6 +8711,42 @@ var AUTHORING_RULES = [
7731
8711
  runtimeTypes: ["flow"],
7732
8712
  run: (stack) => validateReferenceIntegrity(stack)
7733
8713
  },
8714
+ // ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
8715
+ // is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
8716
+ // recurse into it, so until this entry existed the 31 typed prop schemas in
8717
+ // `ComponentPropsMap` were parsed by NOTHING (#4001 批 17's `no gate`
8718
+ // verdict): an undeclared or wrongly-typed prop parsed clean, was retained,
8719
+ // and reached objectui's renderer to be ignored there. This dispatches on
8720
+ // `type` and judges the bag; unregistered types are skipped, which is a
8721
+ // required semantic (`type` is an open union — the example corpus authors 87
8722
+ // nodes of 10 types this map does not carry).
8723
+ //
8724
+ // `normalized` for a reason worth stating, since the props bag survives the
8725
+ // Zod parse UNCHANGED and both tiers would otherwise carry the same data: the
8726
+ // ADR-0087 conversion layer runs inside `normalizeStackInput`, so a converted
8727
+ // alias (`page-header-subtitle-alias` rewrites `properties.description` →
8728
+ // `subtitle`) is already canonical here and is never reported as undeclared —
8729
+ // while a schema error elsewhere in the stack cannot take these findings down
8730
+ // with it.
8731
+ //
8732
+ // Advisory, deliberately, and this is the whole shape of #5068's first step:
8733
+ // wiring the parse is the precondition for enforcement, not the enforcement
8734
+ // (#5020, one surface over). The live corpus violates the declarations in two
8735
+ // places that are open contract questions — inline i18n label maps on three
8736
+ // published platform pages (#5728) and the record picker's declared-but-unread
8737
+ // `displayField` (#5775) — so gating today would fail the platform's own pages
8738
+ // to enforce declarations the platform does not keep. The error upgrade is a
8739
+ // separate step, once the warning-period inventory is empty.
8740
+ {
8741
+ name: "validateComponentProps",
8742
+ tier: "advisory",
8743
+ input: "normalized",
8744
+ commands: ALL,
8745
+ source: "packages/lint/src/validate-component-props.ts",
8746
+ surfaces: CLI_ONLY,
8747
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8748
+ run: (stack) => validateComponentProps(stack)
8749
+ },
7734
8750
  // ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
7735
8751
  // `id`), name real CSS properties + design tokens, and carry a `large` base.
7736
8752
  {
@@ -7794,18 +8810,32 @@ var AUTHORING_RULES = [
7794
8810
  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
8811
  run: (stack) => validateCapabilityReferences(stack)
7796
8812
  },
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.
8813
+ // A flow that LOOKS armed and never launches — silently. Reads the pre-parse
8814
+ // tier so an author sees what they wrote.
8815
+ //
8816
+ // `gating` since #5762, which reviewed the file's rules as one family and
8817
+ // split them on a single question: is THIS STACK enough to know the flow is
8818
+ // dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
8819
+ // the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
8820
+ // predicate cannot route at all, and a `record-*` triggerType outside the
8821
+ // closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
8822
+ // can be changed by installing a package, so there is no reading under which
8823
+ // the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
8824
+ // (the object may come from another installed package — a hedge this rule
8825
+ // cannot decide), as did `flow-draft-status-ambiguous` (draft flows DO fire;
8826
+ // that one is ambiguity of intent, not a dead flow).
7800
8827
  {
7801
8828
  name: "validateFlowTriggerReadiness",
7802
- tier: "advisory",
8829
+ tier: "gating",
7803
8830
  input: "normalized",
7804
8831
  commands: ALL,
7805
8832
  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).
8833
+ // Runtime publish gate (#4463): the FLOW family. Its `error` findings now
8834
+ // REFUSE a `state: 'active'` write (P1 gates on `error` only); the rules that
8835
+ // stayed `warning` keep being logged as advisories. The gate judges a
8836
+ // snapshot whose `flows` holds only the written item and subtracts the
8837
+ // baseline's findings, so this refuses the dead flow's own publish — never
8838
+ // another flow's save on account of a stored one.
7809
8839
  surfaces: CLI_AND_RUNTIME,
7810
8840
  runtimeTypes: ["flow"],
7811
8841
  run: (stack) => validateFlowTriggerReadiness(stack)
@@ -8016,9 +9046,31 @@ var AUTHORING_RULES = [
8016
9046
  hint: f.hint
8017
9047
  }))
8018
9048
  },
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).
9049
+ // ADR-0120 D5a — a declared index with bare `unique: true` states no scope
9050
+ // at all (`unique/unscoped-declared-index` the #4986 trap). Fires on the
9051
+ // spelling alone, no tenancy inference; 17.x warns, protocol 18 rejects the
9052
+ // spelling (#5082).
9053
+ {
9054
+ name: "lintUnscopedDeclaredIndexes",
9055
+ tier: "advisory",
9056
+ input: "parsed",
9057
+ commands: ["validate", "build"],
9058
+ source: "packages/lint/src/data-model-rules.ts",
9059
+ surfaces: CLI_ONLY,
9060
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9061
+ scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly ahead of R10 in 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.",
9062
+ run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9063
+ severity: f.severity === "suggestion" ? "info" : f.severity,
9064
+ rule: f.rule,
9065
+ where: f.path,
9066
+ path: f.path,
9067
+ message: f.message,
9068
+ hint: f.fix ?? ""
9069
+ }))
9070
+ },
9071
+ // #3991 / ADR-0120 D5b — a column carrying BOTH a field-level `unique` and a
9072
+ // single-column declared unique index states two scopes of which at most one
9073
+ // takes effect (`unique/double-declaration`, the four-quadrant matrix).
8022
9074
  {
8023
9075
  name: "lintUniqueDeclarations",
8024
9076
  tier: "advisory",
@@ -8037,6 +9089,28 @@ var AUTHORING_RULES = [
8037
9089
  hint: f.fix ?? ""
8038
9090
  }))
8039
9091
  },
9092
+ // ADR-0120 D5c — a declared unique listing the organization column IS the
9093
+ // hand-written per-organization composite (S6). Advisory nudge toward the
9094
+ // `'organization'` respelling, which is also what closes its NULL hole
9095
+ // (#5030). Never auto-fixed: opting in is a real D4 tightening.
9096
+ {
9097
+ name: "lintLegacyOrganizationComposites",
9098
+ tier: "advisory",
9099
+ input: "parsed",
9100
+ commands: ["validate", "build"],
9101
+ source: "packages/lint/src/data-model-rules.ts",
9102
+ surfaces: CLI_ONLY,
9103
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9104
+ scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly alongside R10/R11 in 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.",
9105
+ run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9106
+ severity: f.severity === "suggestion" ? "info" : f.severity,
9107
+ rule: f.rule,
9108
+ where: f.path,
9109
+ path: f.path,
9110
+ message: f.message,
9111
+ hint: f.fix ?? ""
9112
+ }))
9113
+ },
8040
9114
  // ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
8041
9115
  // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
8042
9116
  // binding gate, vocabulary freeze), moving the failure from a runtime deny to
@@ -8064,6 +9138,88 @@ var AUTHORING_RULES = [
8064
9138
  surfaces: CLI_ONLY,
8065
9139
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8066
9140
  run: (stack) => validateOrgAxisRedLines(stack)
9141
+ },
9142
+ // #4698 — the "declared but never read" gate, for the one surface where the
9143
+ // predicate is EXACT rather than inferred. A sharing rule's `condition` has a
9144
+ // single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
9145
+ // the key is `compileCelToFilter(condition, { variables: {} })`; a condition
9146
+ // that does not lower means the rule is SKIPPED at boot, so the grant is
9147
+ // declared and does not exist. The lint calls that same compiler, from the
9148
+ // same package, with the same options — the verdict cannot drift from the
9149
+ // consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
9150
+ // docblock states: the whole authorable surface is enforced, and this was the
9151
+ // one field where that sentence was not yet true.
9152
+ {
9153
+ name: "validateSharingRuleEnforceability",
9154
+ tier: "gating",
9155
+ input: "parsed",
9156
+ commands: ALL,
9157
+ source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
9158
+ surfaces: CLI_ONLY,
9159
+ surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. The rule itself is snapshot-safe \u2014 it reads ONLY `stack.sharingRules[].condition` and needs no other collection \u2014 so widening it here is a `runtimeTypes: ['sharing_rule']` edit once the gate accepts that type, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
9160
+ run: (stack) => validateSharingRuleEnforceability(stack)
9161
+ },
9162
+ // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
9163
+ // which had never been wired to anything: `isSupportedRlsExpression` existed
9164
+ // solely so an authoring command could reject a predicate the runtime drops,
9165
+ // and no authoring command called it. An unlowerable
9166
+ // `rowLevelSecurity[].using` is DROPPED by `RLSCompiler` and — when it is the
9167
+ // only applicable policy — replaced by `RLS_DENY_FILTER`, so the policy reads
9168
+ // as an authorization and behaves as a blanket refusal. Same construction as
9169
+ // the sharing-rule entry: the verdict is the runtime's own function, reached
9170
+ // through `@objectstack/formula` (where #4983 hoisted it), never a model of it.
9171
+ {
9172
+ name: "validateRlsPredicateEnforceability",
9173
+ tier: "gating",
9174
+ input: "parsed",
9175
+ commands: ALL,
9176
+ source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
9177
+ surfaces: CLI_ONLY,
9178
+ surfaceReason: "P2 (#4463): the rule reads `stack.permissions[]`, a stack-wide collection the per-write snapshot does not carry, and P1 gates `flow` alone. It is otherwise snapshot-ready \u2014 it needs no other collection \u2014 so widening it is a `runtimeTypes: ['permission_set']` edit once the gate builds that snapshot, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
9179
+ run: (stack) => validateRlsPredicateEnforceability(stack)
9180
+ },
9181
+ // #4762 — the same "declared but enforces nothing" question, for the two
9182
+ // STATIC artifacts an object validation rule carries. A `format` rule's
9183
+ // `regex` that `new RegExp(...)` throws on, and a `json_schema` rule's schema
9184
+ // ajv cannot compile, are both logged and SKIPPED on the write path
9185
+ // (`rule-validator.ts`), so the rule ships, lists, and protects nothing.
9186
+ // Neither needs a record to judge, so the authoring door is the right one:
9187
+ // rejecting a broken regex at RUNTIME instead would reject every write
9188
+ // touching that field for as long as the metadata is deployed (#4762's own
9189
+ // analysis — the runtime-backstop question stays open for the maintainer).
9190
+ // Gating for the `lint-flow-patterns.ts` bar: no reading of the metadata
9191
+ // behaves as written, because the rule does not run at all.
9192
+ {
9193
+ name: "validateRuleCompilability",
9194
+ tier: "gating",
9195
+ input: "parsed",
9196
+ commands: ALL,
9197
+ source: "packages/lint/src/validate-rule-compilability.ts",
9198
+ surfaces: CLI_ONLY,
9199
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9200
+ run: (stack) => validateRuleCompilability(stack)
9201
+ },
9202
+ // #5178 — the residual half of #5029, which registering `ajv-formats` does
9203
+ // NOT close: under `strict: false` a MISSPELLED format name (`emial`) is
9204
+ // logged once and DROPPED, so the rule compiles, ships, runs on every write
9205
+ // and enforces nothing for the keyword its author wrote — and the record is
9206
+ // accepted, which is the silent direction. Deliberately its own entry rather
9207
+ // than a third finding inside the rule above: that one's whole contract is
9208
+ // compiling in the runtime's exact environment, and a typo'd format compiles
9209
+ // there. This judges the format NAME against the registered set (enumerated
9210
+ // from the same ajv instance, never a hardcoded list) and compiles nothing,
9211
+ // so the #4762/#5029 compile parity is untouched — a judgement beside the
9212
+ // compile, not a divergent compile. Gating for the `lint-flow-patterns.ts`
9213
+ // bar: no reading of the metadata behaves as written.
9214
+ {
9215
+ name: "validateRuleSchemaFormats",
9216
+ tier: "gating",
9217
+ input: "parsed",
9218
+ commands: ALL,
9219
+ source: "packages/lint/src/validate-rule-schema-formats.ts",
9220
+ surfaces: CLI_ONLY,
9221
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9222
+ run: (stack) => validateRuleSchemaFormats(stack)
8067
9223
  }
8068
9224
  ];
8069
9225
  function authoringRulesFor(command) {
@@ -8174,6 +9330,7 @@ function runRuntimeAuthoringRules(args) {
8174
9330
  APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
8175
9331
  APPROVAL_APPROVER_TYPE_DEPRECATED,
8176
9332
  APPROVAL_APPROVER_TYPE_UNKNOWN,
9333
+ APPROVAL_APPROVER_TYPE_UNSUPPORTED,
8177
9334
  APPROVAL_DECISION_OUTPUTS_RESERVED,
8178
9335
  APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
8179
9336
  APPROVAL_EXPRESSION_INVALID,
@@ -8192,15 +9349,19 @@ function runRuntimeAuthoringRules(args) {
8192
9349
  CHART_DIMENSION_UNKNOWN,
8193
9350
  CHART_FIELD_UNKNOWN,
8194
9351
  CHART_MEASURE_UNKNOWN,
9352
+ COMPONENT_PROPS_INVALID,
9353
+ COMPONENT_PROPS_UNKNOWN_KEY,
8195
9354
  DASHBOARD_ACTION_ROUTE_UNRESOLVED,
8196
9355
  DASHBOARD_ACTION_TARGET_UNDEFINED,
8197
9356
  DASHBOARD_FILTER_FIELD_UNKNOWN,
8198
9357
  EXPRESSION_INVALID,
8199
9358
  FIELD_GROUP_EMPTY,
9359
+ FIELD_GROUP_SHADOWED,
8200
9360
  FIELD_GROUP_UNDECLARED,
8201
9361
  FILTER_TOKEN_UNKNOWN,
8202
9362
  FLOW_APPROVAL_REVISE_DEAD_END,
8203
9363
  FLOW_APPROVAL_REVISE_DISABLED,
9364
+ FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED,
8204
9365
  FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
8205
9366
  FLOW_BARE_DOLLAR_REF,
8206
9367
  FLOW_BRANCH_LABEL_UNMATCHED,
@@ -8212,12 +9373,16 @@ function runRuntimeAuthoringRules(args) {
8212
9373
  FLOW_ERROR_LABEL_NOT_FAULT,
8213
9374
  FLOW_INERT_NODE_CONDITION,
8214
9375
  FLOW_MULTIPLE_DEFAULT_EDGES,
9376
+ FLOW_MULTI_WRITE_UNFILTERED,
8215
9377
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
8216
9378
  FLOW_PHANTOM_AGGREGATION,
8217
9379
  FLOW_RUNAS_UNSCOPED,
8218
9380
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
8219
9381
  FLOW_TEMPLATE_UNKNOWN_FIELD,
8220
9382
  FLOW_TIME_RELATIVE_ANTIPATTERN,
9383
+ FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
9384
+ FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
9385
+ FLOW_TRIGGER_UNKNOWN_EVENT,
8221
9386
  FLOW_TRIGGER_UNKNOWN_OBJECT,
8222
9387
  FLOW_UPDATE_READONLY_FIELD,
8223
9388
  FLOW_UPDATE_READONLY_WHEN_FIELD,
@@ -8232,6 +9397,7 @@ function runRuntimeAuthoringRules(args) {
8232
9397
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
8233
9398
  LIVENESS_DEAD_PROPERTY,
8234
9399
  LIVENESS_EXPERIMENTAL_PROPERTY,
9400
+ MAX_SCHEMA_WALK_DEPTH,
8235
9401
  MEASURE_AGGREGATE_INCOHERENT,
8236
9402
  NAV_OBJECT_UNGRANTED,
8237
9403
  NAV_TARGET_UNRESOLVED,
@@ -8245,14 +9411,19 @@ function runRuntimeAuthoringRules(args) {
8245
9411
  REACT_BLOCK_NEEDS_RECORD_CONTEXT,
8246
9412
  REACT_CHART_AGGREGATE_INVALID,
8247
9413
  REACT_CHART_AXIS_UNKNOWN,
9414
+ REACT_CHART_DRILLDOWN_INVALID,
8248
9415
  REACT_CHART_FIELD_UNKNOWN,
8249
9416
  REFERENCE_INTEGRITY_RULES,
9417
+ RLS_PREDICATE_UNENFORCEABLE,
9418
+ RLS_PREDICATE_UNPARSEABLE,
9419
+ RUNTIME_AJV_OPTIONS,
8250
9420
  SEARCHABLE_FIELD_UNKNOWN,
8251
9421
  SEARCHABLE_FIELD_UNSEARCHABLE,
8252
9422
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
8253
9423
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
8254
9424
  SECURITY_DELEGATION_MISSING_REASON,
8255
9425
  SECURITY_EXTERNAL_WIDER,
9426
+ SECURITY_FLS_UNQUALIFIED_KEY,
8256
9427
  SECURITY_GRANT_EXPIRED_AT_AUTHORING,
8257
9428
  SECURITY_MASTER_DETAIL_UNGRANTED,
8258
9429
  SECURITY_OWD_ALIAS,
@@ -8263,6 +9434,8 @@ function runRuntimeAuthoringRules(args) {
8263
9434
  SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
8264
9435
  SEED_VALUE_OUTSIDE_STATE_MACHINE,
8265
9436
  SEMANTIC_ROLE_FIELD_UNKNOWN,
9437
+ SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
9438
+ SHARING_RULE_UNLOWERABLE_CONDITION,
8266
9439
  STYLE_CLASSNAME_TAILWIND,
8267
9440
  STYLE_NODE_MISSING_ID,
8268
9441
  STYLE_RESPONSIVE_NO_BASE,
@@ -8272,8 +9445,14 @@ function runRuntimeAuthoringRules(args) {
8272
9445
  TITLE_FORMAT_RETIRED,
8273
9446
  TITLE_UNRESOLVABLE,
8274
9447
  TRANSLATION_OPTION_KEY_UNKNOWN,
9448
+ TRANSLATION_SECTION_NAME_MISSING,
8275
9449
  TRANSLATION_TARGET_UNKNOWN,
8276
9450
  UNIQUE_DOUBLE_DECLARATION,
9451
+ UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
9452
+ UNIQUE_UNSCOPED_DECLARED_INDEX,
9453
+ VALIDATION_RULE_REGEX_UNCOMPILABLE,
9454
+ VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
9455
+ VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
8277
9456
  VIEW_CONTAINER_SHAPE,
8278
9457
  VIEW_KEY_COLLISION,
8279
9458
  VIEW_REF_FORM_TARGET_KIND,
@@ -8282,6 +9461,8 @@ function runRuntimeAuthoringRules(args) {
8282
9461
  VISIBILITY_ROOT_MISLAYERED,
8283
9462
  WIDGET_DATASET_UNKNOWN,
8284
9463
  WIDGET_DIMENSION_UNKNOWN,
9464
+ WIDGET_LEGACY_ANALYTICS_SHAPE,
9465
+ WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
8285
9466
  WIDGET_MEASURE_UNKNOWN,
8286
9467
  authoringRulesFor,
8287
9468
  buildAccessMatrix,
@@ -8289,12 +9470,16 @@ function runRuntimeAuthoringRules(args) {
8289
9470
  extractHookBodyWriteSet,
8290
9471
  extractHookBodyWrites,
8291
9472
  findUnguardedNullableOperands,
9473
+ isSourceAuthoredPage,
8292
9474
  lintAutonumberFormats,
8293
9475
  lintDataModel,
8294
9476
  lintFlowPatterns,
9477
+ lintLegacyOrganizationComposites,
8295
9478
  lintLivenessProperties,
8296
9479
  lintUniqueDeclarations,
9480
+ lintUnscopedDeclaredIndexes,
8297
9481
  lintViewRefs,
9482
+ nearestRegisteredFormat,
8298
9483
  nullGuardMessage,
8299
9484
  runAuthoringRules,
8300
9485
  runRuntimeAuthoringRules,
@@ -8311,6 +9496,7 @@ function runRuntimeAuthoringRules(args) {
8311
9496
  validateApprovalApprovers,
8312
9497
  validateCapabilityReferences,
8313
9498
  validateChartBindings,
9499
+ validateComponentProps,
8314
9500
  validateDashboardActionRefs,
8315
9501
  validateFilterTokens,
8316
9502
  validateFlowNodeWrites,
@@ -8333,15 +9519,21 @@ function runRuntimeAuthoringRules(args) {
8333
9519
  validateRecordTitle,
8334
9520
  validateReferenceIntegrity,
8335
9521
  validateResponsiveStyles,
9522
+ validateRlsPredicateEnforceability,
9523
+ validateRuleCompilability,
9524
+ validateRuleSchemaFormats,
8336
9525
  validateSearchableFields,
8337
9526
  validateSecurityPosture,
8338
9527
  validateSeedReplaySafety,
8339
9528
  validateSeedStateMachine,
8340
9529
  validateSemanticRoles,
9530
+ validateSharingRuleEnforceability,
8341
9531
  validateStackExpressions,
9532
+ validateTranslatableSections,
8342
9533
  validateTranslationReferences,
8343
9534
  validateViewContainers,
8344
9535
  validateVisibilityPredicates,
8345
- validateWidgetBindings
9536
+ validateWidgetBindings,
9537
+ walkPageComponents
8346
9538
  });
8347
9539
  //# sourceMappingURL=index.cjs.map