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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,12 +3,15 @@ import { isIncoherentAggregate } from "@objectstack/spec/data";
3
3
  import { ChartTypeSchema } from "@objectstack/spec/ui";
4
4
 
5
5
  // src/system-fields.ts
6
- import { FIELD_GROUP_SYSTEM_FIELDS } from "@objectstack/spec/data";
6
+ import { FIELD_GROUP_SYSTEM_FIELDS, resolveInjectedSystemColumns } from "@objectstack/spec/data";
7
7
  import { SystemFieldName } from "@objectstack/spec/system";
8
8
  var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
9
9
  ...FIELD_GROUP_SYSTEM_FIELDS,
10
10
  ...Object.values(SystemFieldName)
11
11
  ]);
12
+ function injectedColumnsFor(objectDef) {
13
+ return resolveInjectedSystemColumns(objectDef).names;
14
+ }
12
15
 
13
16
  // src/validate-widget-bindings.ts
14
17
  var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
@@ -330,21 +333,14 @@ function validateWidgetBindings(stack) {
330
333
  }
331
334
 
332
335
  // src/validate-expressions.ts
333
- import { validateExpression } from "@objectstack/formula";
336
+ import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
334
337
  import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
335
338
 
336
339
  // src/validate-null-guards.ts
337
- import { Environment } from "@marcbachmann/cel-js";
340
+ import { parseCelToAst } from "@objectstack/formula";
338
341
  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.`;
339
342
  var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
340
343
  var DEFAULT_RECORD_ROOTS = ["record", "previous"];
341
- var parseEnv;
342
- function getParseEnv() {
343
- if (!parseEnv) {
344
- parseEnv = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
345
- }
346
- return parseEnv;
347
- }
348
344
  function isNode(v) {
349
345
  return !!v && typeof v === "object" && typeof v.op === "string";
350
346
  }
@@ -477,12 +473,8 @@ function findUnguardedNullableOperands(source, opts) {
477
473
  if (typeof source !== "string" || !source.trim()) return [];
478
474
  if (opts.nullableFields.size === 0) return [];
479
475
  const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
480
- let ast;
481
- try {
482
- ast = getParseEnv().parse(source).ast;
483
- } catch {
484
- return [];
485
- }
476
+ const ast = parseCelToAst(source);
477
+ if (!ast) return [];
486
478
  const hasOperands = /* @__PURE__ */ new Set();
487
479
  collectHasOperands(ast, roots, hasOperands);
488
480
  const findings = [];
@@ -545,10 +537,14 @@ function findUnguardedNullableOperands(source, opts) {
545
537
  visit(ast, /* @__PURE__ */ new Set());
546
538
  return findings;
547
539
  }
548
- function nullGuardMessage(subject, objectName, finding) {
540
+ var OUTCOME_CLAUSE = {
541
+ "fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
542
+ "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)"
543
+ };
544
+ function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
549
545
  const owner = objectName ? `'${objectName}'` : "this object";
550
546
  const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
551
- 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}`;
547
+ 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}`;
552
548
  }
553
549
 
554
550
  // src/validate-expressions.ts
@@ -568,7 +564,7 @@ function buildFieldIndex(objects) {
568
564
  let names = [];
569
565
  if (Array.isArray(fields)) names = fields.map((f) => f.name).filter((n) => typeof n === "string");
570
566
  else if (fields && typeof fields === "object") names = Object.keys(fields);
571
- idx.set(name, names);
567
+ idx.set(name, [.../* @__PURE__ */ new Set([...names, ...injectedColumnsFor(obj)])]);
572
568
  }
573
569
  return idx;
574
570
  }
@@ -628,6 +624,19 @@ function buildNullableFieldIndex(objects) {
628
624
  }
629
625
  return idx;
630
626
  }
627
+ function readsParentRoot(source) {
628
+ const roots = collectCelRootIdentifiers(source);
629
+ return roots.ok && roots.roots.includes("parent");
630
+ }
631
+ function masterDetailCount(obj) {
632
+ let n = 0;
633
+ for (const [, def] of fieldEntries(obj)) {
634
+ if (def.type !== "master_detail") continue;
635
+ const ref = def.reference;
636
+ if (typeof ref === "string" && ref.trim() !== "") n += 1;
637
+ }
638
+ return n;
639
+ }
631
640
  function celSourceOf(raw) {
632
641
  if (typeof raw === "string") return raw;
633
642
  if (raw && typeof raw === "object") {
@@ -641,7 +650,7 @@ function rulePredicates(rule, path) {
641
650
  const out = [];
642
651
  const name = typeof rule.name === "string" ? rule.name : "?";
643
652
  const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
644
- const main = rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula;
653
+ const main = rule.condition;
645
654
  if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
646
655
  if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
647
656
  for (const branch of ["then", "otherwise"]) {
@@ -658,7 +667,7 @@ function validateStackExpressions(stack) {
658
667
  const fieldIndex = buildFieldIndex(objects);
659
668
  const fieldTypeIndex = buildFieldTypeIndex(objects);
660
669
  const nullableIndex = buildNullableFieldIndex(objects);
661
- const checkNullGuards = (where, subject, raw, objectName) => {
670
+ const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
662
671
  if (!objectName) return;
663
672
  const nullableFields = nullableIndex.get(objectName);
664
673
  if (!nullableFields || nullableFields.size === 0) return;
@@ -667,7 +676,7 @@ function validateStackExpressions(stack) {
667
676
  for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
668
677
  issues.push({
669
678
  where,
670
- message: nullGuardMessage(subject, objectName, finding),
679
+ message: nullGuardMessage(subject, objectName, finding, outcome),
671
680
  source,
672
681
  severity: "error"
673
682
  });
@@ -736,31 +745,45 @@ function validateStackExpressions(stack) {
736
745
  }
737
746
  for (const obj of objects) {
738
747
  const objectName = typeof obj.name === "string" ? obj.name : void 0;
739
- const validations = obj.validations ?? obj.validationRules;
748
+ const validations = obj.validations;
740
749
  for (const rule of asArray2(validations)) {
741
750
  const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
742
- check(where, rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula, objectName, "record");
751
+ check(where, rule.condition, objectName, "record");
743
752
  check(`${where} when`, rule.when, objectName, "record");
744
753
  for (const p of rulePredicates(rule, "")) {
745
754
  checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
746
755
  }
747
756
  }
748
757
  const fields = obj.fields;
749
- const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
750
- for (const f of fieldList) {
751
- if (f && typeof f === "object") {
752
- const fname = f.name ?? "?";
753
- for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
754
- check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
755
- }
758
+ 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]) : [];
759
+ const masters = masterDetailCount(obj);
760
+ for (const [fname, f] of fieldList) {
761
+ for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
762
+ check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
763
+ }
764
+ const roWhenSource = celSourceOf(f.readonlyWhen);
765
+ if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
766
+ issues.push({
767
+ where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
768
+ 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\`.`),
769
+ source: roWhenSource,
770
+ severity: "error"
771
+ });
756
772
  }
757
- if (f && typeof f === "object" && f.formula) {
773
+ checkNullGuards(
774
+ `object '${objectName}' \xB7 field '${fname}' requiredWhen`,
775
+ `field '${fname}' requiredWhen`,
776
+ f.requiredWhen,
777
+ objectName,
778
+ "fail-open"
779
+ );
780
+ if (f.expression) {
758
781
  const res = validateExpression(
759
782
  "value",
760
- f.formula,
783
+ f.expression,
761
784
  objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
762
785
  );
763
- const fieldWhere = `object '${objectName}' \xB7 field '${f.name ?? "?"}' formula`;
786
+ const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
764
787
  for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
765
788
  for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
766
789
  }
@@ -768,7 +791,7 @@ function validateStackExpressions(stack) {
768
791
  }
769
792
  const seenActions = /* @__PURE__ */ new Set();
770
793
  const checkAction = (where, action, objectName) => {
771
- const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0) ?? (typeof action.object === "string" ? action.object : void 0);
794
+ const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0);
772
795
  const name = typeof action.name === "string" ? action.name : "?";
773
796
  const key = `${obj ?? ""}:${name}`;
774
797
  if (seenActions.has(key)) return;
@@ -787,10 +810,10 @@ function validateStackExpressions(stack) {
787
810
  checkAction(`object '${objectName}'`, action, objectName);
788
811
  }
789
812
  }
790
- for (const rule of asArray2(stack.sharingRules)) {
791
- const ruleObj = typeof rule.object === "string" ? rule.object : void 0;
792
- const where = `sharingRule '${rule.name ?? "?"}'${ruleObj ? ` (${ruleObj})` : ""} condition`;
793
- check(where, rule.condition ?? rule.criteria ?? rule.predicate, ruleObj, "record");
813
+ for (const sharingRule of asArray2(stack.sharingRules)) {
814
+ const ruleObj = typeof sharingRule.object === "string" ? sharingRule.object : void 0;
815
+ const where = `sharingRule '${sharingRule.name ?? "?"}'${ruleObj ? ` (${ruleObj})` : ""} condition`;
816
+ check(where, sharingRule.condition, ruleObj, "record");
794
817
  }
795
818
  for (const hook of asArray2(stack.hooks)) {
796
819
  const hookName = hook.name ?? "?";
@@ -973,9 +996,12 @@ function validateFunctionalCompleteness(stack) {
973
996
  }
974
997
 
975
998
  // src/validate-flow-trigger-readiness.ts
999
+ import { TimeRelativeTriggerSchema } from "@objectstack/spec/automation";
976
1000
  var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
977
1001
  var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
978
1002
  var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
1003
+ var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
1004
+ var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
979
1005
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
980
1006
  function asArray4(v) {
981
1007
  if (Array.isArray(v)) return v;
@@ -987,6 +1013,12 @@ function asArray4(v) {
987
1013
  }
988
1014
  return [];
989
1015
  }
1016
+ function renderNonObject(v) {
1017
+ const t = typeof v;
1018
+ if (t === "string" || t === "number" || t === "boolean") return `${JSON.stringify(v)} (a ${t})`;
1019
+ if (t === "bigint") return `${String(v)}n (a bigint)`;
1020
+ return `a ${t}`;
1021
+ }
990
1022
  function startNodeOf(flow) {
991
1023
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
992
1024
  const index = nodes.findIndex((n) => n?.type === "start");
@@ -1034,10 +1066,35 @@ function validateFlowTriggerReadiness(stack) {
1034
1066
  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.`
1035
1067
  });
1036
1068
  }
1069
+ const parsed = TimeRelativeTriggerSchema.safeParse(tr);
1070
+ if (!parsed.success) {
1071
+ const problems = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message.replace(/\s+/g, " ").trim()}`).join("; ");
1072
+ findings.push({
1073
+ // `error` (#5762): the verdict is `TimeRelativeTriggerSchema`'s, and it
1074
+ // is the same schema the trigger safeParses at bind time. A descriptor
1075
+ // it refuses is refused at bind too — the sweep is never installed, on
1076
+ // every deployment, with no installed package able to change the
1077
+ // answer. Nothing is left for the author to weigh.
1078
+ severity: "error",
1079
+ rule: FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
1080
+ where: `flow "${flowName}" \u203A start node`,
1081
+ path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
1082
+ 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}`,
1083
+ 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.`
1084
+ });
1085
+ }
1037
1086
  }
1038
1087
  if (start && isRecordTriggered2 && !VALID_RECORD_TRIGGER.test((triggerType ?? "").trim())) {
1039
1088
  findings.push({
1040
- severity: "warning",
1089
+ // `error` (#5762). The token grammar is CLOSED and local: the engine
1090
+ // routes any `record-`-prefixed string to the record-change trigger by a
1091
+ // hardcoded prefix test (no registry lookup, so installing a package
1092
+ // cannot claim a new `record-*` token), and that trigger maps the token
1093
+ // with `triggerTypeToHookEvents` — the same regex this file's
1094
+ // `VALID_RECORD_TRIGGER` mirrors. Off-grammar means zero hook events,
1095
+ // which means bound-to-nothing on every deployment. Unlike an object
1096
+ // name, there is no other-package reading that rescues it.
1097
+ severity: "error",
1041
1098
  rule: FLOW_TRIGGER_UNKNOWN_EVENT,
1042
1099
  where: `flow "${flowName}" \u203A start node`,
1043
1100
  path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
@@ -1047,7 +1104,14 @@ function validateFlowTriggerReadiness(stack) {
1047
1104
  }
1048
1105
  if (start && isArrayRecordTriggered) {
1049
1106
  findings.push({
1050
- severity: "warning",
1107
+ // `error` (#5762), same id and same reason as 1c: an array maps to no
1108
+ // hook event either. The engine routes it to the record-change trigger
1109
+ // for the express purpose of making it loud, and its own comment names
1110
+ // THIS rule as the primary catch — a primary catch that only warns is
1111
+ // the "declared ≠ enforced" shape the registry's tier exists to close.
1112
+ // Multi-event arrays are deferred, not unsupported-by-accident (#3457),
1113
+ // so if they land the grammar widens here in the same commit.
1114
+ severity: "error",
1051
1115
  rule: FLOW_TRIGGER_UNKNOWN_EVENT,
1052
1116
  where: `flow "${flowName}" \u203A start node`,
1053
1117
  path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
@@ -1055,6 +1119,25 @@ function validateFlowTriggerReadiness(stack) {
1055
1119
  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).`
1056
1120
  });
1057
1121
  }
1122
+ if (start && config.timeRelative != null && typeof config.timeRelative !== "object") {
1123
+ 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;
1124
+ 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.`;
1125
+ findings.push({
1126
+ // `error` (#5762). The criterion IS the engine's routing predicate, so a
1127
+ // value that fails it is not routed to the time-relative trigger by any
1128
+ // deployment — the strongest verdict in this file, and the one case with
1129
+ // no runtime channel to fall back on (not even the bind-time warn 1b-ii
1130
+ // moves earlier). Note the two consequences below are both defects: one
1131
+ // never fires, the other silently drops the descriptor. Neither is a
1132
+ // shape the author can have meant, so both gate.
1133
+ severity: "error",
1134
+ rule: FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
1135
+ where: `flow "${flowName}" \u203A start node`,
1136
+ path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
1137
+ 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}`,
1138
+ 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.`
1139
+ });
1140
+ }
1058
1141
  if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
1059
1142
  findings.push({
1060
1143
  severity: "warning",
@@ -1882,6 +1965,8 @@ import {
1882
1965
  REACT_BLOCKS,
1883
1966
  RECORD_CONTEXT_BLOCK_TAGS,
1884
1967
  REACT_RECORD_BLOCK_ALTERNATIVES,
1968
+ ChartAggregateSchema,
1969
+ ChartDrillDownSchema,
1885
1970
  chartAggregateResultKeys,
1886
1971
  isRecordContextBlockType
1887
1972
  } from "@objectstack/spec/ui";
@@ -2090,13 +2175,13 @@ function validateSearchableFields(stack) {
2090
2175
  for (let vi = 0; vi < views.length; vi++) {
2091
2176
  const view = views[vi];
2092
2177
  if (!isRec3(view)) continue;
2093
- const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
2178
+ const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
2094
2179
  const viewObject = strName2(view.objectName) ?? strName2(view.object);
2095
2180
  if (isRec3(view.list)) {
2096
2181
  check(
2097
2182
  view.list.searchableFields,
2098
2183
  listViewObject(view.list) ?? viewObject,
2099
- `view "${viewLabel}" \u203A list`,
2184
+ `view "${viewLabel2}" \u203A list`,
2100
2185
  `views[${vi}].list.searchableFields`,
2101
2186
  "list-view searchableFields",
2102
2187
  "narrowing"
@@ -2108,7 +2193,7 @@ function validateSearchableFields(stack) {
2108
2193
  check(
2109
2194
  lv.searchableFields,
2110
2195
  listViewObject(lv) ?? viewObject,
2111
- `view "${viewLabel}" \u203A listViews.${key}`,
2196
+ `view "${viewLabel2}" \u203A listViews.${key}`,
2112
2197
  `views[${vi}].listViews.${key}.searchableFields`,
2113
2198
  "list-view searchableFields",
2114
2199
  "narrowing"
@@ -2232,8 +2317,11 @@ function sortFieldRefs(value, basePath) {
2232
2317
  }
2233
2318
  var COMPONENT_FIELD_SPECS = {
2234
2319
  "record:highlights": { props: ["fields"] },
2235
- // `sections`/`hideFields` are not in RecordDetailsProps, but every real page
2236
- // authors them (they survive because `properties` is unvalidated).
2320
+ // `sections` (object form) and `hideFields` are what every real page authors,
2321
+ // and since #5611 they are what `RecordDetailsProps` declares — this model and
2322
+ // the spec agree. (Before that, `sections` was declared as an ID `string[]`
2323
+ // and `hideFields` not at all; both survived only because `properties` is
2324
+ // unvalidated.)
2237
2325
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2238
2326
  "record:path": { props: ["statusField"] },
2239
2327
  "element:number": { props: ["field"] },
@@ -2296,7 +2384,7 @@ function indexObjectFields(stack) {
2296
2384
  }
2297
2385
  return objectFields;
2298
2386
  }
2299
- function checkFieldRefs(refs, objectName, objectFields, where, consequence = "skipped") {
2387
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
2300
2388
  const findings = [];
2301
2389
  if (!objectName) return findings;
2302
2390
  const known = objectFields.get(objectName);
@@ -2305,11 +2393,11 @@ function checkFieldRefs(refs, objectName, objectFields, where, consequence = "sk
2305
2393
  if (ref.name.includes(".")) continue;
2306
2394
  if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
2307
2395
  findings.push({
2308
- severity: consequence === "queried" ? "error" : "warning",
2396
+ severity: consequence2 === "queried" ? "error" : "warning",
2309
2397
  rule: PAGE_FIELD_UNKNOWN,
2310
2398
  where,
2311
2399
  path: ref.path,
2312
- 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."),
2400
+ 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."),
2313
2401
  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(", ")}.` : "")
2314
2402
  });
2315
2403
  }
@@ -2363,6 +2451,42 @@ function validatePageFieldBindings(stack) {
2363
2451
  return findings;
2364
2452
  }
2365
2453
 
2454
+ // src/zod-issue-format.ts
2455
+ var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2456
+ var valueAtPath = (root, path) => {
2457
+ let cur = root;
2458
+ for (const key of path) {
2459
+ if (!isRec6(cur) && !Array.isArray(cur)) return void 0;
2460
+ cur = cur[key];
2461
+ }
2462
+ return cur;
2463
+ };
2464
+ var preview = (value) => {
2465
+ let text;
2466
+ try {
2467
+ text = JSON.stringify(value) ?? String(value);
2468
+ } catch {
2469
+ text = String(value);
2470
+ }
2471
+ return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
2472
+ };
2473
+ function describeIssue(issue, root, depth = 0) {
2474
+ const value = depth === 0 ? valueAtPath(root, issue.path) : void 0;
2475
+ const seen = depth > 0 || issue.code === "custom" || issue.message.includes("received ") ? "" : value === void 0 ? " (nothing is set there)" : ` (received ${preview(value)})`;
2476
+ const armIssues = issue.code === "invalid_union" ? issue.errors : void 0;
2477
+ if (!armIssues || armIssues.length === 0) {
2478
+ return `${issue.message}${seen}`;
2479
+ }
2480
+ const arms = armIssues.map(
2481
+ (arm) => arm.map((inner) => {
2482
+ const where = inner.path.length ? `${inner.path.join(".")} \u2014 ` : "";
2483
+ return `${where}${describeIssue(inner, root, depth + 1)}`;
2484
+ }).join("; ")
2485
+ ).filter((text) => text.length > 0);
2486
+ if (arms.length === 0) return `${issue.message}${seen}`;
2487
+ return `${issue.message}${seen} \u2014 no accepted form matched: ` + arms.map((text, i) => `(${i + 1}) ${text}`).join(" ");
2488
+ }
2489
+
2366
2490
  // src/validate-react-page-props.ts
2367
2491
  var cachedTs = null;
2368
2492
  function loadTypeScript() {
@@ -2467,35 +2591,78 @@ function filterAttrValue(tsc, sf, attr) {
2467
2591
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
2468
2592
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
2469
2593
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
2470
- var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
2471
- var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2594
+ var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
2595
+ function checkChartDrillDown(raw, push2) {
2596
+ if (raw === void 0 || raw === NOT_STATIC) return;
2597
+ if (!isRec7(raw)) {
2598
+ push2(
2599
+ "error",
2600
+ REACT_CHART_DRILLDOWN_INVALID,
2601
+ `drillDown must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
2602
+ "Write drillDown={{ \u2026 }} \u2014 or, to turn the drill on with all defaults, drillDown={{}}. Omit the prop entirely to leave drill off."
2603
+ );
2604
+ return;
2605
+ }
2606
+ const parsed = ChartDrillDownSchema.safeParse(raw);
2607
+ if (parsed.success) return;
2608
+ for (const issue of parsed.error.issues) {
2609
+ const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
2610
+ push2(
2611
+ "error",
2612
+ REACT_CHART_DRILLDOWN_INVALID,
2613
+ `${at}: ${issue.message}`,
2614
+ "The drill config is declared by ChartDrillDownSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
2615
+ );
2616
+ }
2617
+ }
2618
+ function checkChartAggregate(raw, push2) {
2619
+ if (raw === void 0 || raw === NOT_STATIC) return;
2620
+ if (!isRec7(raw)) {
2621
+ push2(
2622
+ "error",
2623
+ REACT_CHART_AGGREGATE_INVALID,
2624
+ `aggregate must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
2625
+ 'Write aggregate={{ function: "count", groupBy: "<field>" }} \u2014 or bind data={\u2026} instead to chart precomputed rows.'
2626
+ );
2627
+ return;
2628
+ }
2629
+ const groupByAbsent = raw.groupBy === void 0;
2630
+ if (groupByAbsent) {
2631
+ push2(
2632
+ "warning",
2633
+ REACT_CHART_AGGREGATE_INVALID,
2634
+ "aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
2635
+ "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."
2636
+ );
2637
+ }
2638
+ const parsed = ChartAggregateSchema.safeParse(raw);
2639
+ if (parsed.success) return;
2640
+ for (const issue of parsed.error.issues) {
2641
+ if (groupByAbsent && issue.path[0] === "groupBy") continue;
2642
+ const at = issue.path.length ? `aggregate.${issue.path.join(".")}` : "aggregate";
2643
+ push2(
2644
+ "error",
2645
+ REACT_CHART_AGGREGATE_INVALID,
2646
+ `${at}: ${describeIssue(issue, raw)}`,
2647
+ "The aggregate is declared by ChartAggregateSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
2648
+ );
2649
+ }
2650
+ }
2651
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2472
2652
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
2473
2653
  function checkObjectChart(attrs, objectFields, findings) {
2474
2654
  const { values, where, path } = attrs;
2475
2655
  const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2656
+ checkChartDrillDown(values.get("drillDown"), push2);
2476
2657
  if (values.has("data")) return;
2477
2658
  const aggregate = values.get("aggregate");
2659
+ checkChartAggregate(aggregate, push2);
2478
2660
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
2479
- if (!isRec6(aggregate)) return;
2661
+ if (!isRec7(aggregate)) return;
2480
2662
  const fn = strOf(aggregate.function);
2481
2663
  const field = strOf(aggregate.field);
2482
2664
  const groupBy = aggregate.groupBy;
2483
- const groupByField = strOf(groupBy) ?? (isRec6(groupBy) ? strOf(groupBy.field) : void 0);
2484
- if (fn && !CHART_FUNCTIONS.includes(fn)) {
2485
- push2(
2486
- "error",
2487
- REACT_CHART_AGGREGATE_INVALID,
2488
- `aggregate.function "${fn}" is not an aggregation this chart can run.`,
2489
- `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
2490
- );
2491
- } else if (fn && fn !== "count" && !field) {
2492
- push2(
2493
- "error",
2494
- REACT_CHART_AGGREGATE_INVALID,
2495
- `aggregate.function "${fn}" has no "field" to aggregate.`,
2496
- 'Add aggregate.field, or use function "count" (the only one that may omit it).'
2497
- );
2498
- }
2665
+ const groupByField = strOf(groupBy) ?? (isRec7(groupBy) ? strOf(groupBy.field) : void 0);
2499
2666
  const objectName = strOf(values.get("objectName"));
2500
2667
  const known = objectName ? objectFields.get(objectName) : void 0;
2501
2668
  if (objectName && known) {
@@ -2528,18 +2695,18 @@ function checkObjectChart(attrs, objectFields, findings) {
2528
2695
  );
2529
2696
  };
2530
2697
  const xAxisRaw = values.get("xAxis");
2531
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec6(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2698
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec7(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2532
2699
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
2533
2700
  axisRef(categoryAxis, categoryProp);
2534
2701
  const yAxisRaw = values.get("yAxis");
2535
2702
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
2536
2703
  for (const a of yAxisList) {
2537
- axisRef(strOf(a) ?? (isRec6(a) ? strOf(a.field) : void 0), "yAxis[].field");
2704
+ axisRef(strOf(a) ?? (isRec7(a) ? strOf(a.field) : void 0), "yAxis[].field");
2538
2705
  }
2539
2706
  const series = values.get("series");
2540
2707
  if (Array.isArray(series)) {
2541
2708
  for (const s of series) {
2542
- if (!isRec6(s)) continue;
2709
+ if (!isRec7(s)) continue;
2543
2710
  const dataKey = strOf(s.dataKey);
2544
2711
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
2545
2712
  }
@@ -2595,7 +2762,7 @@ function subformFieldRefs(value, basePath) {
2595
2762
  if (!Array.isArray(value)) return { child, parent };
2596
2763
  for (let i = 0; i < value.length; i++) {
2597
2764
  const sub = value[i];
2598
- if (!isRec6(sub)) continue;
2765
+ if (!isRec7(sub)) continue;
2599
2766
  const at = (key) => `${basePath}[${i}].${key}`;
2600
2767
  child.push({
2601
2768
  objectName: strOf(sub.childObject),
@@ -2640,20 +2807,20 @@ function reactFieldRefs(spec, values, basePath) {
2640
2807
  }
2641
2808
  for (const key of spec.nestedFields ?? []) {
2642
2809
  const v = readable(key);
2643
- if (isRec6(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2810
+ if (isRec7(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2644
2811
  }
2645
2812
  for (const key of spec.sections ?? []) {
2646
2813
  const v = readable(key);
2647
2814
  if (!Array.isArray(v)) continue;
2648
2815
  for (let i = 0; i < v.length; i++) {
2649
2816
  const section = v[i];
2650
- if (!isRec6(section)) continue;
2817
+ if (!isRec7(section)) continue;
2651
2818
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
2652
2819
  }
2653
2820
  }
2654
2821
  for (const key of spec.keyedByField ?? []) {
2655
2822
  const v = readable(key);
2656
- if (!isRec6(v)) continue;
2823
+ if (!isRec7(v)) continue;
2657
2824
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
2658
2825
  }
2659
2826
  for (const key of spec.filterArrays ?? []) {
@@ -2924,7 +3091,7 @@ function validateSemanticRoles(stack) {
2924
3091
  const where = `object "${objName}"`;
2925
3092
  const path = `objects[${i}]`;
2926
3093
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
2927
- const fieldNames = new Set(Object.keys(fields));
3094
+ const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
2928
3095
  const declaredGroups = new Set(
2929
3096
  (Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
2930
3097
  );
@@ -3317,7 +3484,7 @@ import {
3317
3484
  normalizeDecisionOutputs
3318
3485
  } from "@objectstack/spec/automation";
3319
3486
  import { BUILTIN_MEMBERSHIP_ROLES } from "@objectstack/spec";
3320
- import { collectCelRootIdentifiers } from "@objectstack/formula";
3487
+ import { collectCelRootIdentifiers as collectCelRootIdentifiers2 } from "@objectstack/formula";
3321
3488
  var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
3322
3489
  var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
3323
3490
  var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
@@ -3392,7 +3559,7 @@ function validateApprovalApprovers(stack) {
3392
3559
  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.`
3393
3560
  });
3394
3561
  } else {
3395
- const parsed = collectCelRootIdentifiers(source);
3562
+ const parsed = collectCelRootIdentifiers2(source);
3396
3563
  if (!parsed.ok) {
3397
3564
  findings.push({
3398
3565
  severity: "error",
@@ -3650,7 +3817,7 @@ function asArray20(v) {
3650
3817
  return [];
3651
3818
  }
3652
3819
  function owdOf(obj) {
3653
- return obj.sharingModel ?? obj.security?.sharingModel;
3820
+ return obj.sharingModel;
3654
3821
  }
3655
3822
  function isSystemObject(obj) {
3656
3823
  return obj.isSystem === true || String(obj.name ?? "").startsWith("sys_");
@@ -3664,7 +3831,7 @@ function labelHasRoleWord(label2) {
3664
3831
  return /\brole(s)?\b/i.test(label2);
3665
3832
  }
3666
3833
  function refOf(def) {
3667
- const r = def.reference ?? def.reference_to;
3834
+ const r = def.reference;
3668
3835
  return typeof r === "string" && r ? r : void 0;
3669
3836
  }
3670
3837
  function firstMasterDetailField(obj) {
@@ -3957,6 +4124,7 @@ function validateSecurityPosture(stack, opts) {
3957
4124
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
3958
4125
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
3959
4126
  var ORG_PARENT_FIELD = "parent_organization_id";
4127
+ var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
3960
4128
  function asArray21(v) {
3961
4129
  if (Array.isArray(v)) return v;
3962
4130
  if (v && typeof v === "object") {
@@ -3967,6 +4135,15 @@ function asArray21(v) {
3967
4135
  function str(v) {
3968
4136
  return typeof v === "string" ? v : "";
3969
4137
  }
4138
+ function expressionText(v) {
4139
+ if (typeof v === "string") return v;
4140
+ if (v && typeof v === "object") {
4141
+ const rec = v;
4142
+ if (typeof rec.source === "string") return rec.source;
4143
+ if (rec.ast !== void 0) return JSON.stringify(rec.ast) ?? "";
4144
+ }
4145
+ return "";
4146
+ }
3970
4147
  function isTenancyDisabled(object) {
3971
4148
  const tenancy = object.tenancy;
3972
4149
  if (tenancy && typeof tenancy === "object" && tenancy.enabled === false) return true;
@@ -3978,7 +4155,7 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
3978
4155
  function validateOrgAxisRedLines(stack) {
3979
4156
  const findings = [];
3980
4157
  const cfg = stack ?? {};
3981
- const permissionSets = asArray21(cfg.permissions ?? cfg.permissionSets);
4158
+ const permissionSets = asArray21(cfg.permissions);
3982
4159
  permissionSets.forEach((ps, psIndex) => {
3983
4160
  asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
3984
4161
  for (const clause of ["using", "check"]) {
@@ -3994,53 +4171,167 @@ function validateOrgAxisRedLines(stack) {
3994
4171
  }
3995
4172
  });
3996
4173
  });
3997
- const objects = asArray21(cfg.objects);
3998
- objects.forEach((object, oIndex) => {
3999
- const objectName = str(object.name) || String(oIndex);
4000
- asArray21(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
4001
- for (const clause of ["using", "check"]) {
4002
- if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
4003
- findings.push({
4004
- severity: "error",
4005
- rule: ORG_AXIS_PERMISSION_INHERITANCE,
4006
- where: `object "${objectName}" policy "${str(policy.name) || pIndex}"`,
4007
- path: `objects[${oIndex}].rowLevelSecurity[${pIndex}].${clause}`,
4008
- 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.`,
4009
- hint: INHERITANCE_HINT
4010
- });
4011
- }
4012
- });
4013
- });
4014
- asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
4015
- const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
4016
- const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
4017
- if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
4174
+ asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
4175
+ const slots = [
4176
+ { key: "condition", text: expressionText(rule.condition) },
4177
+ { key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
4178
+ ];
4179
+ for (const slot of slots) {
4180
+ if (!slot.text.includes(ORG_PARENT_FIELD)) continue;
4018
4181
  findings.push({
4019
4182
  severity: "error",
4020
4183
  rule: ORG_AXIS_PERMISSION_INHERITANCE,
4021
4184
  where: `sharing rule "${str(rule.name) || rIndex}"`,
4022
- path: `sharingRules[${rIndex}]`,
4023
- message: `Sharing rule reads \`${ORG_PARENT_FIELD}\`, granting access by walking the organization tree. ADR-0105 D6 forbids permission inheritance along the org axis.`,
4185
+ path: `sharingRules[${rIndex}].${slot.key}`,
4186
+ 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.`,
4024
4187
  hint: INHERITANCE_HINT
4025
4188
  });
4026
4189
  }
4027
4190
  });
4028
4191
  const tenancyDisabledObjects = new Set(
4029
- objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
4192
+ asArray21(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
4030
4193
  );
4031
- asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
4032
- const target = str(rule.object ?? rule.objectName);
4194
+ asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
4195
+ const target = str(rule.object);
4033
4196
  if (!target || !tenancyDisabledObjects.has(target)) return;
4034
- const sharedTo = rule.sharedTo ?? rule.recipient;
4035
- const recipientType = str(sharedTo?.type);
4036
- if (recipientType !== "business_unit") return;
4197
+ const sharedWith = rule.sharedWith;
4198
+ const recipientType = str(sharedWith?.type);
4199
+ if (!BU_TREE_RECIPIENT_TYPES.has(recipientType)) return;
4200
+ const reach = recipientType === "unit_and_subordinates" ? "a business unit AND every descendant unit" : "a business unit";
4037
4201
  findings.push({
4038
4202
  severity: "error",
4039
4203
  rule: ORG_AXIS_CROSS_ORG_BU_GRANT,
4040
4204
  where: `sharing rule "${str(rule.name) || rIndex}" on object "${target}"`,
4041
- path: `sharingRules[${rIndex}].sharedTo`,
4042
- 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).`,
4043
- 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.`
4205
+ path: `sharingRules[${rIndex}].sharedWith`,
4206
+ 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).`,
4207
+ 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.`
4208
+ });
4209
+ });
4210
+ return findings;
4211
+ }
4212
+
4213
+ // src/validate-sharing-rule-enforceability.ts
4214
+ import { compileCelToFilter } from "@objectstack/formula";
4215
+ var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
4216
+ var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
4217
+ function asArray22(v) {
4218
+ if (Array.isArray(v)) return v;
4219
+ if (v && typeof v === "object") {
4220
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4221
+ }
4222
+ return [];
4223
+ }
4224
+ function str2(v) {
4225
+ return typeof v === "string" ? v : "";
4226
+ }
4227
+ function toCompilerInput(condition) {
4228
+ if (typeof condition === "string") return condition.trim() ? condition : null;
4229
+ if (condition && typeof condition === "object") {
4230
+ const source = condition.source;
4231
+ if (typeof source === "string" && source.trim()) return { source };
4232
+ }
4233
+ return null;
4234
+ }
4235
+ function sourceOf(condition) {
4236
+ const input = toCompilerInput(condition);
4237
+ if (typeof input === "string") return input;
4238
+ return str2(input?.source);
4239
+ }
4240
+ 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).";
4241
+ function validateSharingRuleEnforceability(stack) {
4242
+ const findings = [];
4243
+ const cfg = stack ?? {};
4244
+ asArray22(cfg.sharingRules).forEach((rule, index) => {
4245
+ const input = toCompilerInput(rule.condition);
4246
+ if (input === null) return;
4247
+ const result = compileCelToFilter(input, { variables: {} });
4248
+ if (result.ok) return;
4249
+ if (result.reason === "parse-error") return;
4250
+ const name = str2(rule.name) || String(index);
4251
+ const object = str2(rule.object);
4252
+ const where = `sharing rule "${name}"${object ? ` on object "${object}"` : ""}`;
4253
+ const path = `sharingRules[${index}].condition`;
4254
+ const source = sourceOf(rule.condition);
4255
+ 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).";
4256
+ if (result.reason === "unresolved-variable") {
4257
+ findings.push({
4258
+ severity: "error",
4259
+ rule: SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
4260
+ where,
4261
+ path,
4262
+ message: `Sharing-rule condition \`${source}\` reads a runtime variable (${result.detail}), ` + skipped,
4263
+ 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`."
4264
+ });
4265
+ return;
4266
+ }
4267
+ findings.push({
4268
+ severity: "error",
4269
+ rule: SHARING_RULE_UNLOWERABLE_CONDITION,
4270
+ where,
4271
+ path,
4272
+ message: `Sharing-rule condition \`${source}\` is outside the pushdown subset the runtime can compile (${result.detail}), ` + skipped,
4273
+ 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."
4274
+ });
4275
+ });
4276
+ return findings;
4277
+ }
4278
+
4279
+ // src/validate-rls-predicate-enforceability.ts
4280
+ import { isPushdownableCel, isSupportedRlsExpression, sqlPredicateToCel } from "@objectstack/formula";
4281
+ var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
4282
+ var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
4283
+ function asArray23(v) {
4284
+ if (Array.isArray(v)) return v;
4285
+ if (v && typeof v === "object") {
4286
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4287
+ }
4288
+ return [];
4289
+ }
4290
+ function str3(v) {
4291
+ return typeof v === "string" ? v : "";
4292
+ }
4293
+ 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.";
4294
+ function consequence(clause) {
4295
+ 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). ';
4296
+ 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.";
4297
+ }
4298
+ function validateRlsPredicateEnforceability(stack) {
4299
+ const findings = [];
4300
+ const cfg = stack ?? {};
4301
+ asArray23(cfg.permissions).forEach((ps, psIndex) => {
4302
+ asArray23(ps.rowLevelSecurity).forEach((policy, pIndex) => {
4303
+ for (const clause of ["using", "check"]) {
4304
+ const source = str3(policy[clause]);
4305
+ if (!source.trim()) continue;
4306
+ if (isSupportedRlsExpression(source)) continue;
4307
+ const why = isPushdownableCel(sqlPredicateToCel(source));
4308
+ const detail = why.ok ? "" : why.detail;
4309
+ const parseError = !why.ok && why.reason === "parse-error";
4310
+ const psName = str3(ps.name) || String(psIndex);
4311
+ const policyName = str3(policy.name) || String(pIndex);
4312
+ const object = str3(policy.object);
4313
+ const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
4314
+ const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
4315
+ if (parseError) {
4316
+ findings.push({
4317
+ severity: "error",
4318
+ rule: RLS_PREDICATE_UNPARSEABLE,
4319
+ where,
4320
+ path,
4321
+ 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),
4322
+ 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."
4323
+ });
4324
+ continue;
4325
+ }
4326
+ findings.push({
4327
+ severity: "error",
4328
+ rule: RLS_PREDICATE_UNENFORCEABLE,
4329
+ where,
4330
+ path,
4331
+ message: `RLS ${clause} \`${source}\` is outside the pushdown subset the runtime can compile (${detail}), ` + consequence(clause),
4332
+ 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."
4333
+ });
4334
+ }
4044
4335
  });
4045
4336
  });
4046
4337
  return findings;
@@ -4049,7 +4340,7 @@ function validateOrgAxisRedLines(stack) {
4049
4340
  // src/validate-dashboard-action-refs.ts
4050
4341
  var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
4051
4342
  var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
4052
- function asArray22(v) {
4343
+ function asArray24(v) {
4053
4344
  if (Array.isArray(v)) return v;
4054
4345
  if (v && typeof v === "object") {
4055
4346
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4083,14 +4374,14 @@ function collectKnownTargets(stack) {
4083
4374
  const pages = /* @__PURE__ */ new Set();
4084
4375
  const views = /* @__PURE__ */ new Set();
4085
4376
  const collectNames = (v, into, name) => {
4086
- for (const item of asArray22(v)) {
4377
+ for (const item of asArray24(v)) {
4087
4378
  if (!item || typeof item !== "object") continue;
4088
4379
  const n = name(item);
4089
4380
  if (n) into.add(n);
4090
4381
  }
4091
4382
  };
4092
4383
  collectNames(stack.actions, actions, (a) => strName5(a.name));
4093
- for (const obj of asArray22(stack.objects)) {
4384
+ for (const obj of asArray24(stack.objects)) {
4094
4385
  if (!obj || typeof obj !== "object") continue;
4095
4386
  const n = strName5(obj.name);
4096
4387
  if (n) objects.add(n);
@@ -4130,7 +4421,7 @@ function resolveUrlRoute(target, known) {
4130
4421
  function validateDashboardActionRefs(stack) {
4131
4422
  const findings = [];
4132
4423
  if (!stack || typeof stack !== "object") return findings;
4133
- const dashboards = asArray22(stack.dashboards);
4424
+ const dashboards = asArray24(stack.dashboards);
4134
4425
  if (dashboards.length === 0) return findings;
4135
4426
  const known = collectKnownTargets(stack);
4136
4427
  const checkOne = (action, where, path) => {
@@ -4170,7 +4461,7 @@ function validateDashboardActionRefs(stack) {
4170
4461
  if (!dash || typeof dash !== "object") continue;
4171
4462
  const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
4172
4463
  const dashPath = `dashboards[${di}]`;
4173
- const headerActions = asArray22(dash.header?.actions);
4464
+ const headerActions = asArray24(dash.header?.actions);
4174
4465
  for (let ai = 0; ai < headerActions.length; ai++) {
4175
4466
  const action = headerActions[ai];
4176
4467
  if (!action || typeof action !== "object") continue;
@@ -4181,18 +4472,6 @@ function validateDashboardActionRefs(stack) {
4181
4472
  `${dashPath}.header.actions[${ai}].actionUrl`
4182
4473
  );
4183
4474
  }
4184
- const widgets = asArray22(dash.widgets);
4185
- for (let wi = 0; wi < widgets.length; wi++) {
4186
- const widget = widgets[wi];
4187
- if (!widget || typeof widget !== "object") continue;
4188
- if (!strName5(widget.actionUrl)) continue;
4189
- const widgetId = strName5(widget.id) ?? `#${wi}`;
4190
- checkOne(
4191
- { actionType: widget.actionType, actionUrl: widget.actionUrl },
4192
- `dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
4193
- `${dashPath}.widgets[${wi}].actionUrl`
4194
- );
4195
- }
4196
4475
  }
4197
4476
  return findings;
4198
4477
  }
@@ -4201,7 +4480,7 @@ function validateDashboardActionRefs(stack) {
4201
4480
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
4202
4481
  var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
4203
4482
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
4204
- function asArray23(v) {
4483
+ function asArray25(v) {
4205
4484
  if (Array.isArray(v)) return v;
4206
4485
  if (v && typeof v === "object") {
4207
4486
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4270,7 +4549,7 @@ function validateFilterTokens(stack) {
4270
4549
  ["apps", "app"]
4271
4550
  ];
4272
4551
  for (const [key, kind] of surfaces) {
4273
- const items = asArray23(stack[key]);
4552
+ const items = asArray25(stack[key]);
4274
4553
  items.forEach((item, i) => {
4275
4554
  const name = label(item.name ?? item.id, `#${i}`);
4276
4555
  if (kind === "dashboard") {
@@ -4304,7 +4583,7 @@ import {
4304
4583
  var PLATFORM_NAMES = [...PLATFORM_PROVIDED_OBJECT_NAMES];
4305
4584
  var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
4306
4585
  var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
4307
- function asArray24(v) {
4586
+ function asArray26(v) {
4308
4587
  if (Array.isArray(v)) return v;
4309
4588
  if (v && typeof v === "object") {
4310
4589
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4351,7 +4630,7 @@ function distance2(a, b) {
4351
4630
  function validateObjectReferences(stack) {
4352
4631
  const findings = [];
4353
4632
  if (!stack || typeof stack !== "object") return findings;
4354
- const objects = asArray24(stack.objects);
4633
+ const objects = asArray26(stack.objects);
4355
4634
  const ownObjects = /* @__PURE__ */ new Set();
4356
4635
  for (const obj of objects) {
4357
4636
  const n = strName6(obj.name);
@@ -4384,7 +4663,7 @@ function validateObjectReferences(stack) {
4384
4663
  });
4385
4664
  };
4386
4665
  const checkActionParams2 = (action, actionPath, actionLabel) => {
4387
- const params = asArray24(action.params);
4666
+ const params = asArray26(action.params);
4388
4667
  for (let pi = 0; pi < params.length; pi++) {
4389
4668
  const param = params[pi];
4390
4669
  if (!param || typeof param !== "object") continue;
@@ -4406,7 +4685,7 @@ function validateObjectReferences(stack) {
4406
4685
  );
4407
4686
  }
4408
4687
  };
4409
- const globalActions = asArray24(stack.actions);
4688
+ const globalActions = asArray26(stack.actions);
4410
4689
  for (let ai = 0; ai < globalActions.length; ai++) {
4411
4690
  const action = globalActions[ai];
4412
4691
  if (!action || typeof action !== "object") continue;
@@ -4416,7 +4695,7 @@ function validateObjectReferences(stack) {
4416
4695
  const obj = objects[oi];
4417
4696
  if (!obj || typeof obj !== "object") continue;
4418
4697
  const objName = strName6(obj.name) ?? `#${oi}`;
4419
- const objActions = asArray24(obj.actions);
4698
+ const objActions = asArray26(obj.actions);
4420
4699
  for (let ai = 0; ai < objActions.length; ai++) {
4421
4700
  const action = objActions[ai];
4422
4701
  if (!action || typeof action !== "object") continue;
@@ -4427,12 +4706,12 @@ function validateObjectReferences(stack) {
4427
4706
  );
4428
4707
  }
4429
4708
  }
4430
- const dashboards = asArray24(stack.dashboards);
4709
+ const dashboards = asArray26(stack.dashboards);
4431
4710
  for (let di = 0; di < dashboards.length; di++) {
4432
4711
  const dash = dashboards[di];
4433
4712
  if (!dash || typeof dash !== "object") continue;
4434
4713
  const dashName = strName6(dash.name) ?? `#${di}`;
4435
- const filters = asArray24(dash.globalFilters);
4714
+ const filters = asArray26(dash.globalFilters);
4436
4715
  for (let fi = 0; fi < filters.length; fi++) {
4437
4716
  const filter = filters[fi];
4438
4717
  if (!filter || typeof filter !== "object") continue;
@@ -4447,13 +4726,13 @@ function validateObjectReferences(stack) {
4447
4726
  );
4448
4727
  }
4449
4728
  }
4450
- const apps = asArray24(stack.apps);
4729
+ const apps = asArray26(stack.apps);
4451
4730
  for (let ai = 0; ai < apps.length; ai++) {
4452
4731
  const app = apps[ai];
4453
4732
  if (!app || typeof app !== "object") continue;
4454
4733
  const appName = strName6(app.name) ?? `#${ai}`;
4455
4734
  const walkNav = (items, basePath) => {
4456
- const navItems = asArray24(items);
4735
+ const navItems = asArray26(items);
4457
4736
  for (let ni = 0; ni < navItems.length; ni++) {
4458
4737
  const nav = navItems[ni];
4459
4738
  if (!nav || typeof nav !== "object") continue;
@@ -4480,7 +4759,7 @@ function validateObjectReferences(stack) {
4480
4759
  }
4481
4760
  };
4482
4761
  walkNav(app.navigation, `apps[${ai}].navigation`);
4483
- const areas = asArray24(app.areas);
4762
+ const areas = asArray26(app.areas);
4484
4763
  for (let ri = 0; ri < areas.length; ri++) {
4485
4764
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4486
4765
  }
@@ -4490,10 +4769,10 @@ function validateObjectReferences(stack) {
4490
4769
 
4491
4770
  // src/validate-nav-target-refs.ts
4492
4771
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
4493
- var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4494
- function asArray25(v) {
4495
- if (Array.isArray(v)) return v.filter(isRec7);
4496
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
4772
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4773
+ function asArray27(v) {
4774
+ if (Array.isArray(v)) return v.filter(isRec8);
4775
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
4497
4776
  return [];
4498
4777
  }
4499
4778
  function strName7(v) {
@@ -4507,7 +4786,7 @@ var NAV_TARGETS = [
4507
4786
  ];
4508
4787
  function namesOf(collection) {
4509
4788
  const out = /* @__PURE__ */ new Set();
4510
- for (const entry of asArray25(collection)) {
4789
+ for (const entry of asArray27(collection)) {
4511
4790
  const n = strName7(entry.name);
4512
4791
  if (n) out.add(n);
4513
4792
  }
@@ -4515,8 +4794,8 @@ function namesOf(collection) {
4515
4794
  }
4516
4795
  function validateNavTargetRefs(stack) {
4517
4796
  const findings = [];
4518
- if (!isRec7(stack)) return findings;
4519
- const apps = asArray25(stack.apps);
4797
+ if (!isRec8(stack)) return findings;
4798
+ const apps = asArray27(stack.apps);
4520
4799
  if (apps.length === 0) return findings;
4521
4800
  const declared = /* @__PURE__ */ new Map();
4522
4801
  for (const [, , collection] of NAV_TARGETS) {
@@ -4527,7 +4806,7 @@ function validateNavTargetRefs(stack) {
4527
4806
  const walk = (items, basePath) => {
4528
4807
  if (!Array.isArray(items)) return;
4529
4808
  for (const [ni, raw] of items.entries()) {
4530
- if (!isRec7(raw)) continue;
4809
+ if (!isRec8(raw)) continue;
4531
4810
  const nav = raw;
4532
4811
  const navPath = `${basePath}[${ni}]`;
4533
4812
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
@@ -4550,7 +4829,7 @@ function validateNavTargetRefs(stack) {
4550
4829
  }
4551
4830
  };
4552
4831
  walk(app.navigation, `apps[${ai}].navigation`);
4553
- for (const [ari, area] of asArray25(app.areas).entries()) {
4832
+ for (const [ari, area] of asArray27(app.areas).entries()) {
4554
4833
  walk(area.items, `apps[${ai}].areas[${ari}].items`);
4555
4834
  walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
4556
4835
  }
@@ -4560,7 +4839,7 @@ function validateNavTargetRefs(stack) {
4560
4839
 
4561
4840
  // src/validate-action-name-refs.ts
4562
4841
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
4563
- function asArray26(v) {
4842
+ function asArray28(v) {
4564
4843
  if (Array.isArray(v)) return v;
4565
4844
  if (v && typeof v === "object") {
4566
4845
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4604,13 +4883,13 @@ function suggest4(target, known) {
4604
4883
  }
4605
4884
  function collectActionNames(stack) {
4606
4885
  const names = /* @__PURE__ */ new Set();
4607
- for (const action of asArray26(stack.actions)) {
4886
+ for (const action of asArray28(stack.actions)) {
4608
4887
  const n = strName8(action?.name);
4609
4888
  if (n) names.add(n);
4610
4889
  }
4611
- for (const obj of asArray26(stack.objects)) {
4890
+ for (const obj of asArray28(stack.objects)) {
4612
4891
  if (!obj || typeof obj !== "object") continue;
4613
- for (const action of asArray26(obj.actions)) {
4892
+ for (const action of asArray28(obj.actions)) {
4614
4893
  const n = strName8(action?.name);
4615
4894
  if (n) names.add(n);
4616
4895
  }
@@ -4665,7 +4944,7 @@ function validateActionNameRefs(stack) {
4665
4944
  );
4666
4945
  }
4667
4946
  };
4668
- const views = asArray26(stack.views);
4947
+ const views = asArray28(stack.views);
4669
4948
  for (let vi = 0; vi < views.length; vi++) {
4670
4949
  const view = views[vi];
4671
4950
  if (!view || typeof view !== "object") continue;
@@ -4679,7 +4958,7 @@ function validateActionNameRefs(stack) {
4679
4958
  }
4680
4959
  }
4681
4960
  }
4682
- const objects = asArray26(stack.objects);
4961
+ const objects = asArray28(stack.objects);
4683
4962
  for (let oi = 0; oi < objects.length; oi++) {
4684
4963
  const obj = objects[oi];
4685
4964
  if (!obj || typeof obj !== "object") continue;
@@ -4690,7 +4969,7 @@ function validateActionNameRefs(stack) {
4690
4969
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
4691
4970
  }
4692
4971
  }
4693
- const pages = asArray26(stack.pages);
4972
+ const pages = asArray28(stack.pages);
4694
4973
  for (let pi = 0; pi < pages.length; pi++) {
4695
4974
  const page = pages[pi];
4696
4975
  if (!page || typeof page !== "object") continue;
@@ -4709,13 +4988,13 @@ function validateActionNameRefs(stack) {
4709
4988
  }
4710
4989
  }
4711
4990
  }
4712
- const apps = asArray26(stack.apps);
4991
+ const apps = asArray28(stack.apps);
4713
4992
  for (let ai = 0; ai < apps.length; ai++) {
4714
4993
  const app = apps[ai];
4715
4994
  if (!app || typeof app !== "object") continue;
4716
4995
  const appName = strName8(app.name) ?? `#${ai}`;
4717
4996
  const walkNav = (items, basePath) => {
4718
- const navItems = asArray26(items);
4997
+ const navItems = asArray28(items);
4719
4998
  for (let ni = 0; ni < navItems.length; ni++) {
4720
4999
  const nav = navItems[ni];
4721
5000
  if (!nav || typeof nav !== "object") continue;
@@ -4734,7 +5013,7 @@ function validateActionNameRefs(stack) {
4734
5013
  }
4735
5014
  };
4736
5015
  walkNav(app.navigation, `apps[${ai}].navigation`);
4737
- const areas = asArray26(app.areas);
5016
+ const areas = asArray28(app.areas);
4738
5017
  for (let ri = 0; ri < areas.length; ri++) {
4739
5018
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4740
5019
  }
@@ -4744,7 +5023,7 @@ function validateActionNameRefs(stack) {
4744
5023
 
4745
5024
  // src/validate-action-locations.ts
4746
5025
  var ACTION_NO_PLACEMENT = "action-no-placement";
4747
- function asArray27(v) {
5026
+ function asArray29(v) {
4748
5027
  if (Array.isArray(v)) return v;
4749
5028
  if (v && typeof v === "object") {
4750
5029
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4765,7 +5044,7 @@ function collectNamePlacedActions(stack) {
4765
5044
  for (const key of ["rowActions", "bulkActions"]) {
4766
5045
  for (const n of strList2(list3[key])) placed.add(n);
4767
5046
  }
4768
- for (const def of asArray27(list3.bulkActionDefs)) {
5047
+ for (const def of asArray29(list3.bulkActionDefs)) {
4769
5048
  const n = strName9(def?.name);
4770
5049
  if (n) placed.add(n);
4771
5050
  }
@@ -4774,12 +5053,12 @@ function collectNamePlacedActions(stack) {
4774
5053
  if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
4775
5054
  for (const lv of Object.values(listViews)) harvest(lv);
4776
5055
  };
4777
- for (const view of asArray27(stack.views)) {
5056
+ for (const view of asArray29(stack.views)) {
4778
5057
  if (!view || typeof view !== "object") continue;
4779
5058
  harvest(view.list);
4780
5059
  harvestListViews(view.listViews);
4781
5060
  }
4782
- for (const obj of asArray27(stack.objects)) {
5061
+ for (const obj of asArray29(stack.objects)) {
4783
5062
  if (!obj || typeof obj !== "object") continue;
4784
5063
  harvestListViews(obj.listViews);
4785
5064
  }
@@ -4804,37 +5083,121 @@ function validateActionLocations(stack) {
4804
5083
  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."
4805
5084
  });
4806
5085
  };
4807
- const actions = asArray27(stack.actions);
5086
+ const actions = asArray29(stack.actions);
4808
5087
  for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
4809
- const objects = asArray27(stack.objects);
5088
+ const objects = asArray29(stack.objects);
4810
5089
  for (let oi = 0; oi < objects.length; oi++) {
4811
5090
  const obj = objects[oi];
4812
5091
  if (!obj || typeof obj !== "object") continue;
4813
- const own = asArray27(obj.actions);
5092
+ const own = asArray29(obj.actions);
4814
5093
  for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
4815
5094
  }
4816
5095
  return findings;
4817
5096
  }
4818
5097
 
5098
+ // src/validate-component-props.ts
5099
+ import { ComponentPropsMap } from "@objectstack/spec/ui";
5100
+ import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
5101
+ var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5102
+ var COMPONENT_PROPS_INVALID = "component-props-invalid";
5103
+ function isRec9(v) {
5104
+ return !!v && typeof v === "object" && !Array.isArray(v);
5105
+ }
5106
+ function strName10(v) {
5107
+ return typeof v === "string" && v.length > 0 ? v : void 0;
5108
+ }
5109
+ function asArray30(v) {
5110
+ if (Array.isArray(v)) return v;
5111
+ if (v && typeof v === "object") {
5112
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5113
+ }
5114
+ return [];
5115
+ }
5116
+ var PROPS_SCHEMAS = ComponentPropsMap;
5117
+ var DATASOURCE_SUPPLIED_PROP = "object";
5118
+ function suppliedByDataSource(issue, component) {
5119
+ if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
5120
+ const dataSource = isRec9(component.dataSource) ? component.dataSource : void 0;
5121
+ return strName10(dataSource?.object) !== void 0;
5122
+ }
5123
+ function validateComponentProps(stack) {
5124
+ const findings = [];
5125
+ if (!isRec9(stack)) return findings;
5126
+ const pages = asArray30(stack.pages);
5127
+ for (let pi = 0; pi < pages.length; pi++) {
5128
+ const page = pages[pi];
5129
+ if (!isRec9(page)) continue;
5130
+ const pageName = strName10(page.name) ?? `#${pi}`;
5131
+ for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5132
+ const type = strName10(component.type);
5133
+ if (!type) continue;
5134
+ const schema = PROPS_SCHEMAS[type];
5135
+ if (!schema) continue;
5136
+ const props = isRec9(component.properties) ? component.properties : void 0;
5137
+ if (!props) continue;
5138
+ const where = `page "${pageName}" \xB7 ${type}`;
5139
+ const base = `${path}.properties`;
5140
+ for (const f of lintUnknownKeysAgainstSchema(schema, props, type, base)) {
5141
+ findings.push({
5142
+ severity: "warning",
5143
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
5144
+ where,
5145
+ path: f.path,
5146
+ 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}\`?` : ""),
5147
+ 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.`)
5148
+ });
5149
+ }
5150
+ const parsed = schema.safeParse(props);
5151
+ if (parsed.success) continue;
5152
+ for (const issue of parsed.error?.issues ?? []) {
5153
+ if (suppliedByDataSource(issue, component)) continue;
5154
+ const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
5155
+ if (issue.code === "unrecognized_keys") {
5156
+ for (const key of issue.keys ?? []) {
5157
+ findings.push({
5158
+ severity: "warning",
5159
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
5160
+ where,
5161
+ path: `${at}.${key}`,
5162
+ message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${issue.message}`,
5163
+ hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
5164
+ });
5165
+ }
5166
+ continue;
5167
+ }
5168
+ findings.push({
5169
+ severity: "warning",
5170
+ rule: COMPONENT_PROPS_INVALID,
5171
+ where,
5172
+ path: at,
5173
+ message: `${at.slice(base.length + 1) || "properties"}: ${describeIssue(issue, props)}`,
5174
+ 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).`
5175
+ });
5176
+ }
5177
+ }
5178
+ }
5179
+ return findings;
5180
+ }
5181
+
4819
5182
  // src/validate-chart-bindings.ts
4820
5183
  var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
4821
5184
  var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
4822
5185
  var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
4823
5186
  var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
4824
- function asArray28(v) {
5187
+ function asArray31(v) {
4825
5188
  if (Array.isArray(v)) return v;
4826
5189
  if (v && typeof v === "object") {
4827
5190
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4828
5191
  }
4829
5192
  return [];
4830
5193
  }
4831
- function strName10(v) {
5194
+ function strName11(v) {
4832
5195
  return typeof v === "string" && v.length > 0 ? v : void 0;
4833
5196
  }
4834
5197
  function strList3(v) {
4835
5198
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
4836
5199
  }
4837
- function isRec8(v) {
5200
+ function isRec10(v) {
4838
5201
  return !!v && typeof v === "object" && !Array.isArray(v);
4839
5202
  }
4840
5203
  function distance4(a, b) {
@@ -4872,17 +5235,17 @@ function list2(names) {
4872
5235
  }
4873
5236
  function indexDatasets(stack) {
4874
5237
  const out = /* @__PURE__ */ new Map();
4875
- for (const ds of asArray28(stack.datasets)) {
4876
- const name = strName10(ds.name);
5238
+ for (const ds of asArray31(stack.datasets)) {
5239
+ const name = strName11(ds.name);
4877
5240
  if (!name) continue;
4878
5241
  const dimensions = /* @__PURE__ */ new Set();
4879
- for (const d of asArray28(ds.dimensions)) {
4880
- const n = strName10(d.name);
5242
+ for (const d of asArray31(ds.dimensions)) {
5243
+ const n = strName11(d.name);
4881
5244
  if (n) dimensions.add(n);
4882
5245
  }
4883
5246
  const measures = /* @__PURE__ */ new Set();
4884
- for (const m of asArray28(ds.measures)) {
4885
- const n = strName10(m.name);
5247
+ for (const m of asArray31(ds.measures)) {
5248
+ const n = strName11(m.name);
4886
5249
  if (n) measures.add(n);
4887
5250
  }
4888
5251
  out.set(name, { dimensions, measures });
@@ -4960,29 +5323,29 @@ function validateChartBindings(stack) {
4960
5323
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
4961
5324
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
4962
5325
  };
4963
- const reports = asArray28(stack.reports);
5326
+ const reports = asArray31(stack.reports);
4964
5327
  for (let ri = 0; ri < reports.length; ri++) {
4965
5328
  const report = reports[ri];
4966
- if (!isRec8(report)) continue;
4967
- const reportName = strName10(report.name) ?? `#${ri}`;
5329
+ if (!isRec10(report)) continue;
5330
+ const reportName = strName11(report.name) ?? `#${ri}`;
4968
5331
  const checkReportChart = (chart, dataset, values, where, path) => {
4969
- if (!isRec8(chart)) return;
5332
+ if (!isRec10(chart)) return;
4970
5333
  check({
4971
5334
  dataset,
4972
5335
  // `values` is the report's measure SELECTION, not a chart ref; feeding
4973
5336
  // it in lets the yAxis "declared but not selected" check work without
4974
5337
  // reporting the selection itself twice.
4975
5338
  values: { names: values, path: `${path}.values` },
4976
- xAxis: strName10(chart.xAxis) ? { name: strName10(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
4977
- yAxis: strName10(chart.yAxis) ? { name: strName10(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
4978
- series: asArray28(chart.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
5339
+ xAxis: strName11(chart.xAxis) ? { name: strName11(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
5340
+ yAxis: strName11(chart.yAxis) ? { name: strName11(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
5341
+ series: asArray31(chart.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
4979
5342
  where,
4980
5343
  path: `${path}.chart`
4981
5344
  });
4982
5345
  };
4983
5346
  checkReportChart(
4984
5347
  report.chart,
4985
- strName10(report.dataset),
5348
+ strName11(report.dataset),
4986
5349
  strList3(report.values),
4987
5350
  `report "${reportName}" \xB7 chart`,
4988
5351
  `reports[${ri}]`
@@ -4990,45 +5353,45 @@ function validateChartBindings(stack) {
4990
5353
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
4991
5354
  for (let bi = 0; bi < blocks.length; bi++) {
4992
5355
  const block = blocks[bi];
4993
- if (!isRec8(block)) continue;
5356
+ if (!isRec10(block)) continue;
4994
5357
  checkReportChart(
4995
5358
  block.chart,
4996
- strName10(block.dataset),
5359
+ strName11(block.dataset),
4997
5360
  strList3(block.values),
4998
- `report "${reportName}" \xB7 block "${strName10(block.name) ?? `#${bi}`}" chart`,
5361
+ `report "${reportName}" \xB7 block "${strName11(block.name) ?? `#${bi}`}" chart`,
4999
5362
  `reports[${ri}].blocks[${bi}]`
5000
5363
  );
5001
5364
  }
5002
5365
  }
5003
5366
  const checkListChart = (container, where, path) => {
5004
- if (!isRec8(container)) return;
5367
+ if (!isRec10(container)) return;
5005
5368
  const chart = container.chart;
5006
- if (!isRec8(chart)) return;
5369
+ if (!isRec10(chart)) return;
5007
5370
  check({
5008
- dataset: strName10(chart.dataset),
5371
+ dataset: strName11(chart.dataset),
5009
5372
  dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
5010
5373
  values: { names: strList3(chart.values), path: `${path}.chart.values` },
5011
5374
  where,
5012
5375
  path: `${path}.chart`
5013
5376
  });
5014
5377
  };
5015
- const views = asArray28(stack.views);
5378
+ const views = asArray31(stack.views);
5016
5379
  for (let vi = 0; vi < views.length; vi++) {
5017
5380
  const view = views[vi];
5018
- if (!isRec8(view)) continue;
5019
- const viewName = strName10(view.name) ?? strName10(view.objectName) ?? `#${vi}`;
5381
+ if (!isRec10(view)) continue;
5382
+ const viewName = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
5020
5383
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
5021
- if (isRec8(view.listViews)) {
5384
+ if (isRec10(view.listViews)) {
5022
5385
  for (const [key, lv] of Object.entries(view.listViews)) {
5023
5386
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
5024
5387
  }
5025
5388
  }
5026
5389
  }
5027
- const objects = asArray28(stack.objects);
5390
+ const objects = asArray31(stack.objects);
5028
5391
  for (let oi = 0; oi < objects.length; oi++) {
5029
5392
  const obj = objects[oi];
5030
- if (!isRec8(obj) || !isRec8(obj.listViews)) continue;
5031
- const objName = strName10(obj.name) ?? `#${oi}`;
5393
+ if (!isRec10(obj) || !isRec10(obj.listViews)) continue;
5394
+ const objName = strName11(obj.name) ?? `#${oi}`;
5032
5395
  for (const [key, lv] of Object.entries(obj.listViews)) {
5033
5396
  checkListChart(
5034
5397
  lv,
@@ -5037,22 +5400,22 @@ function validateChartBindings(stack) {
5037
5400
  );
5038
5401
  }
5039
5402
  }
5040
- const pages = asArray28(stack.pages);
5403
+ const pages = asArray31(stack.pages);
5041
5404
  for (let pi = 0; pi < pages.length; pi++) {
5042
5405
  const page = pages[pi];
5043
- if (!isRec8(page)) continue;
5044
- const pageName = strName10(page.name) ?? `#${pi}`;
5406
+ if (!isRec10(page)) continue;
5407
+ const pageName = strName11(page.name) ?? `#${pi}`;
5045
5408
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5046
- const props = isRec8(component.properties) ? component.properties : void 0;
5047
- if (!props || !strName10(props.dataset)) continue;
5048
- const axisRefs = asArray28(props.yAxis).map((a, ai) => ({ name: strName10(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
5049
- const seriesRefs = asArray28(props.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
5409
+ const props = isRec10(component.properties) ? component.properties : void 0;
5410
+ if (!props || !strName11(props.dataset)) continue;
5411
+ const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName11(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
5412
+ const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
5050
5413
  check({
5051
- dataset: strName10(props.dataset),
5414
+ dataset: strName11(props.dataset),
5052
5415
  dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
5053
5416
  values: { names: strList3(props.values), path: `${path}.properties.values` },
5054
5417
  series: [...axisRefs, ...seriesRefs],
5055
- where: `page "${pageName}" \xB7 ${strName10(component.type) ?? "chart"}`,
5418
+ where: `page "${pageName}" \xB7 ${strName11(component.type) ?? "chart"}`,
5056
5419
  path: `${path}.properties`
5057
5420
  });
5058
5421
  }
@@ -5060,28 +5423,280 @@ function validateChartBindings(stack) {
5060
5423
  return findings;
5061
5424
  }
5062
5425
 
5063
- // src/validate-nav-access.ts
5064
- import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
5065
-
5066
- // src/build-access-matrix.ts
5067
- function asArray29(v) {
5068
- if (Array.isArray(v)) return v;
5069
- if (v && typeof v === "object") {
5070
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5426
+ // src/validate-rule-compilability.ts
5427
+ import { createRequire as createRequire3 } from "module";
5428
+ var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
5429
+ var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
5430
+ var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
5431
+ var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5432
+ function asArray32(v) {
5433
+ if (Array.isArray(v)) return v.filter(isRec11);
5434
+ if (isRec11(v)) {
5435
+ return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ name, ...def }));
5071
5436
  }
5072
5437
  return [];
5073
5438
  }
5074
- function buildAccessMatrix(stack) {
5075
- const entries = [];
5076
- if (!stack || typeof stack !== "object") return { version: 1, entries };
5439
+ var cachedAjv = null;
5440
+ var cachedAddFormats = null;
5441
+ function loadAjv() {
5442
+ if (cachedAjv) return cachedAjv;
5443
+ const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5444
+ let mod;
5445
+ try {
5446
+ mod = createRequire3(anchor)("ajv");
5447
+ } catch (err) {
5448
+ throw new Error(
5449
+ `@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.`
5450
+ );
5451
+ }
5452
+ const ctor = isRec11(mod) && "default" in mod ? mod.default : mod;
5453
+ cachedAjv = ctor;
5454
+ return ctor;
5455
+ }
5456
+ function loadAddFormats() {
5457
+ if (cachedAddFormats) return cachedAddFormats;
5458
+ const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5459
+ let mod;
5460
+ try {
5461
+ mod = createRequire3(anchor)("ajv-formats");
5462
+ } catch (err) {
5463
+ throw new Error(
5464
+ `@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.`
5465
+ );
5466
+ }
5467
+ const plugin = isRec11(mod) && "default" in mod ? mod.default : mod;
5468
+ cachedAddFormats = plugin;
5469
+ return plugin;
5470
+ }
5471
+ function createRuntimeAjv() {
5472
+ const instance = new (loadAjv())(RUNTIME_AJV_OPTIONS);
5473
+ loadAddFormats()(instance);
5474
+ return instance;
5475
+ }
5476
+ function registeredFormatNames() {
5477
+ const names = Object.keys(createRuntimeAjv().formats).sort();
5478
+ if (names.length === 0) {
5479
+ throw new Error(
5480
+ `@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.`
5481
+ );
5482
+ }
5483
+ return names;
5484
+ }
5485
+ function errorText(err) {
5486
+ return err instanceof Error ? err.message : String(err);
5487
+ }
5488
+ var MAX_RULE_NESTING_DEPTH = 16;
5489
+ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
5490
+ const name = typeof rule.name === "string" && rule.name ? rule.name : "?";
5491
+ const label2 = labelTrail ? `${labelTrail} \u2192 '${name}'` : `'${name}'`;
5492
+ const path = pathTrail ? `${pathTrail}.${name}` : name;
5493
+ const out = [{ rule, label: label2, path }];
5494
+ if (depth >= MAX_RULE_NESTING_DEPTH) return out;
5495
+ for (const branch of ["then", "otherwise"]) {
5496
+ const nested = rule[branch];
5497
+ if (isRec11(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
5498
+ }
5499
+ return out;
5500
+ }
5501
+ function walkObjectValidationRules(stack) {
5502
+ const walked = [];
5503
+ if (!isRec11(stack)) return walked;
5504
+ for (const obj of asArray32(stack.objects)) {
5505
+ const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
5506
+ const validations = obj.validations;
5507
+ for (const authored of asArray32(validations)) {
5508
+ for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
5509
+ walked.push({
5510
+ rule,
5511
+ objectName,
5512
+ label: label2,
5513
+ where: `object '${objectName}' \xB7 validation ${label2}`,
5514
+ basePath: `objects.${objectName}.validations.${path}`
5515
+ });
5516
+ }
5517
+ }
5518
+ }
5519
+ return walked;
5520
+ }
5521
+ function validateRuleCompilability(stack) {
5522
+ const findings = [];
5523
+ for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
5524
+ if (rule.type === "format" && typeof rule.regex === "string" && rule.regex !== "") {
5525
+ try {
5526
+ new RegExp(rule.regex);
5527
+ } catch (err) {
5528
+ findings.push({
5529
+ severity: "error",
5530
+ rule: VALIDATION_RULE_REGEX_UNCOMPILABLE,
5531
+ where,
5532
+ path: `${basePath}.regex`,
5533
+ 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.`,
5534
+ 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').`
5535
+ });
5536
+ }
5537
+ }
5538
+ if (rule.type === "json_schema" && isRec11(rule.schema)) {
5539
+ try {
5540
+ createRuntimeAjv().compile(rule.schema);
5541
+ } catch (err) {
5542
+ findings.push({
5543
+ severity: "error",
5544
+ rule: VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
5545
+ where,
5546
+ path: `${basePath}.schema`,
5547
+ 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.`,
5548
+ 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.`
5549
+ });
5550
+ }
5551
+ }
5552
+ }
5553
+ return findings;
5554
+ }
5555
+
5556
+ // src/validate-rule-schema-formats.ts
5557
+ var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
5558
+ var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5559
+ var SUBSCHEMA_KEYS = [
5560
+ "additionalItems",
5561
+ "additionalProperties",
5562
+ "contains",
5563
+ "propertyNames",
5564
+ "if",
5565
+ "then",
5566
+ "else",
5567
+ "not",
5568
+ "unevaluatedItems",
5569
+ "unevaluatedProperties"
5570
+ ];
5571
+ var SUBSCHEMA_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
5572
+ var SUBSCHEMA_MAP_KEYS = [
5573
+ "properties",
5574
+ "patternProperties",
5575
+ "$defs",
5576
+ "definitions",
5577
+ "dependentSchemas"
5578
+ ];
5579
+ var MAX_SCHEMA_WALK_DEPTH = 32;
5580
+ var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
5581
+ function collectFormatUses(schema, pointer, out, depth) {
5582
+ if (!isRec12(schema)) return;
5583
+ if (typeof schema.format === "string") {
5584
+ out.push({ pointer: `${pointer}/format`, name: schema.format });
5585
+ }
5586
+ if (depth >= MAX_SCHEMA_WALK_DEPTH) return;
5587
+ for (const key of SUBSCHEMA_KEYS) {
5588
+ if (key in schema) {
5589
+ collectFormatUses(schema[key], `${pointer}/${escapePointerSegment(key)}`, out, depth + 1);
5590
+ }
5591
+ }
5592
+ for (const key of SUBSCHEMA_LIST_KEYS) {
5593
+ const value = schema[key];
5594
+ if (!Array.isArray(value)) continue;
5595
+ value.forEach((entry, index) => {
5596
+ collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${index}`, out, depth + 1);
5597
+ });
5598
+ }
5599
+ for (const key of SUBSCHEMA_MAP_KEYS) {
5600
+ const value = schema[key];
5601
+ if (!isRec12(value)) continue;
5602
+ for (const [name, entry] of Object.entries(value)) {
5603
+ collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
5604
+ }
5605
+ }
5606
+ const items = schema.items;
5607
+ if (Array.isArray(items)) {
5608
+ items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
5609
+ } else if (isRec12(items)) {
5610
+ collectFormatUses(items, `${pointer}/items`, out, depth + 1);
5611
+ }
5612
+ const dependencies = schema.dependencies;
5613
+ if (isRec12(dependencies)) {
5614
+ for (const [name, entry] of Object.entries(dependencies)) {
5615
+ if (!isRec12(entry)) continue;
5616
+ collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
5617
+ }
5618
+ }
5619
+ }
5620
+ function editDistance2(a, b) {
5621
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
5622
+ for (let i = 1; i <= a.length; i++) {
5623
+ const current = [i];
5624
+ for (let j = 1; j <= b.length; j++) {
5625
+ current[j] = Math.min(
5626
+ previous[j] + 1,
5627
+ current[j - 1] + 1,
5628
+ previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
5629
+ );
5630
+ }
5631
+ previous = current;
5632
+ }
5633
+ return previous[b.length];
5634
+ }
5635
+ function nearestRegisteredFormat(name, registered) {
5636
+ const budget = Math.min(3, Math.floor(name.length / 2));
5637
+ if (budget < 1) return null;
5638
+ const authored = name.toLowerCase();
5639
+ let best = null;
5640
+ let bestDistance = Number.POSITIVE_INFINITY;
5641
+ for (const candidate of [...registered].sort()) {
5642
+ const distance7 = editDistance2(authored, candidate);
5643
+ if (distance7 < bestDistance) {
5644
+ bestDistance = distance7;
5645
+ best = candidate;
5646
+ }
5647
+ }
5648
+ return bestDistance <= budget ? best : null;
5649
+ }
5650
+ function validateRuleSchemaFormats(stack) {
5651
+ const findings = [];
5652
+ const pending = [];
5653
+ for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
5654
+ if (rule.type !== "json_schema" || !isRec12(rule.schema)) continue;
5655
+ const uses = [];
5656
+ collectFormatUses(rule.schema, "", uses, 0);
5657
+ for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
5658
+ }
5659
+ if (pending.length === 0) return findings;
5660
+ const registered = registeredFormatNames();
5661
+ const known = new Set(registered);
5662
+ for (const { use, where, label: label2, objectName, basePath } of pending) {
5663
+ if (known.has(use.name)) continue;
5664
+ const suggestion = nearestRegisteredFormat(use.name, registered);
5665
+ const pointer = `#${use.pointer}`;
5666
+ findings.push({
5667
+ severity: "error",
5668
+ rule: VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
5669
+ where,
5670
+ path: `${basePath}.schema${pointer}`,
5671
+ 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.`,
5672
+ 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.`
5673
+ });
5674
+ }
5675
+ return findings;
5676
+ }
5677
+
5678
+ // src/validate-nav-access.ts
5679
+ import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
5680
+
5681
+ // src/build-access-matrix.ts
5682
+ function asArray33(v) {
5683
+ if (Array.isArray(v)) return v;
5684
+ if (v && typeof v === "object") {
5685
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5686
+ }
5687
+ return [];
5688
+ }
5689
+ function buildAccessMatrix(stack) {
5690
+ const entries = [];
5691
+ if (!stack || typeof stack !== "object") return { version: 1, entries };
5077
5692
  const owdByObject = /* @__PURE__ */ new Map();
5078
- for (const obj of asArray29(stack.objects)) {
5693
+ for (const obj of asArray33(stack.objects)) {
5079
5694
  const name = typeof obj.name === "string" ? obj.name : "";
5080
5695
  if (!name) continue;
5081
5696
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
5082
5697
  if (typeof owd === "string") owdByObject.set(name, owd);
5083
5698
  }
5084
- for (const ps of asArray29(stack.permissions)) {
5699
+ for (const ps of asArray33(stack.permissions)) {
5085
5700
  const psName = typeof ps.name === "string" ? ps.name : "";
5086
5701
  if (!psName) continue;
5087
5702
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -5154,34 +5769,34 @@ function diffAccessMatrix(before, after) {
5154
5769
 
5155
5770
  // src/validate-nav-access.ts
5156
5771
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
5157
- function asArray30(v) {
5772
+ function asArray34(v) {
5158
5773
  if (Array.isArray(v)) return v;
5159
5774
  if (v && typeof v === "object") {
5160
5775
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5161
5776
  }
5162
5777
  return [];
5163
5778
  }
5164
- function strName11(v) {
5779
+ function strName12(v) {
5165
5780
  return typeof v === "string" && v.length > 0 ? v : void 0;
5166
5781
  }
5167
5782
  function collectNavExposures(stack) {
5168
5783
  const out = [];
5169
- const apps = asArray30(stack.apps);
5784
+ const apps = asArray34(stack.apps);
5170
5785
  for (let ai = 0; ai < apps.length; ai++) {
5171
5786
  const app = apps[ai];
5172
5787
  if (!app || typeof app !== "object") continue;
5173
- const appName = strName11(app.name) ?? `#${ai}`;
5788
+ const appName = strName12(app.name) ?? `#${ai}`;
5174
5789
  const walk = (items, basePath) => {
5175
- const navItems = asArray30(items);
5790
+ const navItems = asArray34(items);
5176
5791
  for (let ni = 0; ni < navItems.length; ni++) {
5177
5792
  const nav = navItems[ni];
5178
5793
  if (!nav || typeof nav !== "object") continue;
5179
5794
  const navPath = `${basePath}[${ni}]`;
5180
- const objectName = strName11(nav.objectName);
5795
+ const objectName = strName12(nav.objectName);
5181
5796
  if (nav.type === "object" && objectName) {
5182
5797
  out.push({
5183
5798
  objectName,
5184
- where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
5799
+ where: `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
5185
5800
  path: `${navPath}.objectName`
5186
5801
  });
5187
5802
  }
@@ -5189,7 +5804,7 @@ function collectNavExposures(stack) {
5189
5804
  }
5190
5805
  };
5191
5806
  walk(app.navigation, `apps[${ai}].navigation`);
5192
- const areas = asArray30(app.areas);
5807
+ const areas = asArray34(app.areas);
5193
5808
  for (let ri = 0; ri < areas.length; ri++) {
5194
5809
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
5195
5810
  }
@@ -5199,13 +5814,13 @@ function collectNavExposures(stack) {
5199
5814
  function validateNavAccess(stack) {
5200
5815
  const findings = [];
5201
5816
  if (!stack || typeof stack !== "object") return findings;
5202
- const permissionSets = asArray30(stack.permissions);
5817
+ const permissionSets = asArray34(stack.permissions);
5203
5818
  if (permissionSets.length === 0) return findings;
5204
5819
  const exposures = collectNavExposures(stack);
5205
5820
  if (exposures.length === 0) return findings;
5206
5821
  const ownObjects = /* @__PURE__ */ new Set();
5207
- for (const obj of asArray30(stack.objects)) {
5208
- const n = strName11(obj.name);
5822
+ for (const obj of asArray34(stack.objects)) {
5823
+ const n = strName12(obj.name);
5209
5824
  if (n) ownObjects.add(n);
5210
5825
  }
5211
5826
  const readable = /* @__PURE__ */ new Set();
@@ -5237,15 +5852,15 @@ function validateNavAccess(stack) {
5237
5852
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
5238
5853
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
5239
5854
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
5240
- function isRec9(v) {
5855
+ function isRec13(v) {
5241
5856
  return !!v && typeof v === "object" && !Array.isArray(v);
5242
5857
  }
5243
- function asArray31(v) {
5858
+ function asArray35(v) {
5244
5859
  if (Array.isArray(v)) return v;
5245
- if (isRec9(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec9(def) ? def : {} }));
5860
+ if (isRec13(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec13(def) ? def : {} }));
5246
5861
  return [];
5247
5862
  }
5248
- function strName12(v) {
5863
+ function strName13(v) {
5249
5864
  return typeof v === "string" && v.length > 0 ? v : void 0;
5250
5865
  }
5251
5866
  function distance5(a, b) {
@@ -5303,35 +5918,32 @@ function collectViewRecord(view, factsFor) {
5303
5918
  const addView = (objectName, name) => {
5304
5919
  if (objectName && name) factsFor(objectName).views.add(name);
5305
5920
  };
5306
- const listBinding = isRec9(view.list) ? bindingOf(view.list) : void 0;
5307
- if (isRec9(view.list)) addView(listBinding, strName12(view.list.name));
5308
- addView(recordObject ?? listBinding, strName12(view.name));
5921
+ const addSections = (container, binding) => {
5922
+ if (!binding) return;
5923
+ for (const section of asArray35(container.sections)) {
5924
+ const sectionName = strName13(section.name);
5925
+ if (sectionName) factsFor(binding).sections.add(sectionName);
5926
+ }
5927
+ };
5928
+ const listBinding = isRec13(view.list) ? bindingOf(view.list) : void 0;
5929
+ if (isRec13(view.list)) addView(listBinding, strName13(view.list.name));
5930
+ addView(recordObject ?? listBinding, strName13(view.name));
5309
5931
  for (const key of ["listViews", "formViews"]) {
5310
5932
  const container = view[key];
5311
- if (!isRec9(container)) continue;
5933
+ if (!isRec13(container)) continue;
5312
5934
  for (const [subKey, sub] of Object.entries(container)) {
5313
- if (!isRec9(sub)) continue;
5935
+ if (!isRec13(sub)) continue;
5314
5936
  const binding = bindingOf(sub) ?? listBinding;
5315
5937
  addView(binding, subKey);
5316
- addView(binding, strName12(sub.name));
5317
- if (binding) {
5318
- for (const section of asArray31(sub.sections)) {
5319
- const sectionName = strName12(section.name);
5320
- if (sectionName) factsFor(binding).sections.add(sectionName);
5321
- }
5322
- }
5323
- }
5324
- }
5325
- const sectionBinding = recordObject ?? listBinding;
5326
- if (sectionBinding) {
5327
- for (const section of asArray31(view.sections)) {
5328
- const sectionName = strName12(section.name);
5329
- if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
5938
+ addView(binding, strName13(sub.name));
5939
+ addSections(sub, binding);
5330
5940
  }
5331
5941
  }
5942
+ if (isRec13(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
5943
+ addSections(view, recordObject ?? listBinding);
5332
5944
  }
5333
5945
  function viewObjectName(view) {
5334
- return strName12(view.objectName) ?? strName12(view.object) ?? (isRec9(view.data) ? strName12(view.data.object) : void 0);
5946
+ return strName13(view.objectName) ?? strName13(view.object) ?? (isRec13(view.data) ? strName13(view.data.object) : void 0);
5335
5947
  }
5336
5948
  function readOptions(field) {
5337
5949
  const raw = field.options;
@@ -5343,14 +5955,14 @@ function readOptions(field) {
5343
5955
  values.add(opt);
5344
5956
  continue;
5345
5957
  }
5346
- if (!isRec9(opt)) continue;
5347
- const value = strName12(opt.value);
5958
+ if (!isRec13(opt)) continue;
5959
+ const value = strName13(opt.value);
5348
5960
  if (!value) continue;
5349
5961
  values.add(value);
5350
- const label2 = strName12(opt.label);
5962
+ const label2 = strName13(opt.label);
5351
5963
  if (label2) byLabel.set(label2.toLowerCase(), value);
5352
5964
  }
5353
- } else if (isRec9(raw)) {
5965
+ } else if (isRec13(raw)) {
5354
5966
  for (const [value, label2] of Object.entries(raw)) {
5355
5967
  values.add(value);
5356
5968
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -5370,48 +5982,48 @@ function buildUniverse(stack) {
5370
5982
  }
5371
5983
  return facts;
5372
5984
  };
5373
- for (const obj of asArray31(stack.objects)) {
5374
- const objectName = strName12(obj.name);
5985
+ for (const obj of asArray35(stack.objects)) {
5986
+ const objectName = strName13(obj.name);
5375
5987
  if (!objectName) continue;
5376
5988
  const facts = factsFor(objectName);
5377
- for (const field of asArray31(obj.fields)) {
5378
- const fieldName = strName12(field.name);
5989
+ for (const field of asArray35(obj.fields)) {
5990
+ const fieldName = strName13(field.name);
5379
5991
  if (fieldName) facts.fields.set(fieldName, field);
5380
5992
  }
5381
- for (const action of asArray31(obj.actions)) {
5382
- const actionName = strName12(action.name);
5993
+ for (const action of asArray35(obj.actions)) {
5994
+ const actionName = strName13(action.name);
5383
5995
  if (actionName) facts.actions.set(actionName, action);
5384
5996
  }
5385
- for (const view of asArray31(obj.views)) {
5386
- collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
5997
+ for (const view of asArray35(obj.views)) {
5998
+ collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
5387
5999
  }
5388
6000
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
5389
- for (const group of asArray31(obj.fieldGroups)) {
5390
- const key = strName12(group.key) ?? strName12(group.name);
6001
+ for (const group of asArray35(obj.fieldGroups)) {
6002
+ const key = strName13(group.key) ?? strName13(group.name);
5391
6003
  if (key) facts.sections.add(key);
5392
6004
  }
5393
6005
  }
5394
- for (const view of asArray31(stack.views)) {
6006
+ for (const view of asArray35(stack.views)) {
5395
6007
  collectViewRecord(view, factsFor);
5396
6008
  }
5397
- const pages = asArray31(stack.pages);
6009
+ const pages = asArray35(stack.pages);
5398
6010
  for (let pi = 0; pi < pages.length; pi++) {
5399
6011
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
5400
6012
  if (!walked.objectName) continue;
5401
- const props = isRec9(walked.component.properties) ? walked.component.properties : void 0;
6013
+ const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
5402
6014
  if (!props) continue;
5403
- for (const section of asArray31(props.sections)) {
5404
- const sectionName = strName12(section.name);
6015
+ for (const section of asArray35(props.sections)) {
6016
+ const sectionName = strName13(section.name);
5405
6017
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
5406
6018
  }
5407
6019
  }
5408
6020
  }
5409
6021
  const globalActions = /* @__PURE__ */ new Map();
5410
6022
  const actionOwners = /* @__PURE__ */ new Map();
5411
- for (const action of asArray31(stack.actions)) {
5412
- const actionName = strName12(action.name);
6023
+ for (const action of asArray35(stack.actions)) {
6024
+ const actionName = strName13(action.name);
5413
6025
  if (!actionName) continue;
5414
- const owner = strName12(action.objectName) ?? strName12(action.object);
6026
+ const owner = strName13(action.objectName) ?? strName13(action.object);
5415
6027
  if (owner) {
5416
6028
  factsFor(owner).actions.set(actionName, action);
5417
6029
  actionOwners.set(actionName, owner);
@@ -5425,41 +6037,41 @@ function buildUniverse(stack) {
5425
6037
  }
5426
6038
  }
5427
6039
  const apps = /* @__PURE__ */ new Map();
5428
- for (const app of asArray31(stack.apps)) {
5429
- const appName = strName12(app.name);
6040
+ for (const app of asArray35(stack.apps)) {
6041
+ const appName = strName13(app.name);
5430
6042
  if (!appName) continue;
5431
6043
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
5432
6044
  const walkNav = (items) => {
5433
- for (const item of asArray31(items)) {
5434
- const id = strName12(item.id);
6045
+ for (const item of asArray35(items)) {
6046
+ const id = strName13(item.id);
5435
6047
  if (id) navIds.add(id);
5436
6048
  if (item.children) walkNav(item.children);
5437
6049
  }
5438
6050
  };
5439
6051
  walkNav(app.navigation);
5440
- for (const area of asArray31(app.areas)) {
5441
- const areaId = strName12(area.id);
6052
+ for (const area of asArray35(app.areas)) {
6053
+ const areaId = strName13(area.id);
5442
6054
  if (areaId) navIds.add(areaId);
5443
6055
  walkNav(area.navigation);
5444
6056
  }
5445
6057
  apps.set(appName, navIds);
5446
6058
  }
5447
6059
  const dashboards = /* @__PURE__ */ new Map();
5448
- for (const dash of asArray31(stack.dashboards)) {
5449
- const dashName = strName12(dash.name);
6060
+ for (const dash of asArray35(stack.dashboards)) {
6061
+ const dashName = strName13(dash.name);
5450
6062
  if (!dashName) continue;
5451
6063
  const widgets = /* @__PURE__ */ new Set();
5452
- for (const widget of asArray31(dash.widgets)) {
5453
- const id = strName12(widget.id) ?? strName12(widget.name);
6064
+ for (const widget of asArray35(dash.widgets)) {
6065
+ const id = strName13(widget.id) ?? strName13(widget.name);
5454
6066
  if (id) widgets.add(id);
5455
6067
  }
5456
6068
  const actions = /* @__PURE__ */ new Set();
5457
6069
  const headerActions = [
5458
- ...asArray31(isRec9(dash.header) ? dash.header.actions : void 0),
5459
- ...asArray31(dash.actions)
6070
+ ...asArray35(isRec13(dash.header) ? dash.header.actions : void 0),
6071
+ ...asArray35(dash.actions)
5460
6072
  ];
5461
6073
  for (const action of headerActions) {
5462
- const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
6074
+ const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
5463
6075
  if (key) actions.add(key);
5464
6076
  }
5465
6077
  dashboards.set(dashName, { widgets, actions });
@@ -5471,7 +6083,7 @@ function localePath(bundleIndex, locale) {
5471
6083
  }
5472
6084
  function validateTranslationReferences(stack) {
5473
6085
  const findings = [];
5474
- if (!isRec9(stack)) return findings;
6086
+ if (!isRec13(stack)) return findings;
5475
6087
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
5476
6088
  if (bundles.length === 0) return findings;
5477
6089
  const universe = buildUniverse(stack);
@@ -5480,13 +6092,13 @@ function validateTranslationReferences(stack) {
5480
6092
  };
5481
6093
  for (let bi = 0; bi < bundles.length; bi++) {
5482
6094
  const bundle = bundles[bi];
5483
- if (!isRec9(bundle)) continue;
6095
+ if (!isRec13(bundle)) continue;
5484
6096
  for (const [locale, rawData] of Object.entries(bundle)) {
5485
- if (!isRec9(rawData)) continue;
6097
+ if (!isRec13(rawData)) continue;
5486
6098
  const base = localePath(bi, locale);
5487
6099
  const inLocale = `locale "${locale}"`;
5488
6100
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
5489
- if (!isRec9(rawNode)) continue;
6101
+ if (!isRec13(rawNode)) continue;
5490
6102
  const objPath = `${base}.objects.${objectName}`;
5491
6103
  const facts = universe.objects.get(objectName);
5492
6104
  if (!facts) {
@@ -5512,7 +6124,7 @@ function validateTranslationReferences(stack) {
5512
6124
  );
5513
6125
  continue;
5514
6126
  }
5515
- if (!isRec9(rawField)) continue;
6127
+ if (!isRec13(rawField)) continue;
5516
6128
  checkOptionKeys(findings, {
5517
6129
  optionMap: rawField.options,
5518
6130
  field,
@@ -5594,7 +6206,7 @@ function validateTranslationReferences(stack) {
5594
6206
  );
5595
6207
  continue;
5596
6208
  }
5597
- if (!isRec9(rawApp)) continue;
6209
+ if (!isRec13(rawApp)) continue;
5598
6210
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
5599
6211
  if (navIds.has(navId)) continue;
5600
6212
  orphan(
@@ -5617,7 +6229,7 @@ function validateTranslationReferences(stack) {
5617
6229
  );
5618
6230
  continue;
5619
6231
  }
5620
- if (!isRec9(rawDash)) continue;
6232
+ if (!isRec13(rawDash)) continue;
5621
6233
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
5622
6234
  if (dash.widgets.has(widgetId)) continue;
5623
6235
  orphan(
@@ -5642,7 +6254,7 @@ function validateTranslationReferences(stack) {
5642
6254
  return findings;
5643
6255
  }
5644
6256
  function asRecord(v) {
5645
- return isRec9(v) ? v : {};
6257
+ return isRec13(v) ? v : {};
5646
6258
  }
5647
6259
  function checkOptionKeys(findings, ctx) {
5648
6260
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -5654,7 +6266,7 @@ function checkOptionKeys(findings, ctx) {
5654
6266
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
5655
6267
  where: ctx.where,
5656
6268
  path: ctx.path,
5657
- 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.`,
6269
+ 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.`,
5658
6270
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
5659
6271
  });
5660
6272
  return;
@@ -5673,11 +6285,11 @@ function checkOptionKeys(findings, ctx) {
5673
6285
  }
5674
6286
  }
5675
6287
  function checkActionParams(findings, ctx) {
5676
- const rawParams = Object.keys(asRecord(isRec9(ctx.rawAction) ? ctx.rawAction.params : void 0));
6288
+ const rawParams = Object.keys(asRecord(isRec13(ctx.rawAction) ? ctx.rawAction.params : void 0));
5677
6289
  if (rawParams.length === 0) return;
5678
6290
  const declared = /* @__PURE__ */ new Set();
5679
- for (const param of asArray31(ctx.action.params)) {
5680
- const name = strName12(param.name) ?? strName12(param.field);
6291
+ for (const param of asArray35(ctx.action.params)) {
6292
+ const name = strName13(param.name) ?? strName13(param.field);
5681
6293
  if (name) declared.add(name);
5682
6294
  }
5683
6295
  for (const paramName of rawParams) {
@@ -5693,16 +6305,161 @@ function checkActionParams(findings, ctx) {
5693
6305
  }
5694
6306
  }
5695
6307
 
6308
+ // src/validate-translatable-sections.ts
6309
+ var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
6310
+ function isRec14(v) {
6311
+ return !!v && typeof v === "object" && !Array.isArray(v);
6312
+ }
6313
+ function strName14(v) {
6314
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6315
+ }
6316
+ function viewObjectName2(view) {
6317
+ return strName14(view.objectName) ?? strName14(view.object) ?? (isRec14(view.data) ? strName14(view.data.object) : void 0);
6318
+ }
6319
+ function collectionEntries(v, base) {
6320
+ if (Array.isArray(v)) {
6321
+ const out = [];
6322
+ for (let i = 0; i < v.length; i++) {
6323
+ if (isRec14(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
6324
+ }
6325
+ return out;
6326
+ }
6327
+ if (isRec14(v)) {
6328
+ return Object.entries(v).filter(([, def]) => isRec14(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
6329
+ }
6330
+ return [];
6331
+ }
6332
+ function viewLabel(view) {
6333
+ const name = strName14(view.name);
6334
+ return name ? `view "${name}"` : "";
6335
+ }
6336
+ function joinWhere(...parts) {
6337
+ return parts.filter((p) => p.length > 0).join(" \xB7 ");
6338
+ }
6339
+ function collectViewSites(view, basePath, label2, sites) {
6340
+ const recordObject = viewObjectName2(view);
6341
+ const listBinding = isRec14(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
6342
+ const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
6343
+ sites.push({
6344
+ path: `${basePath}.sections`,
6345
+ surface: label2,
6346
+ objectName: recordObject ?? listBinding,
6347
+ sections: view.sections
6348
+ });
6349
+ if (isRec14(view.form)) {
6350
+ sites.push({
6351
+ path: `${basePath}.form.sections`,
6352
+ surface: joinWhere(label2, "form"),
6353
+ objectName: bindingOf(view.form) ?? listBinding,
6354
+ sections: view.form.sections
6355
+ });
6356
+ }
6357
+ for (const key of ["listViews", "formViews"]) {
6358
+ const container = view[key];
6359
+ if (!isRec14(container)) continue;
6360
+ for (const [subKey, sub] of Object.entries(container)) {
6361
+ if (!isRec14(sub)) continue;
6362
+ sites.push({
6363
+ path: `${basePath}.${key}.${subKey}.sections`,
6364
+ surface: joinWhere(label2, `${key}.${subKey}`),
6365
+ objectName: bindingOf(sub) ?? listBinding,
6366
+ sections: sub.sections
6367
+ });
6368
+ }
6369
+ }
6370
+ }
6371
+ function translatedObjectNames(stack) {
6372
+ const out = /* @__PURE__ */ new Set();
6373
+ const bundles = Array.isArray(stack.translations) ? stack.translations : [];
6374
+ for (const bundle of bundles) {
6375
+ if (!isRec14(bundle)) continue;
6376
+ for (const data of Object.values(bundle)) {
6377
+ if (!isRec14(data) || !isRec14(data.objects)) continue;
6378
+ for (const [objectName, node] of Object.entries(data.objects)) {
6379
+ if (isRec14(node)) out.add(objectName);
6380
+ }
6381
+ }
6382
+ }
6383
+ return out;
6384
+ }
6385
+ function suggestedName(label2) {
6386
+ const slug = label2.toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
6387
+ return slug.length > 0 ? slug : void 0;
6388
+ }
6389
+ function validateTranslatableSections(stack) {
6390
+ const findings = [];
6391
+ if (!isRec14(stack)) return findings;
6392
+ const translated = translatedObjectNames(stack);
6393
+ if (translated.size === 0) return findings;
6394
+ const sites = [];
6395
+ for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
6396
+ const objectName = strName14(obj.name);
6397
+ if (!objectName) continue;
6398
+ for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
6399
+ collectViewSites(
6400
+ { ...view, object: strName14(view.object) ?? objectName },
6401
+ path,
6402
+ viewLabel(view),
6403
+ sites
6404
+ );
6405
+ }
6406
+ if (isRec14(obj.listViews)) {
6407
+ collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
6408
+ }
6409
+ }
6410
+ for (const { rec: view, path } of collectionEntries(stack.views, "views")) {
6411
+ collectViewSites(view, path, viewLabel(view), sites);
6412
+ }
6413
+ for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
6414
+ const pageName = strName14(page.name);
6415
+ const pageLabel = pageName ? `page "${pageName}"` : "";
6416
+ for (const walked of walkPageComponents(page, pagePath)) {
6417
+ if (!walked.objectName) continue;
6418
+ const props = isRec14(walked.component.properties) ? walked.component.properties : void 0;
6419
+ if (!props) continue;
6420
+ const type = strName14(walked.component.type) ?? "component";
6421
+ sites.push({
6422
+ path: `${walked.path}.properties.sections`,
6423
+ surface: joinWhere(pageLabel, type),
6424
+ objectName: walked.objectName,
6425
+ sections: props.sections
6426
+ });
6427
+ }
6428
+ }
6429
+ for (const site of sites) {
6430
+ const objectName = site.objectName;
6431
+ if (!objectName || !translated.has(objectName)) continue;
6432
+ if (!Array.isArray(site.sections)) continue;
6433
+ for (let i = 0; i < site.sections.length; i++) {
6434
+ const section = site.sections[i];
6435
+ if (!isRec14(section)) continue;
6436
+ if (strName14(section.name)) continue;
6437
+ const heading = strName14(section.label) ?? strName14(section.title);
6438
+ if (!heading) continue;
6439
+ const slug = suggestedName(heading);
6440
+ findings.push({
6441
+ severity: "warning",
6442
+ rule: TRANSLATION_SECTION_NAME_MISSING,
6443
+ where: joinWhere(`object "${objectName}"`, site.surface, `section "${heading}"`),
6444
+ path: `${site.path}[${i}]`,
6445
+ 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.`,
6446
+ 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.`
6447
+ });
6448
+ }
6449
+ }
6450
+ return findings;
6451
+ }
6452
+
5696
6453
  // src/validate-ai-surface-affinity.ts
5697
6454
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
5698
- function asArray32(v) {
6455
+ function asArray36(v) {
5699
6456
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5700
6457
  if (v && typeof v === "object") {
5701
6458
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5702
6459
  }
5703
6460
  return [];
5704
6461
  }
5705
- function strName13(v) {
6462
+ function strName15(v) {
5706
6463
  return typeof v === "string" && v.length > 0 ? v : void 0;
5707
6464
  }
5708
6465
  function surfaceOf(v) {
@@ -5712,18 +6469,18 @@ function validateAiSurfaceAffinity(stack) {
5712
6469
  const findings = [];
5713
6470
  if (!stack || typeof stack !== "object") return findings;
5714
6471
  const skillsByName = /* @__PURE__ */ new Map();
5715
- for (const skill of asArray32(stack.skills)) {
5716
- const n = strName13(skill.name);
6472
+ for (const skill of asArray36(stack.skills)) {
6473
+ const n = strName15(skill.name);
5717
6474
  if (n) skillsByName.set(n, skill);
5718
6475
  }
5719
- const agents = asArray32(stack.agents);
6476
+ const agents = asArray36(stack.agents);
5720
6477
  for (let ai = 0; ai < agents.length; ai++) {
5721
6478
  const agent = agents[ai];
5722
- const agentName = strName13(agent.name) ?? `#${ai}`;
6479
+ const agentName = strName15(agent.name) ?? `#${ai}`;
5723
6480
  const agentSurface = surfaceOf(agent.surface);
5724
6481
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
5725
6482
  for (let si = 0; si < skillRefs.length; si++) {
5726
- const ref = strName13(skillRefs[si]);
6483
+ const ref = strName15(skillRefs[si]);
5727
6484
  if (!ref) continue;
5728
6485
  const skill = skillsByName.get(ref);
5729
6486
  if (!skill) continue;
@@ -5745,14 +6502,14 @@ function validateAiSurfaceAffinity(stack) {
5745
6502
  // src/validate-ai-tool-references.ts
5746
6503
  import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
5747
6504
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
5748
- function asArray33(v) {
6505
+ function asArray37(v) {
5749
6506
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5750
6507
  if (v && typeof v === "object") {
5751
6508
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5752
6509
  }
5753
6510
  return [];
5754
6511
  }
5755
- function strName14(v) {
6512
+ function strName16(v) {
5756
6513
  return typeof v === "string" && v.length > 0 ? v : void 0;
5757
6514
  }
5758
6515
  function distance6(a, b) {
@@ -5793,26 +6550,26 @@ function materialisesAsTool(action) {
5793
6550
  if (!ai || typeof ai !== "object") return false;
5794
6551
  const aiRec = ai;
5795
6552
  if (aiRec.exposed !== true) return false;
5796
- if (!strName14(aiRec.description)) return false;
5797
- const type = strName14(action.type);
6553
+ if (!strName16(aiRec.description)) return false;
6554
+ const type = strName16(action.type);
5798
6555
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
5799
6556
  if (type === "script") return Boolean(action.target || action.body);
5800
6557
  return Boolean(action.target);
5801
6558
  }
5802
6559
  function collectToolUniverse(stack) {
5803
6560
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
5804
- for (const tool of asArray33(stack.tools)) {
5805
- const n = strName14(tool.name);
6561
+ for (const tool of asArray37(stack.tools)) {
6562
+ const n = strName16(tool.name);
5806
6563
  if (n) universe.add(n);
5807
6564
  }
5808
6565
  const addActionFamily = (actions) => {
5809
- for (const action of asArray33(actions)) {
5810
- const n = strName14(action.name);
6566
+ for (const action of asArray37(actions)) {
6567
+ const n = strName16(action.name);
5811
6568
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
5812
6569
  }
5813
6570
  };
5814
6571
  addActionFamily(stack.actions);
5815
- for (const obj of asArray33(stack.objects)) {
6572
+ for (const obj of asArray37(stack.objects)) {
5816
6573
  addActionFamily(obj.actions);
5817
6574
  }
5818
6575
  return universe;
@@ -5820,13 +6577,13 @@ function collectToolUniverse(stack) {
5820
6577
  function collectUnexposedActionNames(stack) {
5821
6578
  const names = /* @__PURE__ */ new Set();
5822
6579
  const scan = (actions) => {
5823
- for (const action of asArray33(actions)) {
5824
- const n = strName14(action.name);
6580
+ for (const action of asArray37(actions)) {
6581
+ const n = strName16(action.name);
5825
6582
  if (n && !materialisesAsTool(action)) names.add(n);
5826
6583
  }
5827
6584
  };
5828
6585
  scan(stack.actions);
5829
- for (const obj of asArray33(stack.objects)) scan(obj.actions);
6586
+ for (const obj of asArray37(stack.objects)) scan(obj.actions);
5830
6587
  return names;
5831
6588
  }
5832
6589
  function validateAiToolReferences(stack) {
@@ -5844,13 +6601,13 @@ function validateAiToolReferences(stack) {
5844
6601
  }
5845
6602
  return universe.has(ref);
5846
6603
  };
5847
- const skills = asArray33(stack.skills);
6604
+ const skills = asArray37(stack.skills);
5848
6605
  for (let si = 0; si < skills.length; si++) {
5849
6606
  const skill = skills[si];
5850
- const skillName = strName14(skill.name) ?? `#${si}`;
6607
+ const skillName = strName16(skill.name) ?? `#${si}`;
5851
6608
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
5852
6609
  for (let ti = 0; ti < refs.length; ti++) {
5853
- const ref = strName14(refs[ti]);
6610
+ const ref = strName16(refs[ti]);
5854
6611
  if (!ref || resolves(ref)) continue;
5855
6612
  const isPattern = ref.endsWith("*");
5856
6613
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -5869,24 +6626,24 @@ function validateAiToolReferences(stack) {
5869
6626
 
5870
6627
  // src/validate-ai-agent-authoring.ts
5871
6628
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
5872
- function asArray34(v) {
6629
+ function asArray38(v) {
5873
6630
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5874
6631
  if (v && typeof v === "object") {
5875
6632
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5876
6633
  }
5877
6634
  return [];
5878
6635
  }
5879
- function strName15(v) {
6636
+ function strName17(v) {
5880
6637
  return typeof v === "string" && v.length > 0 ? v : void 0;
5881
6638
  }
5882
6639
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
5883
6640
  function validateAiAgentAuthoring(stack) {
5884
6641
  const findings = [];
5885
6642
  if (!stack || typeof stack !== "object") return findings;
5886
- const agents = asArray34(stack.agents);
6643
+ const agents = asArray38(stack.agents);
5887
6644
  for (let ai = 0; ai < agents.length; ai++) {
5888
6645
  const agent = agents[ai];
5889
- const name = strName15(agent.name) ?? `#${ai}`;
6646
+ const name = strName17(agent.name) ?? `#${ai}`;
5890
6647
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
5891
6648
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
5892
6649
  findings.push({
@@ -5902,14 +6659,14 @@ function validateAiAgentAuthoring(stack) {
5902
6659
  }
5903
6660
 
5904
6661
  // src/validate-hook-body-writes.ts
5905
- import { createRequire as createRequire3 } from "module";
6662
+ import { createRequire as createRequire4 } from "module";
5906
6663
  import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
5907
6664
  var cachedTs2 = null;
5908
6665
  function loadTypeScript2() {
5909
6666
  if (cachedTs2) return cachedTs2;
5910
6667
  const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5911
6668
  try {
5912
- cachedTs2 = createRequire3(anchor)("typescript");
6669
+ cachedTs2 = createRequire4(anchor)("typescript");
5913
6670
  } catch (err) {
5914
6671
  throw new Error(
5915
6672
  `@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.`
@@ -5990,24 +6747,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
5990
6747
  "owner",
5991
6748
  "record_type"
5992
6749
  ]);
5993
- var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5994
- function asArray35(v) {
5995
- if (Array.isArray(v)) return v.filter((x) => isRec10(x));
5996
- if (isRec10(v)) {
6750
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6751
+ function asArray39(v) {
6752
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
6753
+ if (isRec15(v)) {
5997
6754
  return Object.entries(v).map(([name, def]) => ({
5998
6755
  name,
5999
- ...isRec10(def) ? def : {}
6756
+ ...isRec15(def) ? def : {}
6000
6757
  }));
6001
6758
  }
6002
6759
  return [];
6003
6760
  }
6004
6761
  function indexObjectFields2(stack) {
6005
6762
  const out = /* @__PURE__ */ new Map();
6006
- for (const obj of asArray35(stack.objects)) {
6763
+ for (const obj of asArray39(stack.objects)) {
6007
6764
  const name = typeof obj.name === "string" ? obj.name : void 0;
6008
6765
  if (!name) continue;
6009
6766
  const names = /* @__PURE__ */ new Set();
6010
- for (const f of asArray35(obj.fields)) {
6767
+ for (const f of asArray39(obj.fields)) {
6011
6768
  if (typeof f.name === "string" && f.name) names.add(f.name);
6012
6769
  }
6013
6770
  out.set(name, names);
@@ -6137,12 +6894,12 @@ ${source}
6137
6894
  }
6138
6895
  function validateHookBodyWrites(stack) {
6139
6896
  const findings = [];
6140
- const hooks = asArray35(stack.hooks);
6897
+ const hooks = asArray39(stack.hooks);
6141
6898
  if (hooks.length === 0) return findings;
6142
6899
  let objectFields = null;
6143
6900
  hooks.forEach((hook, hookIndex) => {
6144
6901
  const body = hook.body;
6145
- if (!isRec10(body) || body.language !== "js") return;
6902
+ if (!isRec15(body) || body.language !== "js") return;
6146
6903
  const source = body.source;
6147
6904
  if (typeof source !== "string" || source.trim() === "") return;
6148
6905
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -6223,13 +6980,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
6223
6980
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
6224
6981
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
6225
6982
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
6226
- var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6227
- function asArray36(v) {
6228
- if (Array.isArray(v)) return v.filter((x) => isRec11(x));
6229
- if (isRec11(v)) {
6983
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6984
+ function asArray40(v) {
6985
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
6986
+ if (isRec16(v)) {
6230
6987
  return Object.entries(v).map(([name, def]) => ({
6231
6988
  name,
6232
- ...isRec11(def) ? def : {}
6989
+ ...isRec16(def) ? def : {}
6233
6990
  }));
6234
6991
  }
6235
6992
  return [];
@@ -6243,11 +7000,11 @@ function collectActionBodies(stack) {
6243
7000
  const sites = [];
6244
7001
  const seen = /* @__PURE__ */ new Set();
6245
7002
  const collect = (actions, pathPrefix, parentObject) => {
6246
- asArray36(actions).forEach((action, index) => {
7003
+ asArray40(actions).forEach((action, index) => {
6247
7004
  const type = typeof action.type === "string" ? action.type : "script";
6248
7005
  if (type !== "script") return;
6249
7006
  const body = action.body;
6250
- if (!isRec11(body) || body.language !== "js") return;
7007
+ if (!isRec16(body) || body.language !== "js") return;
6251
7008
  const source = body.source;
6252
7009
  if (typeof source !== "string" || source.trim() === "") return;
6253
7010
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -6258,7 +7015,7 @@ function collectActionBodies(stack) {
6258
7015
  });
6259
7016
  };
6260
7017
  collect(stack.actions, "actions");
6261
- asArray36(stack.objects).forEach((obj, objIndex) => {
7018
+ asArray40(stack.objects).forEach((obj, objIndex) => {
6262
7019
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
6263
7020
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
6264
7021
  });
@@ -6266,7 +7023,7 @@ function collectActionBodies(stack) {
6266
7023
  }
6267
7024
  function validateActionBodyWrites(stack) {
6268
7025
  const findings = [];
6269
- if (!isRec11(stack)) return findings;
7026
+ if (!isRec16(stack)) return findings;
6270
7027
  const sites = collectActionBodies(stack);
6271
7028
  if (sites.length === 0) return findings;
6272
7029
  let objectFields = null;
@@ -6325,13 +7082,13 @@ import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSu
6325
7082
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
6326
7083
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
6327
7084
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
6328
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6329
- function asArray37(v) {
6330
- if (Array.isArray(v)) return v.filter((x) => isRec12(x));
6331
- if (isRec12(v)) {
7085
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7086
+ function asArray41(v) {
7087
+ if (Array.isArray(v)) return v.filter((x) => isRec17(x));
7088
+ if (isRec17(v)) {
6332
7089
  return Object.entries(v).map(([name, def]) => ({
6333
7090
  name,
6334
- ...isRec12(def) ? def : {}
7091
+ ...isRec17(def) ? def : {}
6335
7092
  }));
6336
7093
  }
6337
7094
  return [];
@@ -6344,8 +7101,8 @@ function readLiteralObjectName2(config) {
6344
7101
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
6345
7102
  function validateFlowNodeWrites(stack) {
6346
7103
  const findings = [];
6347
- if (!isRec12(stack)) return findings;
6348
- const flows = asArray37(stack.flows);
7104
+ if (!isRec17(stack)) return findings;
7105
+ const flows = asArray41(stack.flows);
6349
7106
  if (flows.length === 0) return findings;
6350
7107
  let objectFields = null;
6351
7108
  flows.forEach((flow, flowIndex) => {
@@ -6353,10 +7110,10 @@ function validateFlowNodeWrites(stack) {
6353
7110
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
6354
7111
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
6355
7112
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
6356
- const config = isRec12(node.config) ? node.config : void 0;
7113
+ const config = isRec17(node.config) ? node.config : void 0;
6357
7114
  if (!config) return;
6358
7115
  const fields = config.fields;
6359
- if (!isRec12(fields)) return;
7116
+ if (!isRec17(fields)) return;
6360
7117
  const written = Object.keys(fields);
6361
7118
  if (written.length === 0) return;
6362
7119
  const objectName = readLiteralObjectName2(config);
@@ -6403,6 +7160,14 @@ var REFERENCE_INTEGRITY_RULES = [
6403
7160
  // `component` (an unregistered ref renders a named diagnostic, not silence).
6404
7161
  { name: "validateNavTargetRefs", run: validateNavTargetRefs },
6405
7162
  { name: "validateTranslationReferences", run: validateTranslationReferences },
7163
+ // The same family from the other end (#5417). Its sibling above asks "does
7164
+ // this bundle key resolve?"; this one asks "is there a key at all?" — a form
7165
+ // section authored with a `label` and no `name` renders a heading that
7166
+ // `_sections` (keyed by name) can never address, so neither the orphan check
7167
+ // nor the coverage walk can see it. A reference that cannot be written is
7168
+ // still a reference question, and warning-only for the same reason its
7169
+ // sibling is: one heading stays in the source locale, nothing breaks.
7170
+ { name: "validateTranslatableSections", run: validateTranslatableSections },
6406
7171
  { name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
6407
7172
  { name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
6408
7173
  { name: "validateAiToolReferences", run: validateAiToolReferences },
@@ -6488,7 +7253,12 @@ function validateReferenceIntegrity(stack) {
6488
7253
  }
6489
7254
 
6490
7255
  // src/lint-flow-patterns.ts
6491
- function asArray38(v) {
7256
+ import {
7257
+ APPROVAL_NODE_TYPE as APPROVAL_NODE_TYPE2,
7258
+ APPROVAL_REVISE_NODE_TYPE,
7259
+ collectFlowGraphs as collectFlowGraphs2
7260
+ } from "@objectstack/spec/automation";
7261
+ function asArray42(v) {
6492
7262
  if (Array.isArray(v)) return v;
6493
7263
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6494
7264
  return [];
@@ -6511,6 +7281,7 @@ var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
6511
7281
  var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
6512
7282
  var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
6513
7283
  var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
7284
+ var FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED = "flow-approval-revise-target-not-service-owned";
6514
7285
  var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
6515
7286
  var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
6516
7287
  var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
@@ -6518,6 +7289,7 @@ var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
6518
7289
  var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
6519
7290
  var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
6520
7291
  var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
7292
+ var FLOW_MULTI_WRITE_UNFILTERED = "flow-multi-write-unfiltered";
6521
7293
  var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
6522
7294
  "decision",
6523
7295
  "assignment",
@@ -6540,6 +7312,36 @@ var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
6540
7312
  "end"
6541
7313
  ]);
6542
7314
  var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
7315
+ var RUNAS_EFFECTIVE_IDENTITY = "`runAs:'user'` (the default when none is declared)";
7316
+ function findDataNodeAnywhere(nodes, edges) {
7317
+ for (const graph of collectFlowGraphs2({
7318
+ nodes,
7319
+ edges
7320
+ })) {
7321
+ for (const node of graph.nodes) {
7322
+ if (DATA_NODE_TYPES.has(typeof node.type === "string" ? node.type : "")) {
7323
+ return { node, scope: graph.scope };
7324
+ }
7325
+ }
7326
+ }
7327
+ return null;
7328
+ }
7329
+ var BULK_WRITE_CONSEQUENCE = /* @__PURE__ */ new Map([
7330
+ ["delete_record", {
7331
+ verb: "deleted",
7332
+ engineCall: "driver.deleteMany",
7333
+ // The delete dispatch is the one that is EXTRACTED and case-set-pinned
7334
+ // (`engine-delete-dispatch.ts`), so it can be cited by name.
7335
+ dispatchNote: "the engine's delete-dispatch case-set lists `multi with no predicate at all` as a legal `multi` call"
7336
+ }],
7337
+ ["update_record", {
7338
+ verb: "overwritten",
7339
+ engineCall: "driver.updateMany",
7340
+ // Update has no extracted dispatch module, so the branch itself is the
7341
+ // authority — and its refusal fires only WITHOUT the declaration.
7342
+ 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)"
7343
+ }]
7344
+ ]);
6543
7345
  var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
6544
7346
  var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
6545
7347
  function isScheduleTriggered(flow, startCfg) {
@@ -6624,7 +7426,7 @@ function collectTemplateStrings(value, key, out) {
6624
7426
  function edgeLabelOf(e) {
6625
7427
  return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
6626
7428
  }
6627
- function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
7429
+ function scanErrorLabelledEdges(at, nodes, edges, findings) {
6628
7430
  const typeById = /* @__PURE__ */ new Map();
6629
7431
  for (const n of nodes) {
6630
7432
  if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
@@ -6637,14 +7439,14 @@ function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
6637
7439
  const src = typeof e.source === "string" ? e.source : "";
6638
7440
  if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
6639
7441
  findings.push({
6640
- where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
7442
+ where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6641
7443
  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.`,
6642
7444
  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)`,
6643
7445
  rule: FLOW_ERROR_LABEL_NOT_FAULT
6644
7446
  });
6645
7447
  }
6646
7448
  }
6647
- function scanBranchRouting(flowName, nodes, edges, findings) {
7449
+ function scanBranchRouting(at, nodes, edges, findings) {
6648
7450
  const outEdgesBySource = /* @__PURE__ */ new Map();
6649
7451
  for (const e of edges) {
6650
7452
  if (e.type === "fault") continue;
@@ -6657,7 +7459,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6657
7459
  for (const e of outs) {
6658
7460
  if (e.isDefault === true && e.condition) {
6659
7461
  findings.push({
6660
- where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
7462
+ where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6661
7463
  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.`,
6662
7464
  hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
6663
7465
  rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
@@ -6670,7 +7472,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6670
7472
  const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
6671
7473
  if (defaults.length > 1) {
6672
7474
  findings.push({
6673
- where: `flow '${flowName}' \xB7 node '${src}'`,
7475
+ where: `${at} \xB7 node '${src}'`,
6674
7476
  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".`,
6675
7477
  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)`,
6676
7478
  rule: FLOW_MULTIPLE_DEFAULT_EDGES
@@ -6683,7 +7485,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6683
7485
  const cfg = node.config ?? {};
6684
7486
  if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
6685
7487
  findings.push({
6686
- where: `flow '${flowName}' \xB7 node '${String(node.id)}' (${nodeType})`,
7488
+ where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
6687
7489
  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.)`,
6688
7490
  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)`,
6689
7491
  rule: FLOW_INERT_NODE_CONDITION
@@ -6703,7 +7505,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6703
7505
  const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
6704
7506
  if (unclaimed.length > 0) {
6705
7507
  findings.push({
6706
- where: `flow '${flowName}' \xB7 decision '${nid}'`,
7508
+ where: `${at} \xB7 decision '${nid}'`,
6707
7509
  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.`,
6708
7510
  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)`,
6709
7511
  rule: FLOW_BRANCH_LABEL_UNMATCHED,
@@ -6719,7 +7521,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6719
7521
  );
6720
7522
  if (ungated.length > 0) {
6721
7523
  findings.push({
6722
- where: `flow '${flowName}' \xB7 decision '${nid}'`,
7524
+ where: `${at} \xB7 decision '${nid}'`,
6723
7525
  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\`.`,
6724
7526
  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)`,
6725
7527
  rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
@@ -6727,10 +7529,39 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
6727
7529
  }
6728
7530
  }
6729
7531
  }
6730
- function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6731
- const approvals = nodes.filter((n) => n.type === "approval");
7532
+ function filterCarriesNoCondition(filter) {
7533
+ if (filter === void 0 || filter === null) return true;
7534
+ if (typeof filter !== "object" || Array.isArray(filter)) return false;
7535
+ return Object.keys(filter).length === 0;
7536
+ }
7537
+ function scanUnboundedBulkWrites(at, nodes, findings) {
7538
+ for (const node of nodes) {
7539
+ const nodeType = typeof node.type === "string" ? node.type : "";
7540
+ const consequence2 = BULK_WRITE_CONSEQUENCE.get(nodeType);
7541
+ if (!consequence2) continue;
7542
+ const cfg = node.config ?? {};
7543
+ if (cfg.multi !== true) continue;
7544
+ if (!filterCarriesNoCondition(cfg.filter)) continue;
7545
+ const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
7546
+ const filterState = cfg.filter === void 0 || cfg.filter === null ? "no `filter` key" : "an EMPTY `filter`";
7547
+ findings.push({
7548
+ where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
7549
+ 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.`,
7550
+ 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)`,
7551
+ // Warning, not `error`: see the severity policy at the top of this file.
7552
+ // The shape has a legitimate reading the engine grants on purpose, so it is
7553
+ // not provably wrong — unlike the gating members of this family.
7554
+ rule: FLOW_MULTI_WRITE_UNFILTERED
7555
+ });
7556
+ }
7557
+ }
7558
+ function scanApprovalReviseLoops(at, nodes, edges, findings) {
7559
+ const approvals = nodes.filter((n) => n.type === APPROVAL_NODE_TYPE2);
6732
7560
  if (approvals.length === 0) return;
6733
7561
  const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
7562
+ const nodeTypeById = new Map(
7563
+ nodes.filter((n) => typeof n.id === "string").map((n) => [n.id, typeof n.type === "string" ? n.type : ""])
7564
+ );
6734
7565
  const outEdges = /* @__PURE__ */ new Map();
6735
7566
  for (const e of edges) {
6736
7567
  const src = typeof e.source === "string" ? e.source : "";
@@ -6743,7 +7574,18 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6743
7574
  if (!aid) continue;
6744
7575
  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));
6745
7576
  if (reviseTargets.length === 0) continue;
6746
- const where = `flow '${flowName}' \xB7 approval '${aid}'`;
7577
+ const where = `${at} \xB7 approval '${aid}'`;
7578
+ for (const target of reviseTargets) {
7579
+ const targetType = nodeTypeById.get(target) ?? "";
7580
+ if (targetType === APPROVAL_REVISE_NODE_TYPE) continue;
7581
+ findings.push({
7582
+ where,
7583
+ severity: "error",
7584
+ message: `has a 'revise' out-edge into node '${target}' of type '${targetType || "(untyped)"}' \u2014 the revise window must be an '${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.`,
7585
+ hint: `Set node '${target}' to \`type: '${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).`,
7586
+ rule: FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED
7587
+ });
7588
+ }
6747
7589
  const cfg = a.config ?? {};
6748
7590
  if (cfg.maxRevisions === 0) {
6749
7591
  findings.push({
@@ -6771,7 +7613,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6771
7613
  findings.push({
6772
7614
  where,
6773
7615
  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.)`,
6774
- 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.`,
7616
+ hint: `Close the loop: the 'revise' edge should reach an '${APPROVAL_REVISE_NODE_TYPE}' node whose resubmit edge returns to '${aid}' marked \`type: 'back'\` (ADR-0044). See examples/app-showcase showcase_budget_approval.`,
6775
7617
  rule: FLOW_APPROVAL_REVISE_DEAD_END
6776
7618
  });
6777
7619
  } else if (!returnEdges.some((e) => e.type === "back")) {
@@ -6786,7 +7628,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6786
7628
  }
6787
7629
  function lintFlowPatterns(stack) {
6788
7630
  const findings = [];
6789
- for (const flow of asArray38(stack.flows)) {
7631
+ for (const flow of asArray42(stack.flows)) {
6790
7632
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
6791
7633
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
6792
7634
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
@@ -6807,74 +7649,90 @@ function lintFlowPatterns(stack) {
6807
7649
  const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
6808
7650
  const userLessKind = userLessTriggerKind(flow, startCfg);
6809
7651
  if (userLessKind && runAs !== "system") {
6810
- const dataNode = nodes.find((n) => DATA_NODE_TYPES.has(typeof n.type === "string" ? n.type : ""));
7652
+ const dataNode = findDataNodeAnywhere(nodes, edges);
6811
7653
  if (dataNode) {
6812
- const declared = typeof flow.runAs === "string" ? `\`runAs:'${runAs}'\`` : `the default \`runAs:'user'\``;
7654
+ const at = dataNode.scope ? `, in ${dataNode.scope},` : "";
6813
7655
  findings.push({
6814
7656
  where: `flow '${flowName}' \xB7 runAs`,
6815
- 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.`,
7657
+ 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.`,
6816
7658
  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)`,
6817
7659
  rule: FLOW_RUNAS_UNSCOPED,
6818
7660
  severity: "error"
6819
7661
  });
6820
7662
  }
6821
7663
  }
6822
- for (const node of nodes) {
6823
- const nodeWhere = `flow '${flowName}' \xB7 node '${node.id}' (${node.type})`;
6824
- const cfg = node.config ?? {};
6825
- if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
6826
- for (const key of Object.keys(cfg)) {
6827
- if (PHANTOM_AGG_KEYS.has(key)) {
6828
- findings.push({
6829
- where: nodeWhere,
6830
- 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.`,
6831
- 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)`,
6832
- rule: FLOW_PHANTOM_AGGREGATION
6833
- });
6834
- }
6835
- }
6836
- const strings = [];
6837
- collectTemplateStrings(node.config, void 0, strings);
6838
- for (const str2 of strings) {
6839
- if (DOUBLE_BRACE.test(str2)) {
6840
- findings.push({
6841
- where: nodeWhere,
6842
- message: `double-brace interpolation \`${str2.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
6843
- hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
6844
- rule: FLOW_DOUBLE_BRACE_INTERP
6845
- });
7664
+ for (const graph of collectFlowGraphs2({
7665
+ // A cast, not a parse. `FlowNodeSchema.config` is an open `z.record`, so a
7666
+ // region's contents arrive as raw authored records even in a parsed stack —
7667
+ // a nested edge `condition` may still be a bare string where a top-level
7668
+ // one is an Expression envelope. Every rule below reads both
7669
+ // (`conditionSource`), and the walk itself only touches `type` / `config`.
7670
+ // The already-guarded arrays are passed rather than `flow` itself so a
7671
+ // non-array `nodes` still cannot throw: this function promises it never does.
7672
+ nodes,
7673
+ edges
7674
+ })) {
7675
+ const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
7676
+ const graphNodes = graph.nodes;
7677
+ const graphEdges = graph.edges;
7678
+ for (const node of graphNodes) {
7679
+ const nodeWhere = `${at} \xB7 node '${node.id}' (${node.type})`;
7680
+ const cfg = node.config ?? {};
7681
+ if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
7682
+ for (const key of Object.keys(cfg)) {
7683
+ if (PHANTOM_AGG_KEYS.has(key)) {
7684
+ findings.push({
7685
+ where: nodeWhere,
7686
+ 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.`,
7687
+ 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)`,
7688
+ rule: FLOW_PHANTOM_AGGREGATION
7689
+ });
7690
+ }
6846
7691
  }
6847
- if (BARE_DOLLAR_REF.test(str2)) {
6848
- findings.push({
6849
- where: nodeWhere,
6850
- message: `\`${str2.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
6851
- hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
6852
- rule: FLOW_BARE_DOLLAR_REF
6853
- });
7692
+ const strings = [];
7693
+ collectTemplateStrings(stripRegions(node.config), void 0, strings);
7694
+ for (const str4 of strings) {
7695
+ if (DOUBLE_BRACE.test(str4)) {
7696
+ findings.push({
7697
+ where: nodeWhere,
7698
+ message: `double-brace interpolation \`${str4.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
7699
+ hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
7700
+ rule: FLOW_DOUBLE_BRACE_INTERP
7701
+ });
7702
+ }
7703
+ if (BARE_DOLLAR_REF.test(str4)) {
7704
+ findings.push({
7705
+ where: nodeWhere,
7706
+ message: `\`${str4.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
7707
+ hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
7708
+ rule: FLOW_BARE_DOLLAR_REF
7709
+ });
7710
+ }
6854
7711
  }
6855
7712
  }
7713
+ scanApprovalReviseLoops(at, graphNodes, graphEdges, findings);
7714
+ scanErrorLabelledEdges(at, graphNodes, graphEdges, findings);
7715
+ scanBranchRouting(at, graphNodes, graphEdges, findings);
7716
+ scanUnboundedBulkWrites(at, graphNodes, findings);
6856
7717
  }
6857
- scanApprovalReviseLoops(flowName, nodes, edges, findings);
6858
- scanErrorLabelledEdges(flowName, nodes, edges, findings);
6859
- scanBranchRouting(flowName, nodes, edges, findings);
6860
7718
  }
6861
7719
  return findings;
6862
7720
  }
6863
7721
 
6864
7722
  // src/lint-liveness-properties.ts
6865
- import { createRequire as createRequire4 } from "module";
7723
+ import { createRequire as createRequire5 } from "module";
6866
7724
  import { dirname, join } from "path";
6867
7725
  import { existsSync, readFileSync } from "fs";
6868
7726
  var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
6869
7727
  var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
6870
- function asArray39(v) {
7728
+ function asArray43(v) {
6871
7729
  if (Array.isArray(v)) return v;
6872
7730
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6873
7731
  return [];
6874
7732
  }
6875
7733
  function resolveLivenessDir() {
6876
7734
  try {
6877
- const require2 = createRequire4(import.meta.url);
7735
+ const require2 = createRequire5(import.meta.url);
6878
7736
  const pkgJson = require2.resolve("@objectstack/spec/package.json");
6879
7737
  const dir = join(dirname(pkgJson), "liveness");
6880
7738
  return existsSync(dir) ? dir : null;
@@ -6980,7 +7838,16 @@ var TYPE_COLLECTIONS = [
6980
7838
  { type: "job", key: "jobs" },
6981
7839
  { type: "email_template", key: "emailTemplates" },
6982
7840
  { type: "mapping", key: "mappings" },
6983
- { type: "translation", key: "translations" }
7841
+ { type: "translation", key: "translations" },
7842
+ // #4956 — dashboard joins the list the moment its ledger first warns on
7843
+ // anything, which is exactly the rule the comment above states. Drilling
7844
+ // `widgets` produced five warned keys (`colorVariant`, `actionUrl`,
7845
+ // `actionType`, `actionIcon`, `aria`), all under `widgets[]`; `getNested`
7846
+ // fans a dotted path out over an array level, so `widgets.colorVariant`
7847
+ // checks every widget on the dashboard. Registering it here is not optional
7848
+ // bookkeeping: without it the ledger would be newly correct and newly
7849
+ // silent, which is the shape this lint exists to prevent.
7850
+ { type: "dashboard", key: "dashboards" }
6984
7851
  ];
6985
7852
  function lintLivenessProperties(stack) {
6986
7853
  const dir = resolveLivenessDir();
@@ -6988,11 +7855,11 @@ function lintLivenessProperties(stack) {
6988
7855
  const findings = [];
6989
7856
  const objectWarn = loadWarnMap(dir, "object");
6990
7857
  const fieldWarn = loadWarnMap(dir, "field");
6991
- for (const obj of asArray39(stack.objects)) {
7858
+ for (const obj of asArray43(stack.objects)) {
6992
7859
  const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6993
7860
  if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
6994
7861
  if (fieldWarn.size > 0) {
6995
- for (const field of asArray39(obj.fields)) {
7862
+ for (const field of asArray43(obj.fields)) {
6996
7863
  const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
6997
7864
  checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
6998
7865
  }
@@ -7001,7 +7868,7 @@ function lintLivenessProperties(stack) {
7001
7868
  for (const { type, key } of TYPE_COLLECTIONS) {
7002
7869
  const warnMap = loadWarnMap(dir, type);
7003
7870
  if (warnMap.size === 0) continue;
7004
- for (const item of asArray39(stack[key])) {
7871
+ for (const item of asArray43(stack[key])) {
7005
7872
  const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
7006
7873
  checkItem(type, item, `${type} '${name}'`, warnMap, findings);
7007
7874
  }
@@ -7015,7 +7882,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7015
7882
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7016
7883
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
7017
7884
  var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
7018
- function asArray40(v) {
7885
+ function asArray44(v) {
7019
7886
  if (Array.isArray(v)) return v;
7020
7887
  if (v && typeof v === "object") {
7021
7888
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7024,9 +7891,9 @@ function asArray40(v) {
7024
7891
  }
7025
7892
  function lintAutonumberFormats(stack) {
7026
7893
  const findings = [];
7027
- for (const obj of asArray40(stack.objects)) {
7894
+ for (const obj of asArray44(stack.objects)) {
7028
7895
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7029
- const fields = asArray40(obj.fields);
7896
+ const fields = asArray44(obj.fields);
7030
7897
  const fieldMeta = /* @__PURE__ */ new Map();
7031
7898
  for (const f of fields) {
7032
7899
  if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
@@ -7092,7 +7959,7 @@ function lintAutonumberFormats(stack) {
7092
7959
 
7093
7960
  // src/lint-view-refs.ts
7094
7961
  import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
7095
- function asArray41(v) {
7962
+ function asArray45(v) {
7096
7963
  if (Array.isArray(v)) return v;
7097
7964
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7098
7965
  return [];
@@ -7120,7 +7987,7 @@ function lintViewRefs(stack) {
7120
7987
  s.add(kind);
7121
7988
  };
7122
7989
  const containers = [];
7123
- for (const v of asArray41(stack.views)) {
7990
+ for (const v of asArray45(stack.views)) {
7124
7991
  if (v.viewKind) {
7125
7992
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7126
7993
  continue;
@@ -7129,7 +7996,7 @@ function lintViewRefs(stack) {
7129
7996
  const object = viewContainerObjectName(v);
7130
7997
  if (object) containers.push({ object, container: v });
7131
7998
  }
7132
- for (const obj of asArray41(stack.objects)) {
7999
+ for (const obj of asArray45(stack.objects)) {
7133
8000
  const object = typeof obj.name === "string" ? obj.name : void 0;
7134
8001
  if (!object) continue;
7135
8002
  if (obj.list || obj.form || obj.listViews || obj.formViews) {
@@ -7183,11 +8050,11 @@ function lintViewRefs(stack) {
7183
8050
  });
7184
8051
  }
7185
8052
  };
7186
- for (const obj of asArray41(stack.objects)) {
8053
+ for (const obj of asArray45(stack.objects)) {
7187
8054
  const object = typeof obj.name === "string" ? obj.name : void 0;
7188
- for (const action of asArray41(obj.actions)) checkAction(action, object);
8055
+ for (const action of asArray45(obj.actions)) checkAction(action, object);
7189
8056
  }
7190
- for (const action of asArray41(stack.actions)) checkAction(action);
8057
+ for (const action of asArray45(stack.actions)) checkAction(action);
7191
8058
  return findings;
7192
8059
  }
7193
8060
 
@@ -7239,8 +8106,43 @@ function refOf2(def) {
7239
8106
  return def?.reference || def?.reference_to;
7240
8107
  }
7241
8108
  var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
8109
+ var UNIQUE_UNSCOPED_DECLARED_INDEX = "unique/unscoped-declared-index";
8110
+ var UNIQUE_LEGACY_ORGANIZATION_COMPOSITE = "unique/legacy-organization-composite";
8111
+ function authoredTenantColumn(obj) {
8112
+ const declared = obj?.tenancy?.tenantField;
8113
+ return typeof declared === "string" && declared.trim() ? declared.trim() : "organization_id";
8114
+ }
7242
8115
  function uniqueDeclared(u) {
7243
- return u === true || u === "global";
8116
+ return u === true || u === "global" || u === "organization";
8117
+ }
8118
+ function fieldUniqueScope(u) {
8119
+ return u === "global" ? "global" : "organization";
8120
+ }
8121
+ function indexUniqueScope(u) {
8122
+ return u === "organization" ? "organization" : "global";
8123
+ }
8124
+ function lintUnscopedDeclaredIndexes(objects) {
8125
+ const issues = [];
8126
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
8127
+ for (let i = 0; i < objects.length; i++) {
8128
+ const obj = objects[i];
8129
+ if (!obj?.name) continue;
8130
+ const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
8131
+ for (let j = 0; j < declaredIndexes.length; j++) {
8132
+ const idx = declaredIndexes[j];
8133
+ if (idx?.unique !== true) continue;
8134
+ const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
8135
+ const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
8136
+ issues.push({
8137
+ severity: "warning",
8138
+ rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
8139
+ 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).`,
8140
+ path: `objects[${i}].indexes[${j}]`,
8141
+ 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).`
8142
+ });
8143
+ }
8144
+ }
8145
+ return issues;
7244
8146
  }
7245
8147
  function lintUniqueDeclarations(objects) {
7246
8148
  const issues = [];
@@ -7260,23 +8162,70 @@ function lintUniqueDeclarations(objects) {
7260
8162
  if (singleColumnUniqueIndexes.size === 0) continue;
7261
8163
  for (const { name, def } of fieldEntries2(obj.fields)) {
7262
8164
  if (!uniqueDeclared(def?.unique)) continue;
7263
- if (def.unique === "global") continue;
7264
8165
  const idx = singleColumnUniqueIndexes.get(name);
7265
8166
  if (!idx) continue;
8167
+ const fScope = fieldUniqueScope(def.unique);
8168
+ const iScope = indexUniqueScope(idx.unique);
7266
8169
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
8170
+ const fieldSpelling = `\`unique: ${typeof def.unique === "string" ? `'${def.unique}'` : def.unique}\``;
8171
+ const indexSpelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
8172
+ let message;
8173
+ let fix;
8174
+ if (fScope === iScope) {
8175
+ const boundary = fScope === "global" ? "installation-wide" : "per-organization";
8176
+ 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.`;
8177
+ 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.`;
8178
+ } else {
8179
+ const globalSide = fScope === "global" ? `field-level ${fieldSpelling}` : `declared index${indexLabel} (${indexSpelling})`;
8180
+ const orgSide = fScope === "global" ? `declared index${indexLabel} (${indexSpelling})` : `field-level ${fieldSpelling}`;
8181
+ 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.`;
8182
+ 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.`;
8183
+ }
7267
8184
  issues.push({
7268
8185
  severity: "warning",
7269
8186
  rule: UNIQUE_DOUBLE_DECLARATION,
7270
- 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.`,
8187
+ message,
7271
8188
  path: `objects[${i}]`,
7272
- 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.`
8189
+ fix
8190
+ });
8191
+ }
8192
+ }
8193
+ return issues;
8194
+ }
8195
+ function lintLegacyOrganizationComposites(objects) {
8196
+ const issues = [];
8197
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
8198
+ for (let i = 0; i < objects.length; i++) {
8199
+ const obj = objects[i];
8200
+ if (!obj?.name) continue;
8201
+ const tenantColumn = authoredTenantColumn(obj);
8202
+ const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
8203
+ for (let j = 0; j < declaredIndexes.length; j++) {
8204
+ const idx = declaredIndexes[j];
8205
+ if (!uniqueDeclared(idx?.unique) || idx.unique === "organization") continue;
8206
+ const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
8207
+ if (cols.length < 2) continue;
8208
+ if (!cols.includes(tenantColumn)) continue;
8209
+ const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
8210
+ const spelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
8211
+ const rest = cols.filter((c) => c !== tenantColumn);
8212
+ issues.push({
8213
+ severity: "warning",
8214
+ rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
8215
+ 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.`,
8216
+ path: `objects[${i}].indexes[${j}]`,
8217
+ 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.`
7273
8218
  });
7274
8219
  }
7275
8220
  }
7276
8221
  return issues;
7277
8222
  }
7278
8223
  function lintDataModel(objects) {
7279
- const issues = lintUniqueDeclarations(objects);
8224
+ const issues = [
8225
+ ...lintUnscopedDeclaredIndexes(objects),
8226
+ ...lintUniqueDeclarations(objects),
8227
+ ...lintLegacyOrganizationComposites(objects)
8228
+ ];
7280
8229
  if (!Array.isArray(objects) || objects.length === 0) return issues;
7281
8230
  const childrenByParent = {};
7282
8231
  for (const child of objects) {
@@ -7543,6 +8492,42 @@ var AUTHORING_RULES = [
7543
8492
  runtimeTypes: ["flow"],
7544
8493
  run: (stack) => validateReferenceIntegrity(stack)
7545
8494
  },
8495
+ // ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
8496
+ // is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
8497
+ // recurse into it, so until this entry existed the 31 typed prop schemas in
8498
+ // `ComponentPropsMap` were parsed by NOTHING (#4001 批 17's `no gate`
8499
+ // verdict): an undeclared or wrongly-typed prop parsed clean, was retained,
8500
+ // and reached objectui's renderer to be ignored there. This dispatches on
8501
+ // `type` and judges the bag; unregistered types are skipped, which is a
8502
+ // required semantic (`type` is an open union — the example corpus authors 87
8503
+ // nodes of 10 types this map does not carry).
8504
+ //
8505
+ // `normalized` for a reason worth stating, since the props bag survives the
8506
+ // Zod parse UNCHANGED and both tiers would otherwise carry the same data: the
8507
+ // ADR-0087 conversion layer runs inside `normalizeStackInput`, so a converted
8508
+ // alias (`page-header-subtitle-alias` rewrites `properties.description` →
8509
+ // `subtitle`) is already canonical here and is never reported as undeclared —
8510
+ // while a schema error elsewhere in the stack cannot take these findings down
8511
+ // with it.
8512
+ //
8513
+ // Advisory, deliberately, and this is the whole shape of #5068's first step:
8514
+ // wiring the parse is the precondition for enforcement, not the enforcement
8515
+ // (#5020, one surface over). The live corpus violates the declarations in two
8516
+ // places that are open contract questions — inline i18n label maps on three
8517
+ // published platform pages (#5728) and the record picker's declared-but-unread
8518
+ // `displayField` (#5775) — so gating today would fail the platform's own pages
8519
+ // to enforce declarations the platform does not keep. The error upgrade is a
8520
+ // separate step, once the warning-period inventory is empty.
8521
+ {
8522
+ name: "validateComponentProps",
8523
+ tier: "advisory",
8524
+ input: "normalized",
8525
+ commands: ALL,
8526
+ source: "packages/lint/src/validate-component-props.ts",
8527
+ surfaces: CLI_ONLY,
8528
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8529
+ run: (stack) => validateComponentProps(stack)
8530
+ },
7546
8531
  // ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
7547
8532
  // `id`), name real CSS properties + design tokens, and carry a `large` base.
7548
8533
  {
@@ -7606,18 +8591,32 @@ var AUTHORING_RULES = [
7606
8591
  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.',
7607
8592
  run: (stack) => validateCapabilityReferences(stack)
7608
8593
  },
7609
- // A record-change flow whose start-node objectName matches nothing never
7610
- // fires — silently. Reads the pre-parse tier so an author sees what they
7611
- // wrote. Advisory: the object may come from another installed package.
8594
+ // A flow that LOOKS armed and never launches — silently. Reads the pre-parse
8595
+ // tier so an author sees what they wrote.
8596
+ //
8597
+ // `gating` since #5762, which reviewed the file's rules as one family and
8598
+ // split them on a single question: is THIS STACK enough to know the flow is
8599
+ // dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
8600
+ // the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
8601
+ // predicate cannot route at all, and a `record-*` triggerType outside the
8602
+ // closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
8603
+ // can be changed by installing a package, so there is no reading under which
8604
+ // the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
8605
+ // (the object may come from another installed package — a hedge this rule
8606
+ // cannot decide), as did `flow-draft-status-ambiguous` (draft flows DO fire;
8607
+ // that one is ambiguity of intent, not a dead flow).
7612
8608
  {
7613
8609
  name: "validateFlowTriggerReadiness",
7614
- tier: "advisory",
8610
+ tier: "gating",
7615
8611
  input: "normalized",
7616
8612
  commands: ALL,
7617
8613
  source: "packages/lint/src/validate-flow-trigger-readiness.ts",
7618
- // Runtime publish gate (#4463): the FLOW family. Advisory at this surface
7619
- // too its findings are logged, not thrown (P1 gates on `error` only; P2
7620
- // puts advisories on the response for Studio to render).
8614
+ // Runtime publish gate (#4463): the FLOW family. Its `error` findings now
8615
+ // REFUSE a `state: 'active'` write (P1 gates on `error` only); the rules that
8616
+ // stayed `warning` keep being logged as advisories. The gate judges a
8617
+ // snapshot whose `flows` holds only the written item and subtracts the
8618
+ // baseline's findings, so this refuses the dead flow's own publish — never
8619
+ // another flow's save on account of a stored one.
7621
8620
  surfaces: CLI_AND_RUNTIME,
7622
8621
  runtimeTypes: ["flow"],
7623
8622
  run: (stack) => validateFlowTriggerReadiness(stack)
@@ -7828,9 +8827,31 @@ var AUTHORING_RULES = [
7828
8827
  hint: f.hint
7829
8828
  }))
7830
8829
  },
7831
- // #3991 — a column carrying BOTH a field-level `unique: true` and a
7832
- // single-column declared unique index has two intents, of which exactly one
7833
- // takes effect (the global index wins; the tenant composite is unreachable).
8830
+ // ADR-0120 D5a — a declared index with bare `unique: true` states no scope
8831
+ // at all (`unique/unscoped-declared-index` the #4986 trap). Fires on the
8832
+ // spelling alone, no tenancy inference; 17.x warns, protocol 18 rejects the
8833
+ // spelling (#5082).
8834
+ {
8835
+ name: "lintUnscopedDeclaredIndexes",
8836
+ tier: "advisory",
8837
+ input: "parsed",
8838
+ commands: ["validate", "build"],
8839
+ source: "packages/lint/src/data-model-rules.ts",
8840
+ surfaces: CLI_ONLY,
8841
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8842
+ 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.",
8843
+ run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
8844
+ severity: f.severity === "suggestion" ? "info" : f.severity,
8845
+ rule: f.rule,
8846
+ where: f.path,
8847
+ path: f.path,
8848
+ message: f.message,
8849
+ hint: f.fix ?? ""
8850
+ }))
8851
+ },
8852
+ // #3991 / ADR-0120 D5b — a column carrying BOTH a field-level `unique` and a
8853
+ // single-column declared unique index states two scopes of which at most one
8854
+ // takes effect (`unique/double-declaration`, the four-quadrant matrix).
7834
8855
  {
7835
8856
  name: "lintUniqueDeclarations",
7836
8857
  tier: "advisory",
@@ -7849,6 +8870,28 @@ var AUTHORING_RULES = [
7849
8870
  hint: f.fix ?? ""
7850
8871
  }))
7851
8872
  },
8873
+ // ADR-0120 D5c — a declared unique listing the organization column IS the
8874
+ // hand-written per-organization composite (S6). Advisory nudge toward the
8875
+ // `'organization'` respelling, which is also what closes its NULL hole
8876
+ // (#5030). Never auto-fixed: opting in is a real D4 tightening.
8877
+ {
8878
+ name: "lintLegacyOrganizationComposites",
8879
+ tier: "advisory",
8880
+ input: "parsed",
8881
+ commands: ["validate", "build"],
8882
+ source: "packages/lint/src/data-model-rules.ts",
8883
+ surfaces: CLI_ONLY,
8884
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8885
+ 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.",
8886
+ run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
8887
+ severity: f.severity === "suggestion" ? "info" : f.severity,
8888
+ rule: f.rule,
8889
+ where: f.path,
8890
+ path: f.path,
8891
+ message: f.message,
8892
+ hint: f.fix ?? ""
8893
+ }))
8894
+ },
7852
8895
  // ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
7853
8896
  // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
7854
8897
  // binding gate, vocabulary freeze), moving the failure from a runtime deny to
@@ -7876,6 +8919,88 @@ var AUTHORING_RULES = [
7876
8919
  surfaces: CLI_ONLY,
7877
8920
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7878
8921
  run: (stack) => validateOrgAxisRedLines(stack)
8922
+ },
8923
+ // #4698 — the "declared but never read" gate, for the one surface where the
8924
+ // predicate is EXACT rather than inferred. A sharing rule's `condition` has a
8925
+ // single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
8926
+ // the key is `compileCelToFilter(condition, { variables: {} })`; a condition
8927
+ // that does not lower means the rule is SKIPPED at boot, so the grant is
8928
+ // declared and does not exist. The lint calls that same compiler, from the
8929
+ // same package, with the same options — the verdict cannot drift from the
8930
+ // consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
8931
+ // docblock states: the whole authorable surface is enforced, and this was the
8932
+ // one field where that sentence was not yet true.
8933
+ {
8934
+ name: "validateSharingRuleEnforceability",
8935
+ tier: "gating",
8936
+ input: "parsed",
8937
+ commands: ALL,
8938
+ source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
8939
+ surfaces: CLI_ONLY,
8940
+ 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.",
8941
+ run: (stack) => validateSharingRuleEnforceability(stack)
8942
+ },
8943
+ // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
8944
+ // which had never been wired to anything: `isSupportedRlsExpression` existed
8945
+ // solely so an authoring command could reject a predicate the runtime drops,
8946
+ // and no authoring command called it. An unlowerable
8947
+ // `rowLevelSecurity[].using` is DROPPED by `RLSCompiler` and — when it is the
8948
+ // only applicable policy — replaced by `RLS_DENY_FILTER`, so the policy reads
8949
+ // as an authorization and behaves as a blanket refusal. Same construction as
8950
+ // the sharing-rule entry: the verdict is the runtime's own function, reached
8951
+ // through `@objectstack/formula` (where #4983 hoisted it), never a model of it.
8952
+ {
8953
+ name: "validateRlsPredicateEnforceability",
8954
+ tier: "gating",
8955
+ input: "parsed",
8956
+ commands: ALL,
8957
+ source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
8958
+ surfaces: CLI_ONLY,
8959
+ 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.",
8960
+ run: (stack) => validateRlsPredicateEnforceability(stack)
8961
+ },
8962
+ // #4762 — the same "declared but enforces nothing" question, for the two
8963
+ // STATIC artifacts an object validation rule carries. A `format` rule's
8964
+ // `regex` that `new RegExp(...)` throws on, and a `json_schema` rule's schema
8965
+ // ajv cannot compile, are both logged and SKIPPED on the write path
8966
+ // (`rule-validator.ts`), so the rule ships, lists, and protects nothing.
8967
+ // Neither needs a record to judge, so the authoring door is the right one:
8968
+ // rejecting a broken regex at RUNTIME instead would reject every write
8969
+ // touching that field for as long as the metadata is deployed (#4762's own
8970
+ // analysis — the runtime-backstop question stays open for the maintainer).
8971
+ // Gating for the `lint-flow-patterns.ts` bar: no reading of the metadata
8972
+ // behaves as written, because the rule does not run at all.
8973
+ {
8974
+ name: "validateRuleCompilability",
8975
+ tier: "gating",
8976
+ input: "parsed",
8977
+ commands: ALL,
8978
+ source: "packages/lint/src/validate-rule-compilability.ts",
8979
+ surfaces: CLI_ONLY,
8980
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8981
+ run: (stack) => validateRuleCompilability(stack)
8982
+ },
8983
+ // #5178 — the residual half of #5029, which registering `ajv-formats` does
8984
+ // NOT close: under `strict: false` a MISSPELLED format name (`emial`) is
8985
+ // logged once and DROPPED, so the rule compiles, ships, runs on every write
8986
+ // and enforces nothing for the keyword its author wrote — and the record is
8987
+ // accepted, which is the silent direction. Deliberately its own entry rather
8988
+ // than a third finding inside the rule above: that one's whole contract is
8989
+ // compiling in the runtime's exact environment, and a typo'd format compiles
8990
+ // there. This judges the format NAME against the registered set (enumerated
8991
+ // from the same ajv instance, never a hardcoded list) and compiles nothing,
8992
+ // so the #4762/#5029 compile parity is untouched — a judgement beside the
8993
+ // compile, not a divergent compile. Gating for the `lint-flow-patterns.ts`
8994
+ // bar: no reading of the metadata behaves as written.
8995
+ {
8996
+ name: "validateRuleSchemaFormats",
8997
+ tier: "gating",
8998
+ input: "parsed",
8999
+ commands: ALL,
9000
+ source: "packages/lint/src/validate-rule-schema-formats.ts",
9001
+ surfaces: CLI_ONLY,
9002
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9003
+ run: (stack) => validateRuleSchemaFormats(stack)
7879
9004
  }
7880
9005
  ];
7881
9006
  function authoringRulesFor(command) {
@@ -7985,6 +9110,7 @@ export {
7985
9110
  APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
7986
9111
  APPROVAL_APPROVER_TYPE_DEPRECATED,
7987
9112
  APPROVAL_APPROVER_TYPE_UNKNOWN,
9113
+ APPROVAL_APPROVER_TYPE_UNSUPPORTED,
7988
9114
  APPROVAL_DECISION_OUTPUTS_RESERVED,
7989
9115
  APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
7990
9116
  APPROVAL_EXPRESSION_INVALID,
@@ -8003,15 +9129,19 @@ export {
8003
9129
  CHART_DIMENSION_UNKNOWN,
8004
9130
  CHART_FIELD_UNKNOWN,
8005
9131
  CHART_MEASURE_UNKNOWN,
9132
+ COMPONENT_PROPS_INVALID,
9133
+ COMPONENT_PROPS_UNKNOWN_KEY,
8006
9134
  DASHBOARD_ACTION_ROUTE_UNRESOLVED,
8007
9135
  DASHBOARD_ACTION_TARGET_UNDEFINED,
8008
9136
  DASHBOARD_FILTER_FIELD_UNKNOWN,
8009
9137
  EXPRESSION_INVALID,
8010
9138
  FIELD_GROUP_EMPTY,
9139
+ FIELD_GROUP_SHADOWED,
8011
9140
  FIELD_GROUP_UNDECLARED,
8012
9141
  FILTER_TOKEN_UNKNOWN,
8013
9142
  FLOW_APPROVAL_REVISE_DEAD_END,
8014
9143
  FLOW_APPROVAL_REVISE_DISABLED,
9144
+ FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED,
8015
9145
  FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
8016
9146
  FLOW_BARE_DOLLAR_REF,
8017
9147
  FLOW_BRANCH_LABEL_UNMATCHED,
@@ -8023,12 +9153,16 @@ export {
8023
9153
  FLOW_ERROR_LABEL_NOT_FAULT,
8024
9154
  FLOW_INERT_NODE_CONDITION,
8025
9155
  FLOW_MULTIPLE_DEFAULT_EDGES,
9156
+ FLOW_MULTI_WRITE_UNFILTERED,
8026
9157
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
8027
9158
  FLOW_PHANTOM_AGGREGATION,
8028
9159
  FLOW_RUNAS_UNSCOPED,
8029
9160
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
8030
9161
  FLOW_TEMPLATE_UNKNOWN_FIELD,
8031
9162
  FLOW_TIME_RELATIVE_ANTIPATTERN,
9163
+ FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
9164
+ FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
9165
+ FLOW_TRIGGER_UNKNOWN_EVENT,
8032
9166
  FLOW_TRIGGER_UNKNOWN_OBJECT,
8033
9167
  FLOW_UPDATE_READONLY_FIELD,
8034
9168
  FLOW_UPDATE_READONLY_WHEN_FIELD,
@@ -8043,6 +9177,7 @@ export {
8043
9177
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
8044
9178
  LIVENESS_DEAD_PROPERTY,
8045
9179
  LIVENESS_EXPERIMENTAL_PROPERTY,
9180
+ MAX_SCHEMA_WALK_DEPTH,
8046
9181
  MEASURE_AGGREGATE_INCOHERENT,
8047
9182
  NAV_OBJECT_UNGRANTED,
8048
9183
  NAV_TARGET_UNRESOLVED,
@@ -8056,14 +9191,19 @@ export {
8056
9191
  REACT_BLOCK_NEEDS_RECORD_CONTEXT,
8057
9192
  REACT_CHART_AGGREGATE_INVALID,
8058
9193
  REACT_CHART_AXIS_UNKNOWN,
9194
+ REACT_CHART_DRILLDOWN_INVALID,
8059
9195
  REACT_CHART_FIELD_UNKNOWN,
8060
9196
  REFERENCE_INTEGRITY_RULES,
9197
+ RLS_PREDICATE_UNENFORCEABLE,
9198
+ RLS_PREDICATE_UNPARSEABLE,
9199
+ RUNTIME_AJV_OPTIONS,
8061
9200
  SEARCHABLE_FIELD_UNKNOWN,
8062
9201
  SEARCHABLE_FIELD_UNSEARCHABLE,
8063
9202
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
8064
9203
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
8065
9204
  SECURITY_DELEGATION_MISSING_REASON,
8066
9205
  SECURITY_EXTERNAL_WIDER,
9206
+ SECURITY_FLS_UNQUALIFIED_KEY,
8067
9207
  SECURITY_GRANT_EXPIRED_AT_AUTHORING,
8068
9208
  SECURITY_MASTER_DETAIL_UNGRANTED,
8069
9209
  SECURITY_OWD_ALIAS,
@@ -8074,6 +9214,8 @@ export {
8074
9214
  SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
8075
9215
  SEED_VALUE_OUTSIDE_STATE_MACHINE,
8076
9216
  SEMANTIC_ROLE_FIELD_UNKNOWN,
9217
+ SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
9218
+ SHARING_RULE_UNLOWERABLE_CONDITION,
8077
9219
  STYLE_CLASSNAME_TAILWIND,
8078
9220
  STYLE_NODE_MISSING_ID,
8079
9221
  STYLE_RESPONSIVE_NO_BASE,
@@ -8083,8 +9225,14 @@ export {
8083
9225
  TITLE_FORMAT_RETIRED,
8084
9226
  TITLE_UNRESOLVABLE,
8085
9227
  TRANSLATION_OPTION_KEY_UNKNOWN,
9228
+ TRANSLATION_SECTION_NAME_MISSING,
8086
9229
  TRANSLATION_TARGET_UNKNOWN,
8087
9230
  UNIQUE_DOUBLE_DECLARATION,
9231
+ UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
9232
+ UNIQUE_UNSCOPED_DECLARED_INDEX,
9233
+ VALIDATION_RULE_REGEX_UNCOMPILABLE,
9234
+ VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
9235
+ VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
8088
9236
  VIEW_CONTAINER_SHAPE,
8089
9237
  VIEW_KEY_COLLISION,
8090
9238
  VIEW_REF_FORM_TARGET_KIND,
@@ -8093,6 +9241,8 @@ export {
8093
9241
  VISIBILITY_ROOT_MISLAYERED,
8094
9242
  WIDGET_DATASET_UNKNOWN,
8095
9243
  WIDGET_DIMENSION_UNKNOWN,
9244
+ WIDGET_LEGACY_ANALYTICS_SHAPE,
9245
+ WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
8096
9246
  WIDGET_MEASURE_UNKNOWN,
8097
9247
  authoringRulesFor,
8098
9248
  buildAccessMatrix,
@@ -8100,12 +9250,16 @@ export {
8100
9250
  extractHookBodyWriteSet,
8101
9251
  extractHookBodyWrites,
8102
9252
  findUnguardedNullableOperands,
9253
+ isSourceAuthoredPage,
8103
9254
  lintAutonumberFormats,
8104
9255
  lintDataModel,
8105
9256
  lintFlowPatterns,
9257
+ lintLegacyOrganizationComposites,
8106
9258
  lintLivenessProperties,
8107
9259
  lintUniqueDeclarations,
9260
+ lintUnscopedDeclaredIndexes,
8108
9261
  lintViewRefs,
9262
+ nearestRegisteredFormat,
8109
9263
  nullGuardMessage,
8110
9264
  runAuthoringRules,
8111
9265
  runRuntimeAuthoringRules,
@@ -8122,6 +9276,7 @@ export {
8122
9276
  validateApprovalApprovers,
8123
9277
  validateCapabilityReferences,
8124
9278
  validateChartBindings,
9279
+ validateComponentProps,
8125
9280
  validateDashboardActionRefs,
8126
9281
  validateFilterTokens,
8127
9282
  validateFlowNodeWrites,
@@ -8144,15 +9299,21 @@ export {
8144
9299
  validateRecordTitle,
8145
9300
  validateReferenceIntegrity,
8146
9301
  validateResponsiveStyles,
9302
+ validateRlsPredicateEnforceability,
9303
+ validateRuleCompilability,
9304
+ validateRuleSchemaFormats,
8147
9305
  validateSearchableFields,
8148
9306
  validateSecurityPosture,
8149
9307
  validateSeedReplaySafety,
8150
9308
  validateSeedStateMachine,
8151
9309
  validateSemanticRoles,
9310
+ validateSharingRuleEnforceability,
8152
9311
  validateStackExpressions,
9312
+ validateTranslatableSections,
8153
9313
  validateTranslationReferences,
8154
9314
  validateViewContainers,
8155
9315
  validateVisibilityPredicates,
8156
- validateWidgetBindings
9316
+ validateWidgetBindings,
9317
+ walkPageComponents
8157
9318
  };
8158
9319
  //# sourceMappingURL=index.js.map