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

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
@@ -333,7 +333,7 @@ function validateWidgetBindings(stack) {
333
333
  }
334
334
 
335
335
  // src/validate-expressions.ts
336
- import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
336
+ import { validateExpression, collectCelRootIdentifiers, SCOPE_ROOTS } from "@objectstack/formula";
337
337
  import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
338
338
 
339
339
  // src/validate-null-guards.ts
@@ -694,6 +694,38 @@ function validateStackExpressions(stack) {
694
694
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
695
695
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
696
696
  };
697
+ const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
698
+ const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
699
+ const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
700
+ const FIELD_RULE_SLOT_CONSEQUENCE = {
701
+ visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
702
+ readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). Per ADR-0057 D10 the server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
703
+ requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
704
+ // Listed rather than left to the `??` below, so the map covers every slot
705
+ // the field walk passes and the default stays unreachable. `FieldSchema`
706
+ // declares this key only as a `retiredKey`, which rejects it by name, so
707
+ // there is no fourth runtime to measure — the honest clause is the generic
708
+ // one, not a fabricated fourth cell (#6716).
709
+ conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
710
+ };
711
+ const checkFieldRuleRoot = (where, slot, raw) => {
712
+ const source = celSourceOf(raw);
713
+ if (!source) return;
714
+ const roots = collectCelRootIdentifiers(source);
715
+ if (!roots.ok) return;
716
+ const kept = SCOPE_ROOTS.filter(
717
+ (r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
718
+ );
719
+ if (kept.length === 0) return;
720
+ const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
721
+ const prescription = FIELD_RULE_USER_ROOTS.includes(root) ? `To gate the CHOICES of a select by user, move the predicate to the option's own \`visibleWhen\` (\`options: [{ \u2026, visibleWhen: \u2026 }]\`) \u2014 per-option is the one \`*When\` surface that binds \`current_user\` and its ADR-0068 aliases. To hide the FIELD by role, declare field-level security on a permission set (\`fields: { '<object>.<field>': { readable: false } }\`), which the server enforces. To gate on record state, rewrite the predicate against \`record\`.` : root === "data" ? `\`data\` is the root of a METADATA form (a \`*.form\` module \u2014 the metadata row being edited); this is an OBJECT field, whose runtime form binds the row as \`record\` \u2014 one key name, two form kinds, two roots. Rewrite \`data.<key>\` as \`record.<field>\`.` : `\`${root}\` is declared platform-wide and bound at OTHER evaluation sites (flow, automation, screen and action predicates), never at the field level. Rewrite the predicate against \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item), or move the decision to a surface that binds \`${root}\`.`;
722
+ issues.push({
723
+ where,
724
+ message: `\`${slot}\` reads \`${root}\`, but a field-level conditional rule binds only \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item) \u2014 \`${root}\` is unbound here, so ${FIELD_RULE_SLOT_CONSEQUENCE[slot] ?? FIELD_RULE_SLOT_CONSEQUENCE_GENERIC}. ` + prescription,
725
+ source,
726
+ severity: "error"
727
+ });
728
+ };
697
729
  const checkDeclaredPredicate = (where, raw) => {
698
730
  if (raw == null) return;
699
731
  const res = validateExpression("predicate", raw);
@@ -726,7 +758,14 @@ function validateStackExpressions(stack) {
726
758
  if (retired.length > 0) {
727
759
  issues.push({
728
760
  where: `${at} \xB7 node '${node.id}' (script) callable`,
729
- message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + `Run \`os migrate meta --from 16\` to rewrite it automatically.`,
761
+ message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's
762
+ // behaviour, never the retired key's fate — "rewrite it" reads two ways
763
+ // over a branch that DELETES the key (template/recipients/variables/script),
764
+ // "rewrite existing sources" only one. Plain-quoted (not a template literal)
765
+ // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
766
+ // widened scan (#7030) on the same textual shape as the spec corpus — no
767
+ // interpolation lives in this clause, so nothing is lost switching quote style.
768
+ "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
730
769
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
731
770
  });
732
771
  } else if (!fn) {
@@ -760,13 +799,27 @@ function validateStackExpressions(stack) {
760
799
  for (const [fname, f] of fieldList) {
761
800
  for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
762
801
  check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
802
+ checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
803
+ }
804
+ for (const [oi, opt] of asArray2(f.options).entries()) {
805
+ const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
806
+ check(
807
+ `object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
808
+ opt.visibleWhen,
809
+ objectName,
810
+ "record"
811
+ );
763
812
  }
764
- const roWhenSource = celSourceOf(f.readonlyWhen);
765
- if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
813
+ for (const [slot, raw, consequence2] of [
814
+ ["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
815
+ ["requiredWhen", f.requiredWhen, `the requirement would never be enforced \u2014 the predicate faults, the server logs and skips it, and the field stays optional in the database`]
816
+ ]) {
817
+ const source = celSourceOf(raw);
818
+ if (masters === 1 || !source || !readsParentRoot(source)) continue;
766
819
  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,
820
+ where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
821
+ message: `\`${slot}\` 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 ${consequence2}. ` + (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\`.`),
822
+ source,
770
823
  severity: "error"
771
824
  });
772
825
  }
@@ -859,6 +912,347 @@ function validateStackExpressions(stack) {
859
912
  return issues;
860
913
  }
861
914
 
915
+ // src/lint-startup-registry-verdict.ts
916
+ import { createRequire } from "module";
917
+ var cachedTs = null;
918
+ function loadTypeScript() {
919
+ if (cachedTs) return cachedTs;
920
+ const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
921
+ try {
922
+ cachedTs = createRequire(anchor)("typescript");
923
+ } catch (err) {
924
+ throw new Error(
925
+ `@objectstack/lint: checking plugin source for startup registry verdicts 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 plugin source is actually checked.`
926
+ );
927
+ }
928
+ return cachedTs;
929
+ }
930
+ var STARTUP_OPEN_VOCABULARY_VERDICT = "startup-open-vocabulary-verdict";
931
+ var STARTUP_VERDICT_ASSERTIVE_WORDING = "startup-verdict-assertive-wording";
932
+ var OPEN_VOCABULARY_PROBES = /* @__PURE__ */ new Map([
933
+ [
934
+ "getRegisteredNodeTypes",
935
+ "ADR-0018 keeps the flow node-type vocabulary open and runtime-extensible \u2014 a plugin registers its executor from its own init()/start(), which can be after this line (#4771)"
936
+ ],
937
+ [
938
+ "knownNodeTypes",
939
+ "ADR-0018 keeps the flow node-type vocabulary open and runtime-extensible \u2014 a plugin registers its executor from its own init()/start(), which can be after this line (#4771)"
940
+ ],
941
+ [
942
+ "getUnknownNodeTypeAudit",
943
+ 'the unknown-node-type audit is a snapshot of an OPEN vocabulary a plugin can still extend during boot; it is read-only by design and answers "unknown as of now", never "unknown for this deployment" (#4771)'
944
+ ],
945
+ [
946
+ "getActionDescriptors",
947
+ 'ADR-0018 action descriptors are published by plugins during boot, so a type missing from them here means "not published YET" (#4771)'
948
+ ],
949
+ [
950
+ "getRegisteredExecutors",
951
+ "executors are contributed by plugins during boot \u2014 an executor missing here may simply be a plugin that has not started"
952
+ ],
953
+ [
954
+ "listExecutors",
955
+ "executors are contributed by plugins during boot \u2014 an executor missing here may simply be a plugin that has not started"
956
+ ],
957
+ [
958
+ "getRegisteredTools",
959
+ "AI tools are contributed by plugins during boot, so the registered set is not final until every plugin has started"
960
+ ],
961
+ [
962
+ "listConnectors",
963
+ "connectors are contributed by plugins during boot (StackSchema.connectors documents provider-bound instances registered by plugins), so the list is not final until every plugin has started"
964
+ ],
965
+ [
966
+ "getRegisteredConnectors",
967
+ "connectors are contributed by plugins during boot, so the registered set is not final until every plugin has started"
968
+ ],
969
+ [
970
+ "listCapabilities",
971
+ "ADR-0066 capabilities are declared by every installed package, so the set is not final until every plugin has contributed its declarations"
972
+ ],
973
+ [
974
+ "getRegisteredCapabilities",
975
+ "ADR-0066 capabilities are declared by every installed package, so the set is not final until every plugin has contributed its declarations"
976
+ ],
977
+ [
978
+ "listProviders",
979
+ "providers are contributed by plugins during boot \u2014 a provider missing here may simply be a plugin that has not started"
980
+ ],
981
+ [
982
+ "getRegisteredProviders",
983
+ "providers are contributed by plugins during boot \u2014 a provider missing here may simply be a plugin that has not started"
984
+ ]
985
+ ]);
986
+ var PRE_SEAL_PHASES = /* @__PURE__ */ new Map([
987
+ ["constructor", "the constructor runs at composition time \u2014 before ANY plugin has been initialized"],
988
+ ["init", "another plugin's init() may not have run yet (ADR-0116), so the vocabulary is still filling"],
989
+ [
990
+ "start",
991
+ "sibling plugins' start() have not all run, and ADR-0018 lets a plugin register its contributions from start() \u2014 the vocabulary is still filling"
992
+ ]
993
+ ]);
994
+ var SEAL_MARKERS = ["seal"];
995
+ var VERDICT_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
996
+ var ALL_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal", "info", "debug", "trace", "log"]);
997
+ var PERSISTENCE_CALLEES = /* @__PURE__ */ new Set(["insert", "insertOne", "update", "updateOne", "upsert", "save", "saveMetaItem"]);
998
+ var ASSERTIVE_PHRASES = [
999
+ "will fail",
1000
+ "fails at execution",
1001
+ "nothing will register",
1002
+ "no plugin provides",
1003
+ "is not installed",
1004
+ "is not configured",
1005
+ "you need",
1006
+ "you must install",
1007
+ "must be provisioned"
1008
+ ];
1009
+ var HEDGE_PHRASES = [
1010
+ "not yet",
1011
+ "yet been registered",
1012
+ "so far",
1013
+ "as of",
1014
+ "may still",
1015
+ "might still",
1016
+ "still be registered",
1017
+ "has started",
1018
+ "have started"
1019
+ ];
1020
+ var STARTUP_VERDICT_HINT = 'Take one of the three shapes the fixes took: (1) resolve where the value is USED, not where you start \u2014 a lazy accessor or a `kernel:ready`/`kernel:bootstrapped` hook sees a provider that registered later (`createLazyCacheRateLimitStorage()` in plugin-auth, #4772); (2) seal the vocabulary, then judge \u2014 have the host declare the moment it can no longer grow and draw the conclusion there (`AutomationEngine.sealNodeTypeVocabulary()`, called at `kernel:bootstrapped`, #4771); (3) order the verdict AFTER the mutation it describes, so it cannot attest to a state this same boot goes on to contradict (the ADR-0104 attestation, #4769). If the conclusion must stay here, keep the two worlds apart in the wording: "no executor registered YET (as of plugin start)" is true; "will fail at execution time" is a claim about a world that has not finished forming.';
1021
+ function isFunctionLike(t, node) {
1022
+ return t.isFunctionDeclaration(node) || t.isFunctionExpression(node) || t.isArrowFunction(node) || t.isMethodDeclaration(node) || t.isConstructorDeclaration(node) || t.isGetAccessorDeclaration(node) || t.isSetAccessorDeclaration(node);
1023
+ }
1024
+ function walkSameTick(t, node, visit) {
1025
+ node.forEachChild((child) => {
1026
+ if (isFunctionLike(t, child) || t.isClassDeclaration(child) || t.isClassExpression(child)) return;
1027
+ visit(child);
1028
+ walkSameTick(t, child, visit);
1029
+ });
1030
+ }
1031
+ function walkAll(t, node, visit) {
1032
+ node.forEachChild((child) => {
1033
+ visit(child);
1034
+ walkAll(t, child, visit);
1035
+ });
1036
+ }
1037
+ function calleeName(t, node) {
1038
+ if (!t.isCallExpression(node)) return void 0;
1039
+ const expr = node.expression;
1040
+ if (t.isIdentifier(expr)) return expr.text;
1041
+ if (t.isPropertyAccessExpression(expr) && t.isIdentifier(expr.name)) return expr.name.text;
1042
+ return void 0;
1043
+ }
1044
+ function loggerLevel(t, node) {
1045
+ if (!t.isCallExpression(node)) return void 0;
1046
+ const expr = node.expression;
1047
+ if (!t.isPropertyAccessExpression(expr) || !t.isIdentifier(expr.name)) return void 0;
1048
+ const level = expr.name.text;
1049
+ if (!ALL_LEVELS.has(level)) return void 0;
1050
+ const receiver = expr.expression;
1051
+ let receiverName;
1052
+ if (t.isIdentifier(receiver)) receiverName = receiver.text;
1053
+ else if (t.isPropertyAccessExpression(receiver) && t.isIdentifier(receiver.name)) receiverName = receiver.name.text;
1054
+ if (!receiverName) return void 0;
1055
+ return /^(logger|log|console)$/i.test(receiverName) ? level : void 0;
1056
+ }
1057
+ function literalText(t, node) {
1058
+ const parts = [];
1059
+ const take = (n) => {
1060
+ if (t.isStringLiteralLike(n)) parts.push(n.text);
1061
+ else if (t.isTemplateHead(n) || t.isTemplateMiddle(n) || t.isTemplateTail(n)) parts.push(n.text);
1062
+ };
1063
+ take(node);
1064
+ walkAll(t, node, take);
1065
+ return parts.join(" ").toLowerCase();
1066
+ }
1067
+ function mentionsSeal(t, node) {
1068
+ let found = false;
1069
+ const visit = (n) => {
1070
+ if (found) return;
1071
+ if (t.isIdentifier(n)) {
1072
+ const lower = n.text.toLowerCase();
1073
+ if (SEAL_MARKERS.some((m) => lower.includes(m))) found = true;
1074
+ }
1075
+ };
1076
+ visit(node);
1077
+ walkAll(t, node, visit);
1078
+ return found;
1079
+ }
1080
+ function moduleLevelMutableBindings(t, sf) {
1081
+ const names = /* @__PURE__ */ new Set();
1082
+ for (const st of sf.statements) {
1083
+ if (!t.isVariableStatement(st)) continue;
1084
+ if (st.declarationList.flags & t.NodeFlags.Const) continue;
1085
+ for (const d of st.declarationList.declarations) {
1086
+ if (t.isIdentifier(d.name)) names.add(d.name.text);
1087
+ }
1088
+ }
1089
+ return names;
1090
+ }
1091
+ function indexFunctionBodies(t, sf) {
1092
+ const byName = /* @__PURE__ */ new Map();
1093
+ walkAll(t, sf, (node) => {
1094
+ if (t.isFunctionDeclaration(node) && node.name && node.body) byName.set(node.name.text, node.body);
1095
+ else if (t.isMethodDeclaration(node) && t.isIdentifier(node.name) && node.body) byName.set(node.name.text, node.body);
1096
+ else if (t.isVariableDeclaration(node) && t.isIdentifier(node.name) && node.initializer && (t.isArrowFunction(node.initializer) || t.isFunctionExpression(node.initializer)) && node.initializer.body) {
1097
+ byName.set(node.name.text, node.initializer.body);
1098
+ }
1099
+ });
1100
+ return byName;
1101
+ }
1102
+ function collectLifecycleUnits(t, sf) {
1103
+ const units = [];
1104
+ const readClass = (node) => {
1105
+ const phases = [];
1106
+ let hasLifecycle = false;
1107
+ for (const member of node.members) {
1108
+ if (t.isMethodDeclaration(member) && t.isIdentifier(member.name)) {
1109
+ if (member.name.text === "init" || member.name.text === "start") hasLifecycle = true;
1110
+ } else if (t.isPropertyDeclaration(member) && t.isIdentifier(member.name) && (member.name.text === "init" || member.name.text === "start") && member.initializer && (t.isArrowFunction(member.initializer) || t.isFunctionExpression(member.initializer))) {
1111
+ hasLifecycle = true;
1112
+ }
1113
+ }
1114
+ if (!hasLifecycle) return;
1115
+ for (const member of node.members) {
1116
+ if (t.isConstructorDeclaration(member) && member.body) {
1117
+ phases.push({ phase: "constructor", body: member.body });
1118
+ } else if (t.isMethodDeclaration(member) && t.isIdentifier(member.name) && member.body) {
1119
+ if (PRE_SEAL_PHASES.has(member.name.text)) phases.push({ phase: member.name.text, body: member.body });
1120
+ } else if (t.isPropertyDeclaration(member) && t.isIdentifier(member.name) && PRE_SEAL_PHASES.has(member.name.text) && member.initializer && (t.isArrowFunction(member.initializer) || t.isFunctionExpression(member.initializer)) && member.initializer.body) {
1121
+ phases.push({ phase: member.name.text, body: member.initializer.body });
1122
+ }
1123
+ }
1124
+ if (phases.length) units.push({ label: node.name?.text ?? "<anonymous class>", phases });
1125
+ };
1126
+ const readObjectLiteral = (node) => {
1127
+ const phases = [];
1128
+ let name;
1129
+ let hasLifecycle = false;
1130
+ for (const prop of node.properties) {
1131
+ if (!t.isPropertyAssignment(prop) || !t.isIdentifier(prop.name)) continue;
1132
+ const key = prop.name.text;
1133
+ if (key === "name" && t.isStringLiteralLike(prop.initializer)) name = prop.initializer.text;
1134
+ if ((key === "init" || key === "start") && isFunctionLike(t, prop.initializer)) hasLifecycle = true;
1135
+ if (PRE_SEAL_PHASES.has(key) && (t.isArrowFunction(prop.initializer) || t.isFunctionExpression(prop.initializer)) && prop.initializer.body) {
1136
+ phases.push({ phase: key, body: prop.initializer.body });
1137
+ }
1138
+ }
1139
+ if (!name || !hasLifecycle || !phases.length) return;
1140
+ units.push({ label: `${name} (object plugin)`, phases });
1141
+ };
1142
+ walkAll(t, sf, (node) => {
1143
+ if (t.isClassDeclaration(node) || t.isClassExpression(node)) readClass(node);
1144
+ else if (t.isObjectLiteralExpression(node)) readObjectLiteral(node);
1145
+ });
1146
+ return units;
1147
+ }
1148
+ function findStartupRegistryVerdicts(source, options = {}) {
1149
+ if (!source || !source.trim()) return [];
1150
+ let mentionsAny = false;
1151
+ for (const probe of OPEN_VOCABULARY_PROBES.keys()) {
1152
+ if (source.includes(probe)) {
1153
+ mentionsAny = true;
1154
+ break;
1155
+ }
1156
+ }
1157
+ if (!mentionsAny) return [];
1158
+ const t = loadTypeScript();
1159
+ const fileLabel = options.file ?? "source";
1160
+ let sf;
1161
+ try {
1162
+ sf = t.createSourceFile(fileLabel, source, t.ScriptTarget.Latest, true, t.ScriptKind.TS);
1163
+ } catch {
1164
+ return [];
1165
+ }
1166
+ const findings = [];
1167
+ const moduleBindings = moduleLevelMutableBindings(t, sf);
1168
+ const functionBodies = indexFunctionBodies(t, sf);
1169
+ const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
1170
+ for (const unit of collectLifecycleUnits(t, sf)) {
1171
+ for (const { phase, body } of unit.phases) {
1172
+ const scopes = [];
1173
+ const seenNames = /* @__PURE__ */ new Set();
1174
+ const collect = (b, depth) => {
1175
+ scopes.push(b);
1176
+ if (depth >= 2) return;
1177
+ walkSameTick(t, b, (child) => {
1178
+ const name = calleeName(t, child);
1179
+ if (!name || seenNames.has(name)) return;
1180
+ const helper = functionBodies.get(name);
1181
+ if (!helper) return;
1182
+ seenNames.add(name);
1183
+ collect(helper, depth + 1);
1184
+ });
1185
+ };
1186
+ collect(body, 0);
1187
+ const reads = [];
1188
+ for (const scope of scopes) {
1189
+ walkSameTick(t, scope, (node) => {
1190
+ const name = calleeName(t, node);
1191
+ if (name && OPEN_VOCABULARY_PROBES.has(name)) reads.push({ probe: name, node });
1192
+ });
1193
+ }
1194
+ if (!reads.length) continue;
1195
+ if (scopes.some((scope) => mentionsSeal(t, scope))) continue;
1196
+ const records = [];
1197
+ for (const scope of scopes) {
1198
+ walkSameTick(t, scope, (node) => {
1199
+ const level = loggerLevel(t, node);
1200
+ if (level && VERDICT_LEVELS.has(level)) {
1201
+ records.push({ kind: "announced", detail: `${level} log`, line: lineOf(node), node });
1202
+ return;
1203
+ }
1204
+ const cn = calleeName(t, node);
1205
+ if (cn && PERSISTENCE_CALLEES.has(cn)) {
1206
+ records.push({ kind: "persisted", detail: `${cn}()`, line: lineOf(node), node });
1207
+ return;
1208
+ }
1209
+ if (t.isBinaryExpression(node) && node.operatorToken.kind === t.SyntaxKind.EqualsToken) {
1210
+ const lhs = node.left;
1211
+ let target;
1212
+ if (t.isPropertyAccessExpression(lhs) && t.isIdentifier(lhs.name)) {
1213
+ target = lhs.expression.kind === t.SyntaxKind.ThisKeyword ? `this.${lhs.name.text}` : `<obj>.${lhs.name.text}`;
1214
+ } else if (t.isIdentifier(lhs) && moduleBindings.has(lhs.text)) {
1215
+ target = `module-level \`${lhs.text}\``;
1216
+ }
1217
+ if (target) records.push({ kind: "cached", detail: target, line: lineOf(node), node });
1218
+ }
1219
+ });
1220
+ }
1221
+ if (!records.length) continue;
1222
+ const read = reads[0];
1223
+ const record = records[0];
1224
+ const where = `${unit.label}.${phase}${phase === "constructor" ? "" : "()"}`;
1225
+ const note = OPEN_VOCABULARY_PROBES.get(read.probe);
1226
+ const phaseNote = PRE_SEAL_PHASES.get(phase);
1227
+ findings.push({
1228
+ severity: "warning",
1229
+ rule: STARTUP_OPEN_VOCABULARY_VERDICT,
1230
+ where,
1231
+ path: `${fileLabel}:${lineOf(read.node)}`,
1232
+ message: `\`${read.probe}()\` reads a vocabulary that is still filling, and the conclusion is recorded (${record.kind}: ${record.detail}, line ${record.line}). ${note}; ${phaseNote}. "absent" here has two meanings the recorded verdict cannot tell apart \u2014 no provider in this deployment, or a provider that registers later in this same boot \u2014 and nothing retracts the record when the second one turns out to be the case (#4771 / #4772).`,
1233
+ hint: STARTUP_VERDICT_HINT
1234
+ });
1235
+ for (const rec of records) {
1236
+ if (rec.kind !== "announced") continue;
1237
+ const text = literalText(t, rec.node);
1238
+ if (!text) continue;
1239
+ if (HEDGE_PHRASES.some((h) => text.includes(h))) continue;
1240
+ const hit = ASSERTIVE_PHRASES.find((p) => text.includes(p));
1241
+ if (!hit) continue;
1242
+ findings.push({
1243
+ severity: "warning",
1244
+ rule: STARTUP_VERDICT_ASSERTIVE_WORDING,
1245
+ where,
1246
+ path: `${fileLabel}:${rec.line}`,
1247
+ message: `the diagnostic says "${hit}" about a vocabulary that can still grow during this boot. #4771 printed "will fail at execution time" for eight approval flows 0.8s before the executor that runs them was registered, and a deployment that genuinely lacked the plugin printed the identical eight \u2014 so the line could not tell an operator which of the two they had. #4772's remedy ("you need Redis") sent operators to fix a problem they did not have, and connecting Redis did not change the message.`,
1248
+ hint: STARTUP_VERDICT_HINT
1249
+ });
1250
+ }
1251
+ }
1252
+ }
1253
+ return findings;
1254
+ }
1255
+
862
1256
  // src/validate-list-view-mode.ts
863
1257
  var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
864
1258
  function asArray3(v) {
@@ -1002,6 +1396,7 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
1002
1396
  var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
1003
1397
  var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
1004
1398
  var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
1399
+ var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
1005
1400
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
1006
1401
  function asArray4(v) {
1007
1402
  if (Array.isArray(v)) return v;
@@ -1019,6 +1414,11 @@ function renderNonObject(v) {
1019
1414
  if (t === "bigint") return `${String(v)}n (a bigint)`;
1020
1415
  return `a ${t}`;
1021
1416
  }
1417
+ function renderTriggerToken(v) {
1418
+ if (typeof v === "string") return `'${v}'`;
1419
+ const json = JSON.stringify(v);
1420
+ return json === void 0 ? `a ${typeof v}` : json;
1421
+ }
1022
1422
  function startNodeOf(flow) {
1023
1423
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
1024
1424
  const index = nodes.findIndex((n) => n?.type === "start");
@@ -1138,6 +1538,25 @@ function validateFlowTriggerReadiness(stack) {
1138
1538
  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
1539
  });
1140
1540
  }
1541
+ const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
1542
+ if (start && flow.type === "record_change" && !routesToSomeTrigger) {
1543
+ const hasTriggerType = config.triggerType != null;
1544
+ findings.push({
1545
+ // `error` (#5762's criterion, applied to a fourth id). The verdict is
1546
+ // the engine's own routing chain — literal `startsWith`/`typeof` tests
1547
+ // with no registry lookup in them — so no installed package can make
1548
+ // this token resolve. `registerTrigger` is keyed by the RESOLVED type,
1549
+ // which is the near-miss worth stating: a plugin can supply the
1550
+ // record-change trigger itself, and it still would not help, because
1551
+ // the flow never reaches the point of asking for one.
1552
+ severity: "error",
1553
+ rule: FLOW_TRIGGER_UNROUTABLE,
1554
+ where: `flow "${flowName}" \u203A start node`,
1555
+ path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
1556
+ message: `declares type: 'record_change' but ` + (hasTriggerType ? `its start node's triggerType is ${renderTriggerToken(config.triggerType)}, which the engine routes to NO trigger` : `its start node has no triggerType at all, so there is nothing for the engine to route`) + ` \u2014 it binds a record-change flow only for a token starting with 'record-', so this flow is demoted to a manual one and never fires. Nothing NAMES it: the unbound-flow audit resolves the same binding and skips the flow as "manual \u2014 nothing to bind", so neither the boot warning nor the startup summary lists it; the only trace is the banner's flow count being one higher than its bound count.`,
1557
+ hint: `Use record-{before,after}-{create,update,delete,write} ('write' is create OR update in one flow, #3427; create/insert are synonyms). If the flow really is launched by hand or from a screen, declare type: 'autolaunched' or 'screen' instead of 'record_change' \u2014 those types have no trigger to be missing.`
1558
+ });
1559
+ }
1141
1560
  if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
1142
1561
  findings.push({
1143
1562
  severity: "warning",
@@ -1907,13 +2326,13 @@ function validateJsxPages(stack, opts = {}) {
1907
2326
  }
1908
2327
 
1909
2328
  // src/validate-react-pages.ts
1910
- import { createRequire } from "module";
2329
+ import { createRequire as createRequire2 } from "module";
1911
2330
  var cachedTransform = null;
1912
2331
  function loadSucraseTransform() {
1913
2332
  if (cachedTransform) return cachedTransform;
1914
2333
  const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
1915
2334
  try {
1916
- cachedTransform = createRequire(anchor)("sucrase").transform;
2335
+ cachedTransform = createRequire2(anchor)("sucrase").transform;
1917
2336
  } catch (err) {
1918
2337
  throw new Error(
1919
2338
  `@objectstack/lint: validating a kind:'react' page requires the "sucrase" 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 "sucrase" in the image; it is only loaded when a react-source page is validated.`
@@ -1960,7 +2379,7 @@ function validateReactPages(stack) {
1960
2379
  }
1961
2380
 
1962
2381
  // src/validate-react-page-props.ts
1963
- import { createRequire as createRequire2 } from "module";
2382
+ import { createRequire as createRequire3 } from "module";
1964
2383
  import {
1965
2384
  REACT_BLOCKS,
1966
2385
  RECORD_CONTEXT_BLOCK_TAGS,
@@ -1975,6 +2394,7 @@ import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
1975
2394
  // src/validate-searchable-fields.ts
1976
2395
  import {
1977
2396
  resolveSearchFieldResolution,
2397
+ isVirtualSearchField,
1978
2398
  SEARCHABLE_TEXTUAL_TYPES,
1979
2399
  SEARCHABLE_ENUM_TYPES,
1980
2400
  SEARCH_AUTO_EXCLUDED_FIELDS
@@ -2092,7 +2512,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2092
2512
  where,
2093
2513
  path: `${path}[${i}]`,
2094
2514
  message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest2(name, known)),
2095
- hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a formula field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
2515
+ hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a stored text field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
2516
+ });
2517
+ continue;
2518
+ }
2519
+ if (isVirtualSearchField(target.fields[name])) {
2520
+ const vtype = target.fields[name]?.type;
2521
+ findings.push({
2522
+ severity: "error",
2523
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
2524
+ where,
2525
+ path: `${path}[${i}]`,
2526
+ message: `${subject} entry "${name}" on object "${objectName}" is a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materializes a column for 'search' to scan and the entry can never match. It reads as search coverage and delivers none \u2014 the runtime used to admit it verbatim because the declaration named it (#6674).`,
2527
+ hint: `Mirror the computed value onto a stored text field on "${objectName}" and declare that instead, or drop "${name}". At runtime the ingress gate now refuses this entry with 400 INVALID_FIELD, the same answer a stale entry gets (#4254).`
2096
2528
  });
2097
2529
  continue;
2098
2530
  }
@@ -2127,7 +2559,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2127
2559
  where,
2128
2560
  path: `${path}[${i}]`,
2129
2561
  message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
2130
- hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text/formula field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
2562
+ hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
2131
2563
  });
2132
2564
  }
2133
2565
  return findings;
@@ -2327,8 +2759,21 @@ var COMPONENT_FIELD_SPECS = {
2327
2759
  "element:number": { props: ["field"] },
2328
2760
  "element:filter": { props: ["fields"] },
2329
2761
  "element:form": { props: ["fields"] },
2330
- // The schema says `displayField`; real pages author `labelField`. Accept both.
2331
- "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
2762
+ // `labelField` is the one field-bearing prop this element declares. Its former
2763
+ // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
2764
+ // `searchFields` (deleted, ADR-0049) were retired in #5775 and are
2765
+ // `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
2766
+ // spec-conformant page carries either, and this rule's job (resolve a field
2767
+ // NAME against the object) is not the question a retired key raises (#6629).
2768
+ //
2769
+ // A non-conformant page that writes one anyway is not left unattended: the
2770
+ // #5068 props gate reports the key with its rename/delete prescription. That
2771
+ // gate is advisory and CLI-only and lives in a different registry
2772
+ // (`authoring-rules`) from this suite, so it neither precedes nor suppresses
2773
+ // this rule — what these two entries actually added was a SECOND finding,
2774
+ // saying a field named by a key that no longer exists does not exist either.
2775
+ // The prescription is the useful half; this half was noise on top of it.
2776
+ "element:record_picker": { props: ["labelField"] }
2332
2777
  };
2333
2778
  var RELATED_LIST_TYPE = "record:related_list";
2334
2779
  function componentFieldRefs(type, props, basePath, sep = ".") {
@@ -2488,18 +2933,18 @@ function describeIssue(issue, root, depth = 0) {
2488
2933
  }
2489
2934
 
2490
2935
  // src/validate-react-page-props.ts
2491
- var cachedTs = null;
2492
- function loadTypeScript() {
2493
- if (cachedTs) return cachedTs;
2936
+ var cachedTs2 = null;
2937
+ function loadTypeScript2() {
2938
+ if (cachedTs2) return cachedTs2;
2494
2939
  const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
2495
2940
  try {
2496
- cachedTs = createRequire2(anchor)("typescript");
2941
+ cachedTs2 = createRequire3(anchor)("typescript");
2497
2942
  } catch (err) {
2498
2943
  throw new Error(
2499
2944
  `@objectstack/lint: validating a kind:'react' page requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a react-source page is validated.`
2500
2945
  );
2501
2946
  }
2502
- return cachedTs;
2947
+ return cachedTs2;
2503
2948
  }
2504
2949
  var asArray12 = (v) => Array.isArray(v) ? v : [];
2505
2950
  var BLOCKS = new Map(
@@ -2632,7 +3077,7 @@ function checkChartAggregate(raw, push2) {
2632
3077
  "warning",
2633
3078
  REACT_CHART_AGGREGATE_INVALID,
2634
3079
  "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."
3080
+ "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
2636
3081
  );
2637
3082
  }
2638
3083
  const parsed = ChartAggregateSchema.safeParse(raw);
@@ -2892,7 +3337,7 @@ function validateReactPageProps(stack) {
2892
3337
  const source = page.source;
2893
3338
  if (typeof source !== "string" || source.trim() === "") continue;
2894
3339
  const name = String(page.name ?? `#${p}`);
2895
- const tsc = loadTypeScript();
3340
+ const tsc = loadTypeScript2();
2896
3341
  let sf;
2897
3342
  try {
2898
3343
  sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
@@ -3150,7 +3595,7 @@ function validateSemanticRoles(stack) {
3150
3595
  (h) => typeof h === "string" && h.length > 0
3151
3596
  );
3152
3597
  if (declaredStrings.length > 0 && declaredGroups.size > 0) {
3153
- const declaredTitle = [obj.nameField, obj.primaryField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
3598
+ const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
3154
3599
  const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
3155
3600
  const stripSet = new Set(
3156
3601
  declaredStrings.filter((h) => h !== titleField).slice(0, 4)
@@ -3175,6 +3620,60 @@ function validateSemanticRoles(stack) {
3175
3620
  return findings;
3176
3621
  }
3177
3622
 
3623
+ // src/collection-entries.ts
3624
+ function isRec8(v) {
3625
+ return !!v && typeof v === "object" && !Array.isArray(v);
3626
+ }
3627
+ function collectionEntries(v, base) {
3628
+ if (Array.isArray(v)) {
3629
+ const out = [];
3630
+ for (let i = 0; i < v.length; i++) {
3631
+ if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3632
+ }
3633
+ return out;
3634
+ }
3635
+ if (isRec8(v)) {
3636
+ return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3637
+ }
3638
+ return [];
3639
+ }
3640
+
3641
+ // src/view-walk.ts
3642
+ function isRec9(v) {
3643
+ return !!v && typeof v === "object" && !Array.isArray(v);
3644
+ }
3645
+ function strName5(v) {
3646
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3647
+ }
3648
+ function viewObjectName(view) {
3649
+ return strName5(view.objectName) ?? strName5(view.object) ?? (isRec9(view.data) ? strName5(view.data.object) : void 0);
3650
+ }
3651
+ function viewContainerSites(view, basePath) {
3652
+ if (!isRec9(view)) return [];
3653
+ const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3654
+ if (isRec9(view.form)) {
3655
+ sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3656
+ }
3657
+ for (const key of ["listViews", "formViews"]) {
3658
+ const container = view[key];
3659
+ if (!isRec9(container)) continue;
3660
+ const kind = key === "listViews" ? "listView" : "formView";
3661
+ for (const [subKey, sub] of Object.entries(container)) {
3662
+ if (!isRec9(sub)) continue;
3663
+ sites.push({
3664
+ view: sub,
3665
+ path: `${basePath}.${key}.${subKey}`,
3666
+ surface: `${key}.${subKey}`,
3667
+ kind
3668
+ });
3669
+ }
3670
+ }
3671
+ return sites;
3672
+ }
3673
+ function formViewSites(view, basePath) {
3674
+ return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
3675
+ }
3676
+
3178
3677
  // src/validate-form-layout.ts
3179
3678
  var FORM_FIELD_UNKNOWN = "form-field-unknown";
3180
3679
  var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
@@ -3185,6 +3684,12 @@ function asArray16(v) {
3185
3684
  }
3186
3685
  return [];
3187
3686
  }
3687
+ function isRec10(v) {
3688
+ return !!v && typeof v === "object" && !Array.isArray(v);
3689
+ }
3690
+ function strName6(v) {
3691
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3692
+ }
3188
3693
  function fieldNameOf(entry) {
3189
3694
  if (typeof entry === "string") return entry.length > 0 ? entry : null;
3190
3695
  if (entry && typeof entry === "object" && !Array.isArray(entry)) {
@@ -3193,13 +3698,6 @@ function fieldNameOf(entry) {
3193
3698
  }
3194
3699
  return null;
3195
3700
  }
3196
- function boundObject(view) {
3197
- const data = view.data;
3198
- if (data && typeof data === "object" && typeof data.object === "string") {
3199
- return data.object;
3200
- }
3201
- return typeof view.objectName === "string" ? view.objectName : void 0;
3202
- }
3203
3701
  function validateFormLayout(stack) {
3204
3702
  const findings = [];
3205
3703
  const objectFields = /* @__PURE__ */ new Map();
@@ -3209,44 +3707,44 @@ function validateFormLayout(stack) {
3209
3707
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
3210
3708
  objectFields.set(name, new Set(fields));
3211
3709
  }
3212
- const views = asArray16(stack.views);
3213
- for (let i = 0; i < views.length; i++) {
3214
- const view = views[i];
3215
- if (!view || typeof view !== "object") continue;
3216
- const sections = Array.isArray(view.sections) ? view.sections : null;
3217
- if (!sections) continue;
3218
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
3219
- const objName = boundObject(view);
3220
- const known = objName ? objectFields.get(objName) : void 0;
3221
- const where = `view "${viewName}"`;
3222
- const base = `views[${i}]`;
3223
- for (let s = 0; s < sections.length; s++) {
3224
- const sec = sections[s];
3225
- const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
3226
- for (let f = 0; f < secFields.length; f++) {
3227
- const entry = secFields[f];
3228
- const fname = fieldNameOf(entry);
3229
- const fpath = `${base}.sections[${s}].fields[${f}]`;
3230
- if (fname && known && !known.has(fname)) {
3231
- findings.push({
3232
- severity: "warning",
3233
- rule: FORM_FIELD_UNKNOWN,
3234
- where,
3235
- path: fpath,
3236
- message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
3237
- hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
3238
- });
3239
- }
3240
- const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
3241
- if (colSpan != null) {
3242
- findings.push({
3243
- severity: "warning",
3244
- rule: FORM_COLSPAN_ABSOLUTE,
3245
- where,
3246
- path: `${fpath}.colSpan`,
3247
- message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
3248
- hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
3249
- });
3710
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
3711
+ const viewName = strName6(view.name) ?? strName6(view.object) ?? viewPath;
3712
+ const containerObject = viewObjectName(view);
3713
+ for (const site of formViewSites(view, viewPath)) {
3714
+ const objName = viewObjectName(site.view) ?? containerObject;
3715
+ const known = objName ? objectFields.get(objName) : void 0;
3716
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
3717
+ for (const bucket of ["sections", "groups"]) {
3718
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
3719
+ for (let s = 0; s < sections.length; s++) {
3720
+ const sec = sections[s];
3721
+ const secFields = isRec10(sec) && Array.isArray(sec.fields) ? sec.fields : [];
3722
+ for (let f = 0; f < secFields.length; f++) {
3723
+ const entry = secFields[f];
3724
+ const fname = fieldNameOf(entry);
3725
+ const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
3726
+ if (fname && known && !known.has(fname)) {
3727
+ findings.push({
3728
+ severity: "warning",
3729
+ rule: FORM_FIELD_UNKNOWN,
3730
+ where,
3731
+ path: fpath,
3732
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
3733
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
3734
+ });
3735
+ }
3736
+ const colSpan = isRec10(entry) ? entry.colSpan : void 0;
3737
+ if (colSpan != null) {
3738
+ findings.push({
3739
+ severity: "warning",
3740
+ rule: FORM_COLSPAN_ABSOLUTE,
3741
+ where,
3742
+ path: `${fpath}.colSpan`,
3743
+ message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
3744
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
3745
+ });
3746
+ }
3747
+ }
3250
3748
  }
3251
3749
  }
3252
3750
  }
@@ -3255,17 +3753,17 @@ function validateFormLayout(stack) {
3255
3753
  }
3256
3754
 
3257
3755
  // src/validate-visibility-predicates.ts
3258
- var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
3756
+ import {
3757
+ collectCelRootIdentifiers as collectCelRootIdentifiers2,
3758
+ firstUndeclaredReference,
3759
+ parseCelToAst as parseCelToAst2,
3760
+ parseCelToAstWithReason
3761
+ } from "@objectstack/formula";
3259
3762
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
3763
+ var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
3764
+ var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
3765
+ var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
3260
3766
  var CANONICAL = "visibleWhen";
3261
- var ALIASES = ["visibleOn", "visibility"];
3262
- function asArray17(v) {
3263
- if (Array.isArray(v)) return v;
3264
- if (v && typeof v === "object") {
3265
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3266
- }
3267
- return [];
3268
- }
3269
3767
  function predicateSource(v) {
3270
3768
  if (typeof v === "string") return v;
3271
3769
  if (v && typeof v === "object" && typeof v.source === "string") {
@@ -3276,6 +3774,67 @@ function predicateSource(v) {
3276
3774
  function usesRoot(source, root) {
3277
3775
  return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
3278
3776
  }
3777
+ function withoutStringLiterals(source) {
3778
+ return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
3779
+ }
3780
+ var NON_CEL_SPELLINGS = [
3781
+ { wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
3782
+ { wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
3783
+ { wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
3784
+ { wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
3785
+ { wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
3786
+ { wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
3787
+ // Assignment where a comparison was meant. Last, and fenced off from every
3788
+ // operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
3789
+ { wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
3790
+ ];
3791
+ function quoteSource(source) {
3792
+ const flat = source.replace(/\s+/g, " ").trim();
3793
+ return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
3794
+ }
3795
+ function celRefusal(source) {
3796
+ if (!source.trim()) return null;
3797
+ const parsed = parseCelToAstWithReason(source);
3798
+ if (parsed.ok || parsed.kind === "empty") return null;
3799
+ if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
3800
+ const identifiers = collectCelRootIdentifiers2(source);
3801
+ const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
3802
+ const scannable = withoutStringLiterals(source);
3803
+ return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
3804
+ }
3805
+ function boundName(overrun) {
3806
+ return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
3807
+ }
3808
+ var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
3809
+ function isNode2(v) {
3810
+ return !!v && typeof v === "object" && typeof v.op === "string";
3811
+ }
3812
+ function namespaceRoots(node, out) {
3813
+ if (Array.isArray(node)) {
3814
+ for (const child of node) namespaceRoots(child, out);
3815
+ return;
3816
+ }
3817
+ if (!isNode2(node)) return;
3818
+ const args = node.args;
3819
+ if (Array.isArray(args)) {
3820
+ const receiver = node.op === "rcall" ? args[1] : args[0];
3821
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
3822
+ out.add(receiver.args);
3823
+ }
3824
+ }
3825
+ namespaceRoots(args, out);
3826
+ }
3827
+ function firstBareIdentifier(source) {
3828
+ const ast = parseCelToAst2(source);
3829
+ if (!ast) return null;
3830
+ const rooted = /* @__PURE__ */ new Set();
3831
+ namespaceRoots(ast, rooted);
3832
+ return firstUndeclaredReference(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
3833
+ }
3834
+ var CANONICAL_ROOT_BY_LAYER = {
3835
+ runtime: "record",
3836
+ metadata: "data"
3837
+ };
3279
3838
  var MISLAYER_BY_LAYER = {
3280
3839
  runtime: {
3281
3840
  forbiddenRoot: "data",
@@ -3289,18 +3848,6 @@ var MISLAYER_BY_LAYER = {
3289
3848
  }
3290
3849
  };
3291
3850
  function checkElement(el, where, path, layer, findings) {
3292
- for (const alias of ALIASES) {
3293
- if (el[alias] !== void 0) {
3294
- findings.push({
3295
- severity: "warning",
3296
- rule: VISIBILITY_ALIAS_DEPRECATED,
3297
- where,
3298
- path: `${path}.${alias}`,
3299
- message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
3300
- hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
3301
- });
3302
- }
3303
- }
3304
3851
  const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
3305
3852
  const source = predicateSource(raw);
3306
3853
  const rule = MISLAYER_BY_LAYER[layer];
@@ -3314,6 +3861,43 @@ function checkElement(el, where, path, layer, findings) {
3314
3861
  hint: rule.hint
3315
3862
  });
3316
3863
  }
3864
+ const refusal = source ? celRefusal(source) : null;
3865
+ if (source && refusal?.kind === "bounds") {
3866
+ const bound = boundName(refusal.overrun);
3867
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
3868
+ findings.push({
3869
+ severity: "error",
3870
+ rule: VISIBILITY_PREDICATE_OVER_BUDGET,
3871
+ where,
3872
+ path,
3873
+ message: `visibility predicate is syntactically valid CEL but overruns ${bound} (${refusal.overrun.summary}) (predicate: \`${quoteSource(source)}\`). The canonical front end refuses it, so it can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
3874
+ hint: `There is no syntax or dialect error to correct here \u2014 this is a SIZE fault, not a dialect mistake, so re-spelling the predicate will not fix it. Make it smaller, or move the work off the predicate: (1) collapse a long \`${root}.f == 'a' || ${root}.f == 'b' || \u2026\` chain into a single \`${root}.f in ['a', 'b', \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2); (2) precompute the heavy part into a formula/rollup field on the object and test that one field instead. Logic genuinely this large is not element visibility \u2014 compute it once on the record rather than re-deriving it in every predicate that needs it.`
3875
+ });
3876
+ }
3877
+ if (source && refusal?.kind === "syntax") {
3878
+ findings.push({
3879
+ severity: "error",
3880
+ rule: VISIBILITY_PREDICATE_SYNTAX,
3881
+ where,
3882
+ path,
3883
+ message: `visibility predicate is not valid CEL \u2014 ${refusal.detail} (predicate: \`${quoteSource(source)}\`). A predicate that does not parse can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
3884
+ hint: refusal.token ? `\`${refusal.token.wrote}\` is not a CEL operator \u2014 CEL spells it \`${refusal.token.cel}\`. Replace \`${refusal.token.wrote}\` with \`${refusal.token.cel}\`, e.g. \`${refusal.token.example}\`.` : `Visibility predicates are bare CEL, e.g. \`record.status == 'open'\`. Spellings from other languages do not parse: write \`==\` (not \`===\`), \`!=\` (not \`!==\` or \`<>\`), \`&&\` (not \`and\`), \`||\` (not \`or\`), \`!\` (not \`not\`).`
3885
+ });
3886
+ }
3887
+ if (source && !refusal) {
3888
+ const bare = firstBareIdentifier(source);
3889
+ if (bare) {
3890
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
3891
+ findings.push({
3892
+ severity: "error",
3893
+ rule: VISIBILITY_BARE_IDENTIFIER,
3894
+ where,
3895
+ path,
3896
+ message: `visibility predicate references \`${bare}\` as a bare identifier. Values are bound under a namespace on this surface \u2014 they are never flattened to top level \u2014 so \`${bare}\` resolves to nothing, the predicate can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
3897
+ hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a `*.form.ts` metadata-editing form binds the row under edit as `data`).")
3898
+ });
3899
+ }
3900
+ }
3317
3901
  }
3318
3902
  function isFieldObject(entry) {
3319
3903
  return !!entry && typeof entry === "object" && !Array.isArray(entry);
@@ -3321,43 +3905,304 @@ function isFieldObject(entry) {
3321
3905
  function validateVisibilityPredicates(stack, opts = {}) {
3322
3906
  const layer = opts.layer ?? "runtime";
3323
3907
  const findings = [];
3324
- const views = asArray17(stack.views);
3325
- for (let i = 0; i < views.length; i++) {
3326
- const view = views[i];
3327
- if (!view || typeof view !== "object") continue;
3328
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
3329
- const where = `view "${viewName}"`;
3330
- for (const bucket of ["sections", "groups"]) {
3331
- const sections = Array.isArray(view[bucket]) ? view[bucket] : [];
3332
- for (let s = 0; s < sections.length; s++) {
3333
- const sec = sections[s];
3334
- if (!sec || typeof sec !== "object") continue;
3335
- const secPath = `views[${i}].${bucket}[${s}]`;
3336
- checkElement(sec, where, secPath, layer, findings);
3337
- const secFields = Array.isArray(sec.fields) ? sec.fields : [];
3338
- for (let f = 0; f < secFields.length; f++) {
3339
- const entry = secFields[f];
3340
- if (isFieldObject(entry)) {
3341
- checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
3908
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
3909
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
3910
+ for (const site of formViewSites(view, viewPath)) {
3911
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
3912
+ for (const bucket of ["sections", "groups"]) {
3913
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
3914
+ for (let s = 0; s < sections.length; s++) {
3915
+ const sec = sections[s];
3916
+ if (!sec || typeof sec !== "object") continue;
3917
+ const secPath = `${site.path}.${bucket}[${s}]`;
3918
+ checkElement(sec, where, secPath, layer, findings);
3919
+ const secFields = Array.isArray(sec.fields) ? sec.fields : [];
3920
+ for (let f = 0; f < secFields.length; f++) {
3921
+ const entry = secFields[f];
3922
+ if (isFieldObject(entry)) {
3923
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
3924
+ }
3342
3925
  }
3343
3926
  }
3344
3927
  }
3345
3928
  }
3346
3929
  }
3347
- const pages = asArray17(stack.pages);
3348
- for (let i = 0; i < pages.length; i++) {
3349
- const page = pages[i];
3350
- if (!page || typeof page !== "object") continue;
3351
- const pageName = typeof page.name === "string" ? page.name : `(page ${i})`;
3352
- const where = `page "${pageName}"`;
3353
- const regions = Array.isArray(page.regions) ? page.regions : [];
3354
- for (let r = 0; r < regions.length; r++) {
3355
- const region = regions[r];
3356
- const components = region && typeof region === "object" && Array.isArray(region.components) ? region.components : [];
3357
- for (let c = 0; c < components.length; c++) {
3358
- const comp = components[c];
3359
- if (comp && typeof comp === "object") {
3360
- checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
3930
+ for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3931
+ const pageName = typeof page.name === "string" ? page.name : void 0;
3932
+ const where = `page "${pageName ?? pagePath}"`;
3933
+ for (const walked of walkPageComponents(page, pagePath)) {
3934
+ checkElement(walked.component, where, walked.path, layer, findings);
3935
+ }
3936
+ }
3937
+ return findings;
3938
+ }
3939
+
3940
+ // src/validate-predicate-path-refs.ts
3941
+ import { parseCelToAst as parseCelToAst3 } from "@objectstack/formula";
3942
+ import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
3943
+ import { findClosestMatches, formatSuggestion } from "@objectstack/spec";
3944
+ var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
3945
+ var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
3946
+ var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
3947
+ var ROOT = "data";
3948
+ var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
3949
+ function defOf(schema) {
3950
+ if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
3951
+ const s = schema;
3952
+ return s.def ?? s._def;
3953
+ }
3954
+ function peel(schema, depth = 0) {
3955
+ if (!schema || depth > 25) return schema;
3956
+ const d = defOf(schema);
3957
+ if (!d) return schema;
3958
+ switch (d.type) {
3959
+ case "optional":
3960
+ case "nullable":
3961
+ case "default":
3962
+ case "prefault":
3963
+ case "readonly":
3964
+ case "catch":
3965
+ case "nonoptional":
3966
+ return peel(d.innerType, depth + 1);
3967
+ case "lazy":
3968
+ return peel(d.getter(), depth + 1);
3969
+ case "pipe": {
3970
+ const inner = peel(d.in, depth + 1);
3971
+ return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
3972
+ }
3973
+ default:
3974
+ return schema;
3975
+ }
3976
+ }
3977
+ function optionsOf(d) {
3978
+ return Array.isArray(d?.options) ? d.options : [];
3979
+ }
3980
+ function keysOf(schema, depth = 0) {
3981
+ if (depth > 25) return null;
3982
+ const u = peel(schema);
3983
+ const d = defOf(u);
3984
+ if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
3985
+ if (d?.type === "union" || d?.type === "discriminated_union") {
3986
+ const all = /* @__PURE__ */ new Set();
3987
+ let keyBearing = false;
3988
+ for (const option of optionsOf(d)) {
3989
+ const k = keysOf(option, depth + 1);
3990
+ if (!k) continue;
3991
+ keyBearing = true;
3992
+ for (const key of k) all.add(key);
3993
+ }
3994
+ return keyBearing ? [...all] : null;
3995
+ }
3996
+ if (d?.type === "intersection") {
3997
+ const left = keysOf(d.left, depth + 1);
3998
+ const right = keysOf(d.right, depth + 1);
3999
+ if (!left && !right) return null;
4000
+ return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
4001
+ }
4002
+ return null;
4003
+ }
4004
+ function propertyOf(schema, key, depth = 0) {
4005
+ if (depth > 25) return void 0;
4006
+ const u = peel(schema);
4007
+ const d = defOf(u);
4008
+ if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
4009
+ if (d?.type === "union" || d?.type === "discriminated_union") {
4010
+ for (const option of optionsOf(d)) {
4011
+ const found = propertyOf(option, key, depth + 1);
4012
+ if (found !== void 0) return found;
4013
+ }
4014
+ }
4015
+ if (d?.type === "intersection") {
4016
+ return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
4017
+ }
4018
+ return void 0;
4019
+ }
4020
+ function rowScopeOf(scope, key) {
4021
+ const prop = propertyOf(scope, key);
4022
+ if (prop === void 0) return void 0;
4023
+ let node = peel(prop);
4024
+ for (let i = 0; i < 25; i++) {
4025
+ const d = defOf(node);
4026
+ if (d?.type === "array") node = peel(d.element);
4027
+ else if (d?.type === "record") node = peel(d.valueType);
4028
+ else return node;
4029
+ }
4030
+ return node;
4031
+ }
4032
+ function stepInto(scope, segment) {
4033
+ const u = peel(scope);
4034
+ const d = defOf(u);
4035
+ if (d?.type === "record") return { kind: "declared", next: d.valueType };
4036
+ const declared = keysOf(u);
4037
+ if (declared === null) return { kind: "opaque" };
4038
+ if (!declared.includes(segment)) return { kind: "undeclared", declared };
4039
+ return { kind: "declared", next: propertyOf(u, segment) };
4040
+ }
4041
+ function isNode3(v) {
4042
+ return !!v && typeof v === "object" && typeof v.op === "string";
4043
+ }
4044
+ function memberChain(node) {
4045
+ if (!isNode3(node)) return null;
4046
+ if (node.op === "id" && typeof node.args === "string") return [node.args];
4047
+ if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
4048
+ const head = memberChain(node.args[0]);
4049
+ return head ? [...head, node.args[1]] : null;
4050
+ }
4051
+ return null;
4052
+ }
4053
+ function rootedPaths(node, out) {
4054
+ if (Array.isArray(node)) {
4055
+ for (const child of node) rootedPaths(child, out);
4056
+ return;
4057
+ }
4058
+ if (!isNode3(node)) return;
4059
+ if (node.op === ".") {
4060
+ const chain = memberChain(node);
4061
+ if (chain && chain[0] === ROOT && chain.length > 1) {
4062
+ out.push(chain.slice(1));
4063
+ return;
4064
+ }
4065
+ }
4066
+ rootedPaths(node.args, out);
4067
+ }
4068
+ function classifyIdentifiers(node, values, excluded) {
4069
+ if (Array.isArray(node)) {
4070
+ for (const child of node) classifyIdentifiers(child, values, excluded);
4071
+ return;
4072
+ }
4073
+ if (!isNode3(node)) return;
4074
+ const args = node.args;
4075
+ if (Array.isArray(args)) {
4076
+ const receiver = node.op === "rcall" ? args[1] : args[0];
4077
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
4078
+ excluded.add(receiver.args);
4079
+ }
4080
+ if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
4081
+ const macroArgs = args[2];
4082
+ if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
4083
+ const bound = macroArgs[0];
4084
+ if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
4085
+ excluded.add(bound.args);
4086
+ }
4087
+ }
4088
+ }
4089
+ }
4090
+ if (node.op === "id" && typeof node.args === "string") {
4091
+ values.add(node.args);
4092
+ return;
4093
+ }
4094
+ classifyIdentifiers(args, values, excluded);
4095
+ }
4096
+ function predicateSource2(v) {
4097
+ if (typeof v === "string") return v;
4098
+ if (v && typeof v === "object" && typeof v.source === "string") {
4099
+ return v.source;
4100
+ }
4101
+ return void 0;
4102
+ }
4103
+ function isRec11(v) {
4104
+ return !!v && typeof v === "object" && !Array.isArray(v);
4105
+ }
4106
+ function schemaIdOf(view) {
4107
+ const data = view.data;
4108
+ if (!isRec11(data)) return void 0;
4109
+ if (data.provider !== "schema") return void 0;
4110
+ return typeof data.schemaId === "string" ? data.schemaId : void 0;
4111
+ }
4112
+ function checkPredicate(source, scope, where, path, findings) {
4113
+ const ast = parseCelToAst3(source);
4114
+ if (!ast) return;
4115
+ const paths = [];
4116
+ rootedPaths(ast, paths);
4117
+ for (const segments of paths) {
4118
+ let cursor = scope;
4119
+ const walked = [];
4120
+ for (const segment of segments) {
4121
+ const step = stepInto(cursor, segment);
4122
+ if (step.kind === "opaque") break;
4123
+ if (step.kind === "undeclared") {
4124
+ const full = [ROOT, ...walked, segment].join(".");
4125
+ const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
4126
+ findings.push({
4127
+ severity: "error",
4128
+ rule: PREDICATE_PATH_UNRESOLVED,
4129
+ where,
4130
+ path,
4131
+ message: `predicate references \`${full}\`, which the target schema does not declare \u2014 \`${segment}\` is not a key of \`${container}\`. The reference resolves to nothing, so the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149).`,
4132
+ hint: `${formatSuggestion(findClosestMatches(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
4133
+ });
4134
+ break;
4135
+ }
4136
+ walked.push(segment);
4137
+ cursor = step.next;
4138
+ }
4139
+ }
4140
+ const declaredHere = keysOf(scope);
4141
+ if (!declaredHere) return;
4142
+ const values = /* @__PURE__ */ new Set();
4143
+ const excluded = /* @__PURE__ */ new Set();
4144
+ classifyIdentifiers(ast, values, excluded);
4145
+ for (const id of values) {
4146
+ if (excluded.has(id) || !declaredHere.includes(id)) continue;
4147
+ findings.push({
4148
+ severity: "error",
4149
+ rule: PREDICATE_PATH_UNROOTED,
4150
+ where,
4151
+ path,
4152
+ message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
4153
+ hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
4154
+ });
4155
+ }
4156
+ }
4157
+ function walkFields(entries, scope, where, base, findings, depth) {
4158
+ if (!Array.isArray(entries) || depth > 12) return;
4159
+ for (let i = 0; i < entries.length; i++) {
4160
+ const entry = entries[i];
4161
+ if (!isRec11(entry)) continue;
4162
+ const path = `${base}[${i}]`;
4163
+ for (const key of PREDICATE_KEYS) {
4164
+ const source = predicateSource2(entry[key]);
4165
+ if (source !== void 0 && source.trim()) {
4166
+ checkPredicate(source, scope, where, `${path}.${key}`, findings);
4167
+ break;
4168
+ }
4169
+ }
4170
+ if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
4171
+ const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
4172
+ walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
4173
+ }
4174
+ }
4175
+ }
4176
+ function validatePredicatePathRefs(stack, opts = {}) {
4177
+ const resolveSchema = opts.resolveSchema ?? ((schemaId) => getMetadataTypeSchema(schemaId));
4178
+ const findings = [];
4179
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
4180
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
4181
+ for (const site of formViewSites(view, viewPath)) {
4182
+ const schemaId = schemaIdOf(site.view);
4183
+ if (!schemaId) continue;
4184
+ let root;
4185
+ try {
4186
+ root = resolveSchema(schemaId);
4187
+ } catch {
4188
+ continue;
4189
+ }
4190
+ if (!root) continue;
4191
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
4192
+ for (const bucket of ["sections", "groups"]) {
4193
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
4194
+ for (let s = 0; s < sections.length; s++) {
4195
+ const section = sections[s];
4196
+ if (!isRec11(section)) continue;
4197
+ const sectionPath = `${site.path}.${bucket}[${s}]`;
4198
+ for (const key of PREDICATE_KEYS) {
4199
+ const source = predicateSource2(section[key]);
4200
+ if (source !== void 0 && source.trim()) {
4201
+ checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
4202
+ break;
4203
+ }
4204
+ }
4205
+ walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
3361
4206
  }
3362
4207
  }
3363
4208
  }
@@ -3368,7 +4213,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
3368
4213
  // src/validate-capability-references.ts
3369
4214
  import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
3370
4215
  var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
3371
- function asArray18(v) {
4216
+ function asArray17(v) {
3372
4217
  if (Array.isArray(v)) return v;
3373
4218
  if (v && typeof v === "object") {
3374
4219
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -3393,13 +4238,13 @@ function validateCapabilityReferences(stack) {
3393
4238
  const findings = [];
3394
4239
  if (!stack || typeof stack !== "object") return findings;
3395
4240
  const known = new Set(PLATFORM_CAPABILITY_NAMES);
3396
- for (const cap of asArray18(stack.capabilities)) {
4241
+ for (const cap of asArray17(stack.capabilities)) {
3397
4242
  if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
3398
4243
  }
3399
- for (const ps of asArray18(stack.permissions)) {
4244
+ for (const ps of asArray17(stack.permissions)) {
3400
4245
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
3401
4246
  }
3402
- for (const seed of asArray18(stack.data)) {
4247
+ for (const seed of asArray17(stack.data)) {
3403
4248
  if (seed.object !== "sys_capability") continue;
3404
4249
  for (const rec of Array.isArray(seed.records) ? seed.records : []) {
3405
4250
  const name = rec?.name;
@@ -3418,7 +4263,7 @@ function validateCapabilityReferences(stack) {
3418
4263
  hint
3419
4264
  });
3420
4265
  };
3421
- const objects = asArray18(stack.objects);
4266
+ const objects = asArray17(stack.objects);
3422
4267
  for (let i = 0; i < objects.length; i++) {
3423
4268
  const obj = objects[i];
3424
4269
  if (!obj || typeof obj !== "object") continue;
@@ -3427,27 +4272,27 @@ function validateCapabilityReferences(stack) {
3427
4272
  for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
3428
4273
  flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
3429
4274
  }
3430
- const fields = asArray18(obj.fields);
4275
+ const fields = asArray17(obj.fields);
3431
4276
  for (const f of fields) {
3432
4277
  const fname = typeof f.name === "string" ? f.name : "(field)";
3433
4278
  for (const cap of asCapArray(f.requiredPermissions)) {
3434
4279
  flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
3435
4280
  }
3436
4281
  }
3437
- for (const [ai, action] of asArray18(obj.actions).entries()) {
4282
+ for (const [ai, action] of asArray17(obj.actions).entries()) {
3438
4283
  const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
3439
4284
  for (const cap of asCapArray(action.requiredPermissions)) {
3440
4285
  flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
3441
4286
  }
3442
4287
  }
3443
4288
  }
3444
- for (const [i, action] of asArray18(stack.actions).entries()) {
4289
+ for (const [i, action] of asArray17(stack.actions).entries()) {
3445
4290
  const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
3446
4291
  for (const cap of asCapArray(action.requiredPermissions)) {
3447
4292
  flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
3448
4293
  }
3449
4294
  }
3450
- const apps = asArray18(stack.apps);
4295
+ const apps = asArray17(stack.apps);
3451
4296
  for (let i = 0; i < apps.length; i++) {
3452
4297
  const app = apps[i];
3453
4298
  if (!app || typeof app !== "object") continue;
@@ -3484,7 +4329,7 @@ import {
3484
4329
  normalizeDecisionOutputs
3485
4330
  } from "@objectstack/spec/automation";
3486
4331
  import { BUILTIN_MEMBERSHIP_ROLES } from "@objectstack/spec";
3487
- import { collectCelRootIdentifiers as collectCelRootIdentifiers2 } from "@objectstack/formula";
4332
+ import { collectCelRootIdentifiers as collectCelRootIdentifiers3 } from "@objectstack/formula";
3488
4333
  var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
3489
4334
  var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
3490
4335
  var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
@@ -3504,7 +4349,7 @@ var TYPE_FIX = {
3504
4349
  business_unit: "department",
3505
4350
  bu: "department"
3506
4351
  };
3507
- function asArray19(v) {
4352
+ function asArray18(v) {
3508
4353
  if (Array.isArray(v)) return v;
3509
4354
  if (v && typeof v === "object") {
3510
4355
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -3514,7 +4359,7 @@ function asArray19(v) {
3514
4359
  function validateApprovalApprovers(stack) {
3515
4360
  const findings = [];
3516
4361
  if (!stack || typeof stack !== "object") return findings;
3517
- const flows = asArray19(stack.flows);
4362
+ const flows = asArray18(stack.flows);
3518
4363
  const validTypes = new Set(ApproverType.options);
3519
4364
  for (let fi = 0; fi < flows.length; fi++) {
3520
4365
  const flow = flows[fi];
@@ -3559,7 +4404,7 @@ function validateApprovalApprovers(stack) {
3559
4404
  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.`
3560
4405
  });
3561
4406
  } else {
3562
- const parsed = collectCelRootIdentifiers2(source);
4407
+ const parsed = collectCelRootIdentifiers3(source);
3563
4408
  if (!parsed.ok) {
3564
4409
  findings.push({
3565
4410
  severity: "error",
@@ -3809,7 +4654,7 @@ var OWD_WIDTH = {
3809
4654
  public_read: 1,
3810
4655
  public_read_write: 2
3811
4656
  };
3812
- function asArray20(v) {
4657
+ function asArray19(v) {
3813
4658
  if (Array.isArray(v)) return v;
3814
4659
  if (v && typeof v === "object") {
3815
4660
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -3835,7 +4680,7 @@ function refOf(def) {
3835
4680
  return typeof r === "string" && r ? r : void 0;
3836
4681
  }
3837
4682
  function firstMasterDetailField(obj) {
3838
- for (const f of asArray20(obj.fields)) {
4683
+ for (const f of asArray19(obj.fields)) {
3839
4684
  if (f.type === "master_detail") {
3840
4685
  return { name: String(f.name ?? "?"), parent: refOf(f) };
3841
4686
  }
@@ -3848,8 +4693,8 @@ function grantsObjectAccess(p) {
3848
4693
  function validateSecurityPosture(stack, opts) {
3849
4694
  const findings = [];
3850
4695
  if (!stack || typeof stack !== "object") return findings;
3851
- const objects = asArray20(stack.objects);
3852
- const permissionSets = asArray20(stack.permissions);
4696
+ const objects = asArray19(stack.objects);
4697
+ const permissionSets = asArray19(stack.permissions);
3853
4698
  for (let i = 0; i < objects.length; i++) {
3854
4699
  const obj = objects[i];
3855
4700
  if (!obj || typeof obj !== "object") continue;
@@ -3978,10 +4823,10 @@ function validateSecurityPosture(stack, opts) {
3978
4823
  if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
3979
4824
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
3980
4825
  flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
3981
- for (const f of asArray20(obj.fields)) {
4826
+ for (const f of asArray19(obj.fields)) {
3982
4827
  flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
3983
4828
  }
3984
- for (const [ai, action] of asArray20(obj.actions).entries()) {
4829
+ for (const [ai, action] of asArray19(obj.actions).entries()) {
3985
4830
  flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
3986
4831
  }
3987
4832
  }
@@ -3990,19 +4835,19 @@ function validateSecurityPosture(stack, opts) {
3990
4835
  if (!ps || typeof ps !== "object") continue;
3991
4836
  flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
3992
4837
  }
3993
- for (const [i, pos] of asArray20(stack.positions).entries()) {
4838
+ for (const [i, pos] of asArray19(stack.positions).entries()) {
3994
4839
  flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
3995
4840
  }
3996
- for (const [i, app] of asArray20(stack.apps).entries()) {
4841
+ for (const [i, app] of asArray19(stack.apps).entries()) {
3997
4842
  flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
3998
4843
  }
3999
- for (const [i, book] of asArray20(stack.books).entries()) {
4844
+ for (const [i, book] of asArray19(stack.books).entries()) {
4000
4845
  flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
4001
4846
  }
4002
4847
  const stackSetNames = new Set(
4003
4848
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
4004
4849
  );
4005
- for (const [i, book] of asArray20(stack.books).entries()) {
4850
+ for (const [i, book] of asArray19(stack.books).entries()) {
4006
4851
  const audience = book.audience;
4007
4852
  if (!audience || typeof audience !== "object") continue;
4008
4853
  const setName = audience.permissionSet;
@@ -4080,7 +4925,7 @@ function validateSecurityPosture(stack, opts) {
4080
4925
  }
4081
4926
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
4082
4927
  const nowMs = opts?.nowMs ?? Date.now();
4083
- for (const [i, seed] of asArray20(stack.data).entries()) {
4928
+ for (const [i, seed] of asArray19(stack.data).entries()) {
4084
4929
  const seedObject = typeof seed.object === "string" ? seed.object : "";
4085
4930
  if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
4086
4931
  const records = Array.isArray(seed.records) ? seed.records : [];
@@ -4125,7 +4970,7 @@ var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
4125
4970
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
4126
4971
  var ORG_PARENT_FIELD = "parent_organization_id";
4127
4972
  var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
4128
- function asArray21(v) {
4973
+ function asArray20(v) {
4129
4974
  if (Array.isArray(v)) return v;
4130
4975
  if (v && typeof v === "object") {
4131
4976
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4155,9 +5000,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
4155
5000
  function validateOrgAxisRedLines(stack) {
4156
5001
  const findings = [];
4157
5002
  const cfg = stack ?? {};
4158
- const permissionSets = asArray21(cfg.permissions);
5003
+ const permissionSets = asArray20(cfg.permissions);
4159
5004
  permissionSets.forEach((ps, psIndex) => {
4160
- asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
5005
+ asArray20(ps.rowLevelSecurity).forEach((policy, pIndex) => {
4161
5006
  for (const clause of ["using", "check"]) {
4162
5007
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
4163
5008
  findings.push({
@@ -4171,7 +5016,7 @@ function validateOrgAxisRedLines(stack) {
4171
5016
  }
4172
5017
  });
4173
5018
  });
4174
- asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
5019
+ asArray20(cfg.sharingRules).forEach((rule, rIndex) => {
4175
5020
  const slots = [
4176
5021
  { key: "condition", text: expressionText(rule.condition) },
4177
5022
  { key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
@@ -4189,9 +5034,9 @@ function validateOrgAxisRedLines(stack) {
4189
5034
  }
4190
5035
  });
4191
5036
  const tenancyDisabledObjects = new Set(
4192
- asArray21(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
5037
+ asArray20(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
4193
5038
  );
4194
- asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
5039
+ asArray20(cfg.sharingRules).forEach((rule, rIndex) => {
4195
5040
  const target = str(rule.object);
4196
5041
  if (!target || !tenancyDisabledObjects.has(target)) return;
4197
5042
  const sharedWith = rule.sharedWith;
@@ -4214,7 +5059,7 @@ function validateOrgAxisRedLines(stack) {
4214
5059
  import { compileCelToFilter } from "@objectstack/formula";
4215
5060
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
4216
5061
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
4217
- function asArray22(v) {
5062
+ function asArray21(v) {
4218
5063
  if (Array.isArray(v)) return v;
4219
5064
  if (v && typeof v === "object") {
4220
5065
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4241,7 +5086,7 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
4241
5086
  function validateSharingRuleEnforceability(stack) {
4242
5087
  const findings = [];
4243
5088
  const cfg = stack ?? {};
4244
- asArray22(cfg.sharingRules).forEach((rule, index) => {
5089
+ asArray21(cfg.sharingRules).forEach((rule, index) => {
4245
5090
  const input = toCompilerInput(rule.condition);
4246
5091
  if (input === null) return;
4247
5092
  const result = compileCelToFilter(input, { variables: {} });
@@ -4277,10 +5122,16 @@ function validateSharingRuleEnforceability(stack) {
4277
5122
  }
4278
5123
 
4279
5124
  // src/validate-rls-predicate-enforceability.ts
4280
- import { isPushdownableCel, isSupportedRlsExpression, sqlPredicateToCel } from "@objectstack/formula";
5125
+ import {
5126
+ isPushdownableCel,
5127
+ isSupportedRlsExpression,
5128
+ parseCelToAstWithReason as parseCelToAstWithReason2,
5129
+ sqlPredicateToCel
5130
+ } from "@objectstack/formula";
4281
5131
  var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
4282
5132
  var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
4283
- function asArray23(v) {
5133
+ var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
5134
+ function asArray22(v) {
4284
5135
  if (Array.isArray(v)) return v;
4285
5136
  if (v && typeof v === "object") {
4286
5137
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4291,6 +5142,13 @@ function str3(v) {
4291
5142
  return typeof v === "string" ? v : "";
4292
5143
  }
4293
5144
  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.";
5145
+ function boundsOverrunOf(bridged) {
5146
+ const parsed = parseCelToAstWithReason2(bridged);
5147
+ return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
5148
+ }
5149
+ function quote(source) {
5150
+ return source.length > 200 ? `${source.slice(0, 197)}...` : source;
5151
+ }
4294
5152
  function consequence(clause) {
4295
5153
  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
5154
  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.";
@@ -4298,20 +5156,36 @@ function consequence(clause) {
4298
5156
  function validateRlsPredicateEnforceability(stack) {
4299
5157
  const findings = [];
4300
5158
  const cfg = stack ?? {};
4301
- asArray23(cfg.permissions).forEach((ps, psIndex) => {
4302
- asArray23(ps.rowLevelSecurity).forEach((policy, pIndex) => {
5159
+ asArray22(cfg.permissions).forEach((ps, psIndex) => {
5160
+ asArray22(ps.rowLevelSecurity).forEach((policy, pIndex) => {
4303
5161
  for (const clause of ["using", "check"]) {
4304
5162
  const source = str3(policy[clause]);
4305
5163
  if (!source.trim()) continue;
4306
5164
  if (isSupportedRlsExpression(source)) continue;
4307
- const why = isPushdownableCel(sqlPredicateToCel(source));
5165
+ const bridged = sqlPredicateToCel(source);
5166
+ const why = isPushdownableCel(bridged);
4308
5167
  const detail = why.ok ? "" : why.detail;
4309
5168
  const parseError = !why.ok && why.reason === "parse-error";
5169
+ const overrun = parseError ? boundsOverrunOf(bridged) : null;
4310
5170
  const psName = str3(ps.name) || String(psIndex);
4311
5171
  const policyName = str3(policy.name) || String(pIndex);
4312
5172
  const object = str3(policy.object);
4313
5173
  const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
4314
5174
  const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
5175
+ if (overrun) {
5176
+ const bound = overrun.limit ?? "an unnamed platform CEL bound";
5177
+ const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
5178
+ const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
5179
+ findings.push({
5180
+ severity: "error",
5181
+ rule: RLS_PREDICATE_OVER_BUDGET,
5182
+ where,
5183
+ path,
5184
+ message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
5185
+ hint: `There is no syntax or dialect error to correct here \u2014 the predicate is well-formed CEL and is simply too large for ${bound}${budget}, so the fix is to make it smaller or to move the work off the predicate. (1) Collapse a long \`field == a || field == b || \u2026\` chain into a single \`field in [a, b, \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2). (2) Pre-resolve the set into a membership key the runtime exposes and test \`field in current_user.<key>\` (ADR-0105 D11) \u2014 one comparison whatever the set size. (3) Denormalise a repeated sub-expression onto this object as a formula/rollup field and test that single column. (4) Split a TOP-LEVEL \`||\` across several \`rowLevelSecurity\` policies: applicable policies are OR-ed, so that is equivalent \u2014 but never split a top-level \`&&\` this way, which would WIDEN access rather than preserve it. Logic genuinely this large is not a row filter: move it to a hook or action body (\`ScriptBody { language: 'js' }\`, the L2 sandboxed surface).`
5186
+ });
5187
+ continue;
5188
+ }
4315
5189
  if (parseError) {
4316
5190
  findings.push({
4317
5191
  severity: "error",
@@ -4340,14 +5214,14 @@ function validateRlsPredicateEnforceability(stack) {
4340
5214
  // src/validate-dashboard-action-refs.ts
4341
5215
  var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
4342
5216
  var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
4343
- function asArray24(v) {
5217
+ function asArray23(v) {
4344
5218
  if (Array.isArray(v)) return v;
4345
5219
  if (v && typeof v === "object") {
4346
5220
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4347
5221
  }
4348
5222
  return [];
4349
5223
  }
4350
- function strName5(v) {
5224
+ function strName7(v) {
4351
5225
  return typeof v === "string" && v.length > 0 ? v : void 0;
4352
5226
  }
4353
5227
  var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
@@ -4364,7 +5238,7 @@ var URL_COLLECTION_TO_STACK_KEY = {
4364
5238
  views: "views"
4365
5239
  };
4366
5240
  function viewContainerName(item) {
4367
- return strName5(item.name) ?? strName5(item.id) ?? strName5(item.object) ?? strName5(item.list?.data && item.list.data.object) ?? strName5(item.form?.data && item.form.data.object);
5241
+ return strName7(item.name) ?? strName7(item.id) ?? strName7(item.object) ?? strName7(item.list?.data && item.list.data.object) ?? strName7(item.form?.data && item.form.data.object);
4368
5242
  }
4369
5243
  function collectKnownTargets(stack) {
4370
5244
  const actions = /* @__PURE__ */ new Set();
@@ -4374,22 +5248,22 @@ function collectKnownTargets(stack) {
4374
5248
  const pages = /* @__PURE__ */ new Set();
4375
5249
  const views = /* @__PURE__ */ new Set();
4376
5250
  const collectNames = (v, into, name) => {
4377
- for (const item of asArray24(v)) {
5251
+ for (const item of asArray23(v)) {
4378
5252
  if (!item || typeof item !== "object") continue;
4379
5253
  const n = name(item);
4380
5254
  if (n) into.add(n);
4381
5255
  }
4382
5256
  };
4383
- collectNames(stack.actions, actions, (a) => strName5(a.name));
4384
- for (const obj of asArray24(stack.objects)) {
5257
+ collectNames(stack.actions, actions, (a) => strName7(a.name));
5258
+ for (const obj of asArray23(stack.objects)) {
4385
5259
  if (!obj || typeof obj !== "object") continue;
4386
- const n = strName5(obj.name);
5260
+ const n = strName7(obj.name);
4387
5261
  if (n) objects.add(n);
4388
- collectNames(obj.actions, actions, (a) => strName5(a.name));
5262
+ collectNames(obj.actions, actions, (a) => strName7(a.name));
4389
5263
  }
4390
- collectNames(stack.reports, reports, (r) => strName5(r.name));
4391
- collectNames(stack.dashboards, dashboards, (d) => strName5(d.name));
4392
- collectNames(stack.pages, pages, (p) => strName5(p.name));
5264
+ collectNames(stack.reports, reports, (r) => strName7(r.name));
5265
+ collectNames(stack.dashboards, dashboards, (d) => strName7(d.name));
5266
+ collectNames(stack.pages, pages, (p) => strName7(p.name));
4393
5267
  collectNames(stack.views, views, viewContainerName);
4394
5268
  for (const o of objects) views.add(o);
4395
5269
  return { actions, objects, reports, dashboards, pages, views };
@@ -4421,14 +5295,14 @@ function resolveUrlRoute(target, known) {
4421
5295
  function validateDashboardActionRefs(stack) {
4422
5296
  const findings = [];
4423
5297
  if (!stack || typeof stack !== "object") return findings;
4424
- const dashboards = asArray24(stack.dashboards);
5298
+ const dashboards = asArray23(stack.dashboards);
4425
5299
  if (dashboards.length === 0) return findings;
4426
5300
  const known = collectKnownTargets(stack);
4427
5301
  const checkOne = (action, where, path) => {
4428
- const target = strName5(action.actionUrl);
5302
+ const target = strName7(action.actionUrl);
4429
5303
  if (!target) return;
4430
5304
  if (target.includes("${")) return;
4431
- const actionType = strName5(action.actionType) ?? "url";
5305
+ const actionType = strName7(action.actionType) ?? "url";
4432
5306
  if (actionType === "script" || actionType === "modal") {
4433
5307
  if (resolveActionTarget(actionType, target, known)) return;
4434
5308
  const kindWord = actionType === "script" ? "script" : "modal";
@@ -4459,13 +5333,13 @@ function validateDashboardActionRefs(stack) {
4459
5333
  for (let di = 0; di < dashboards.length; di++) {
4460
5334
  const dash = dashboards[di];
4461
5335
  if (!dash || typeof dash !== "object") continue;
4462
- const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
5336
+ const dashName = strName7(dash.name) ?? `(dashboard ${di})`;
4463
5337
  const dashPath = `dashboards[${di}]`;
4464
- const headerActions = asArray24(dash.header?.actions);
5338
+ const headerActions = asArray23(dash.header?.actions);
4465
5339
  for (let ai = 0; ai < headerActions.length; ai++) {
4466
5340
  const action = headerActions[ai];
4467
5341
  if (!action || typeof action !== "object") continue;
4468
- const label2 = strName5(action.label) ?? strName5(action.actionUrl) ?? `#${ai}`;
5342
+ const label2 = strName7(action.label) ?? strName7(action.actionUrl) ?? `#${ai}`;
4469
5343
  checkOne(
4470
5344
  action,
4471
5345
  `dashboard "${dashName}" \xB7 header action "${label2}"`,
@@ -4478,19 +5352,75 @@ function validateDashboardActionRefs(stack) {
4478
5352
 
4479
5353
  // src/validate-filter-tokens.ts
4480
5354
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
4481
- var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
5355
+
5356
+ // src/filter-walk.ts
4482
5357
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
4483
- function asArray25(v) {
5358
+ function asArray24(v) {
4484
5359
  if (Array.isArray(v)) return v;
4485
5360
  if (v && typeof v === "object") {
4486
5361
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4487
5362
  }
4488
- return [];
4489
- }
4490
- function label(v, fallback) {
4491
- return typeof v === "string" && v.length > 0 ? v : fallback;
5363
+ return [];
5364
+ }
5365
+ function label(v, fallback) {
5366
+ return typeof v === "string" && v.length > 0 ? v : fallback;
5367
+ }
5368
+ function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
5369
+ if (!node || typeof node !== "object") return;
5370
+ if (seen.has(node)) return;
5371
+ seen.add(node);
5372
+ if (Array.isArray(node)) {
5373
+ node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
5374
+ return;
5375
+ }
5376
+ for (const [k, v] of Object.entries(node)) {
5377
+ const childPath = `${path}.${k}`;
5378
+ if (FILTER_KEYS.has(k)) {
5379
+ visit({ value: v, path: childPath, where });
5380
+ continue;
5381
+ }
5382
+ scanForFilters(v, childPath, where, visit, seen);
5383
+ }
5384
+ }
5385
+ function walkAuthoredFilters(stack, surfaces, visit) {
5386
+ if (!stack || typeof stack !== "object") return;
5387
+ for (const { key, kind } of surfaces) {
5388
+ const items = asArray24(stack[key]);
5389
+ items.forEach((item, i) => {
5390
+ const name = label(item.name ?? item.id, `#${i}`);
5391
+ if (kind === "dashboard") {
5392
+ const widgets = Array.isArray(item.widgets) ? item.widgets : [];
5393
+ widgets.forEach((w, wi) => {
5394
+ const wName = label(w.id ?? w.title, `#${wi}`);
5395
+ scanForFilters(
5396
+ w,
5397
+ `${key}[${i}].widgets[${wi}]`,
5398
+ `dashboard "${name}" \xB7 widget "${wName}"`,
5399
+ visit,
5400
+ /* @__PURE__ */ new Set()
5401
+ );
5402
+ });
5403
+ const { widgets: _skip, ...rest } = item;
5404
+ scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
5405
+ return;
5406
+ }
5407
+ scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
5408
+ });
5409
+ }
4492
5410
  }
5411
+
5412
+ // src/validate-filter-tokens.ts
5413
+ var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
4493
5414
  var KNOWN_LIST = CONTEXT_TOKENS.join("}, {");
5415
+ var TOKEN_FILTER_SURFACES = [
5416
+ { key: "dashboards", kind: "dashboard" },
5417
+ { key: "objects", kind: "object" },
5418
+ { key: "views", kind: "view" },
5419
+ { key: "reports", kind: "report" },
5420
+ { key: "datasets", kind: "dataset" },
5421
+ { key: "pages", kind: "page" },
5422
+ { key: "apps", kind: "app" }
5423
+ ];
4494
5424
  function walkFilterValues(node, path, where, out, seen) {
4495
5425
  if (node === null || node === void 0) return;
4496
5426
  if (typeof node === "string") {
@@ -4519,58 +5449,132 @@ function walkFilterValues(node, path, where, out, seen) {
4519
5449
  walkFilterValues(v, `${path}.${k}`, where, out, seen);
4520
5450
  }
4521
5451
  }
4522
- function scanForFilters(node, path, where, out, seen) {
4523
- if (!node || typeof node !== "object") return;
4524
- if (seen.has(node)) return;
4525
- seen.add(node);
4526
- if (Array.isArray(node)) {
4527
- node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, out, seen));
5452
+ function validateFilterTokens(stack) {
5453
+ if (!stack || typeof stack !== "object") return [];
5454
+ const out = [];
5455
+ walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
5456
+ walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
5457
+ });
5458
+ return out;
5459
+ }
5460
+
5461
+ // src/validate-empty-combinators.ts
5462
+ import { reduceFilterVerdict } from "@objectstack/spec/data";
5463
+ var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
5464
+ var FILTER_EMPTY_NODE = "filter-empty-node";
5465
+ var EMPTY_COMBINATOR_SURFACES = [
5466
+ { key: "dashboards", kind: "dashboard" },
5467
+ { key: "objects", kind: "object" },
5468
+ { key: "views", kind: "view" },
5469
+ { key: "reports", kind: "report" },
5470
+ { key: "datasets", kind: "dataset" },
5471
+ { key: "pages", kind: "page" },
5472
+ { key: "apps", kind: "app" },
5473
+ { key: "flows", kind: "flow" }
5474
+ ];
5475
+ function isFilterNode(value) {
5476
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
5477
+ const proto = Object.getPrototypeOf(value);
5478
+ return proto === Object.prototype || proto === null;
5479
+ }
5480
+ var VERDICT_OF = {
5481
+ $and: reduceFilterVerdict({ $and: [] }),
5482
+ $or: reduceFilterVerdict({ $or: [] }),
5483
+ $not: reduceFilterVerdict({ $not: {} }),
5484
+ node: reduceFilterVerdict({}),
5485
+ /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
5486
+ orWithEmptyBranch: reduceFilterVerdict({ $or: [{ status: "open" }, {}] })
5487
+ };
5488
+ function rows(verdict) {
5489
+ if (verdict === "true") return "matches EVERY row";
5490
+ if (verdict === "false") return "matches NO row";
5491
+ return "carries a real predicate";
5492
+ }
5493
+ var MATCH_NONE_SPELLING = "If you really do want a predicate that selects nothing, `{ <field>: { $in: [] } }` is the declared spelling for it (an empty `$in` list matches nothing, on every backend) \u2014 it says so where an empty combinator only implies it.";
5494
+ var OMIT_THE_KEY = 'To express "no filter", DELETE the key \u2014 an absent `filter` and a filter that reduces to TRUE run identically, and only the absent key says so to the next reader (and to the next AI author that copies this metadata).';
5495
+ function emitEmptyCombinator(key, path, ctx) {
5496
+ const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
5497
+ const message = key === "$and" ? `\`$and: []\` is a conjunction of ZERO conditions. Under the #5322 identity ruling it ${rows(VERDICT_OF.$and)} \u2014 the key is authored, and it constrains nothing, so this surface reads as filtered and is not.` : key === "$or" ? `\`$or: []\` is a disjunction of ZERO branches. Under the #5322 identity ruling it ${rows(VERDICT_OF.$or)}: this surface renders permanently empty, and on a read scope it hides every row (fail-closed by design \u2014 #5134).` : `\`$not: {}\` negates an EMPTY node. An empty node is TRUE and NOT TRUE is FALSE, so it ${rows(VERDICT_OF.$not)} \u2014 the opposite of the "no filter" an empty operand looks like.`;
5498
+ const hint = key === "$and" ? `${OMIT_THE_KEY} To express a constraint, put the conditions in the array. ${MATCH_NONE_SPELLING}` : key === "$or" ? `If you meant "no filter", this is its OPPOSITE: emptying the array does not relax the filter, it closes it. ${OMIT_THE_KEY} If you meant to offer alternatives, put the branches in the array. ${MATCH_NONE_SPELLING}` : `Put the condition you are negating inside \`$not\` (\`{ $not: { status: 'closed' } }\`). ${OMIT_THE_KEY} ${MATCH_NONE_SPELLING}`;
5499
+ ctx.out.push({
5500
+ severity: "error",
5501
+ rule: FILTER_EMPTY_COMBINATOR,
5502
+ where: ctx.where,
5503
+ path,
5504
+ message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
5505
+ hint: `${hint} A PROGRAMMATIC producer that loops to zero operands keeps the runtime identity unchanged \u2014 this rule judges only what is written in the metadata.`
5506
+ });
5507
+ }
5508
+ function emitEmptyNode(position, path, ctx) {
5509
+ if (position === "root") {
5510
+ ctx.out.push({
5511
+ severity: "error",
5512
+ rule: FILTER_EMPTY_NODE,
5513
+ where: ctx.where,
5514
+ path,
5515
+ message: `An EMPTY filter node (\`{}\`) is authored here. Under the #5322 identity ruling an empty node is TRUE \u2014 it ${rows(VERDICT_OF.node)}, exactly as if the key were absent \u2014 so a filter is declared and enforces nothing.`,
5516
+ hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
5517
+ });
4528
5518
  return;
4529
5519
  }
4530
- for (const [k, v] of Object.entries(node)) {
4531
- const childPath = `${path}.${k}`;
4532
- if (FILTER_KEYS.has(k)) {
4533
- walkFilterValues(v, childPath, where, out, /* @__PURE__ */ new Set());
5520
+ if (position === "or-branch") {
5521
+ ctx.out.push({
5522
+ severity: "error",
5523
+ rule: FILTER_EMPTY_NODE,
5524
+ where: ctx.where,
5525
+ path,
5526
+ message: `An EMPTY branch (\`{}\`) of a \`$or\`. An empty node is TRUE, and one TRUE disjunct ABSORBS the whole disjunction (\`{ $or: [{ status: 'open' }, {}] }\` ${rows(VERDICT_OF.orWithEmptyBranch)}), so every branch you wrote beside it is dead.`,
5527
+ hint: "Delete the empty branch \u2014 the `$or` then means what it looks like. If it was meant to carry a condition, write it. (A compiler that DROPPED the empty branch instead would silently NARROW the scope to the surviving branches, which is why the runtime absorbs rather than filters \u2014 #5297.)"
5528
+ });
5529
+ return;
5530
+ }
5531
+ ctx.out.push({
5532
+ severity: "error",
5533
+ rule: FILTER_EMPTY_NODE,
5534
+ where: ctx.where,
5535
+ path,
5536
+ message: "An EMPTY branch (`{}`) of a `$and`. An empty node is TRUE \u2014 the AND identity \u2014 so the branch contributes no condition and the conjunction means whatever its other branches mean.",
5537
+ hint: "Delete the empty branch, or write the condition it was meant to carry. A branch that constrains nothing is indistinguishable from one whose condition was lost in an edit."
5538
+ });
5539
+ }
5540
+ function scanNodeKeys(node, path, ctx) {
5541
+ for (const [key, value] of Object.entries(node)) {
5542
+ if (key === "$and" || key === "$or") {
5543
+ if (!Array.isArray(value)) continue;
5544
+ if (value.length === 0) {
5545
+ emitEmptyCombinator(key, `${path}.${key}`, ctx);
5546
+ continue;
5547
+ }
5548
+ value.forEach((element, index) => {
5549
+ scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
5550
+ });
5551
+ continue;
5552
+ }
5553
+ if (key === "$not") {
5554
+ if (!isFilterNode(value)) continue;
5555
+ if (Object.keys(value).length === 0) {
5556
+ emitEmptyCombinator("$not", `${path}.$not`, ctx);
5557
+ continue;
5558
+ }
5559
+ scanNodeKeys(value, `${path}.$not`, ctx);
4534
5560
  continue;
4535
5561
  }
4536
- scanForFilters(v, childPath, where, out, seen);
4537
5562
  }
4538
5563
  }
4539
- function validateFilterTokens(stack) {
5564
+ function scanBranch(value, path, position, ctx) {
5565
+ if (!isFilterNode(value)) return;
5566
+ if (Object.keys(value).length === 0) {
5567
+ emitEmptyNode(position, path, ctx);
5568
+ return;
5569
+ }
5570
+ scanNodeKeys(value, path, ctx);
5571
+ }
5572
+ function validateEmptyCombinators(stack) {
4540
5573
  if (!stack || typeof stack !== "object") return [];
4541
5574
  const out = [];
4542
- const surfaces = [
4543
- ["dashboards", "dashboard"],
4544
- ["objects", "object"],
4545
- ["views", "view"],
4546
- ["reports", "report"],
4547
- ["datasets", "dataset"],
4548
- ["pages", "page"],
4549
- ["apps", "app"]
4550
- ];
4551
- for (const [key, kind] of surfaces) {
4552
- const items = asArray25(stack[key]);
4553
- items.forEach((item, i) => {
4554
- const name = label(item.name ?? item.id, `#${i}`);
4555
- if (kind === "dashboard") {
4556
- const widgets = Array.isArray(item.widgets) ? item.widgets : [];
4557
- widgets.forEach((w, wi) => {
4558
- const wName = label(w.id ?? w.title, `#${wi}`);
4559
- scanForFilters(
4560
- w,
4561
- `${key}[${i}].widgets[${wi}]`,
4562
- `dashboard "${name}" \xB7 widget "${wName}"`,
4563
- out,
4564
- /* @__PURE__ */ new Set()
4565
- );
4566
- });
4567
- const { widgets: _skip, ...rest } = item;
4568
- scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
4569
- return;
4570
- }
4571
- scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
4572
- });
4573
- }
5575
+ walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
5576
+ scanBranch(value, path, "root", { where, out });
5577
+ });
4574
5578
  return out;
4575
5579
  }
4576
5580
 
@@ -4583,14 +5587,14 @@ import {
4583
5587
  var PLATFORM_NAMES = [...PLATFORM_PROVIDED_OBJECT_NAMES];
4584
5588
  var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
4585
5589
  var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
4586
- function asArray26(v) {
5590
+ function asArray25(v) {
4587
5591
  if (Array.isArray(v)) return v;
4588
5592
  if (v && typeof v === "object") {
4589
5593
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4590
5594
  }
4591
5595
  return [];
4592
5596
  }
4593
- function strName6(v) {
5597
+ function strName8(v) {
4594
5598
  return typeof v === "string" && v.length > 0 ? v : void 0;
4595
5599
  }
4596
5600
  function isInterpolated(target) {
@@ -4630,14 +5634,14 @@ function distance2(a, b) {
4630
5634
  function validateObjectReferences(stack) {
4631
5635
  const findings = [];
4632
5636
  if (!stack || typeof stack !== "object") return findings;
4633
- const objects = asArray26(stack.objects);
5637
+ const objects = asArray25(stack.objects);
4634
5638
  const ownObjects = /* @__PURE__ */ new Set();
4635
5639
  for (const obj of objects) {
4636
- const n = strName6(obj.name);
5640
+ const n = strName8(obj.name);
4637
5641
  if (n) ownObjects.add(n);
4638
5642
  }
4639
5643
  const check = (target, where, path, subject, fix) => {
4640
- const name = strName6(target);
5644
+ const name = strName8(target);
4641
5645
  if (!name) return;
4642
5646
  if (isInterpolated(name)) return;
4643
5647
  if (ownObjects.has(name)) return;
@@ -4663,21 +5667,21 @@ function validateObjectReferences(stack) {
4663
5667
  });
4664
5668
  };
4665
5669
  const checkActionParams2 = (action, actionPath, actionLabel) => {
4666
- const params = asArray26(action.params);
5670
+ const params = asArray25(action.params);
4667
5671
  for (let pi = 0; pi < params.length; pi++) {
4668
5672
  const param = params[pi];
4669
5673
  if (!param || typeof param !== "object") continue;
4670
- const paramLabel = strName6(param.name) ?? strName6(param.field) ?? `#${pi}`;
5674
+ const paramLabel = strName8(param.name) ?? strName8(param.field) ?? `#${pi}`;
4671
5675
  const where = `${actionLabel} \xB7 param "${paramLabel}"`;
4672
5676
  check(
4673
- strName6(param.reference),
5677
+ strName8(param.reference),
4674
5678
  where,
4675
5679
  `${actionPath}.params[${pi}].reference`,
4676
5680
  "record-picker target",
4677
5681
  "Without a resolvable target the picker degrades to a raw record-id text input."
4678
5682
  );
4679
5683
  check(
4680
- strName6(param.objectOverride),
5684
+ strName8(param.objectOverride),
4681
5685
  where,
4682
5686
  `${actionPath}.params[${pi}].objectOverride`,
4683
5687
  "field-backed param object",
@@ -4685,70 +5689,70 @@ function validateObjectReferences(stack) {
4685
5689
  );
4686
5690
  }
4687
5691
  };
4688
- const globalActions = asArray26(stack.actions);
5692
+ const globalActions = asArray25(stack.actions);
4689
5693
  for (let ai = 0; ai < globalActions.length; ai++) {
4690
5694
  const action = globalActions[ai];
4691
5695
  if (!action || typeof action !== "object") continue;
4692
- checkActionParams2(action, `actions[${ai}]`, `action "${strName6(action.name) ?? `#${ai}`}"`);
5696
+ checkActionParams2(action, `actions[${ai}]`, `action "${strName8(action.name) ?? `#${ai}`}"`);
4693
5697
  }
4694
5698
  for (let oi = 0; oi < objects.length; oi++) {
4695
5699
  const obj = objects[oi];
4696
5700
  if (!obj || typeof obj !== "object") continue;
4697
- const objName = strName6(obj.name) ?? `#${oi}`;
4698
- const objActions = asArray26(obj.actions);
5701
+ const objName = strName8(obj.name) ?? `#${oi}`;
5702
+ const objActions = asArray25(obj.actions);
4699
5703
  for (let ai = 0; ai < objActions.length; ai++) {
4700
5704
  const action = objActions[ai];
4701
5705
  if (!action || typeof action !== "object") continue;
4702
5706
  checkActionParams2(
4703
5707
  action,
4704
5708
  `objects[${oi}].actions[${ai}]`,
4705
- `object "${objName}" \xB7 action "${strName6(action.name) ?? `#${ai}`}"`
5709
+ `object "${objName}" \xB7 action "${strName8(action.name) ?? `#${ai}`}"`
4706
5710
  );
4707
5711
  }
4708
5712
  }
4709
- const dashboards = asArray26(stack.dashboards);
5713
+ const dashboards = asArray25(stack.dashboards);
4710
5714
  for (let di = 0; di < dashboards.length; di++) {
4711
5715
  const dash = dashboards[di];
4712
5716
  if (!dash || typeof dash !== "object") continue;
4713
- const dashName = strName6(dash.name) ?? `#${di}`;
4714
- const filters = asArray26(dash.globalFilters);
5717
+ const dashName = strName8(dash.name) ?? `#${di}`;
5718
+ const filters = asArray25(dash.globalFilters);
4715
5719
  for (let fi = 0; fi < filters.length; fi++) {
4716
5720
  const filter = filters[fi];
4717
5721
  if (!filter || typeof filter !== "object") continue;
4718
5722
  const optionsFrom = filter.optionsFrom;
4719
5723
  if (!optionsFrom || typeof optionsFrom !== "object") continue;
4720
5724
  check(
4721
- strName6(optionsFrom.object),
4722
- `dashboard "${dashName}" \xB7 filter "${strName6(filter.name) ?? `#${fi}`}"`,
5725
+ strName8(optionsFrom.object),
5726
+ `dashboard "${dashName}" \xB7 filter "${strName8(filter.name) ?? `#${fi}`}"`,
4723
5727
  `dashboards[${di}].globalFilters[${fi}].optionsFrom.object`,
4724
5728
  "filter options source",
4725
5729
  "The dropdown fetches its options from this object; an unknown one renders an always-empty filter."
4726
5730
  );
4727
5731
  }
4728
5732
  }
4729
- const apps = asArray26(stack.apps);
5733
+ const apps = asArray25(stack.apps);
4730
5734
  for (let ai = 0; ai < apps.length; ai++) {
4731
5735
  const app = apps[ai];
4732
5736
  if (!app || typeof app !== "object") continue;
4733
- const appName = strName6(app.name) ?? `#${ai}`;
5737
+ const appName = strName8(app.name) ?? `#${ai}`;
4734
5738
  const walkNav = (items, basePath) => {
4735
- const navItems = asArray26(items);
5739
+ const navItems = asArray25(items);
4736
5740
  for (let ni = 0; ni < navItems.length; ni++) {
4737
5741
  const nav = navItems[ni];
4738
5742
  if (!nav || typeof nav !== "object") continue;
4739
- const navId = strName6(nav.id) ?? `#${ni}`;
5743
+ const navId = strName8(nav.id) ?? `#${ni}`;
4740
5744
  const where = `app "${appName}" \xB7 nav "${navId}"`;
4741
5745
  const navPath = `${basePath}[${ni}]`;
4742
5746
  check(
4743
- strName6(nav.requiresObject),
5747
+ strName8(nav.requiresObject),
4744
5748
  where,
4745
5749
  `${navPath}.requiresObject`,
4746
5750
  "capability gate object",
4747
5751
  "The entry is hidden unless this object is registered, so a typo hides it permanently \u2014 and it suppresses the nav cross-reference check that would have caught the target."
4748
5752
  );
4749
- if (nav.requiresObject && strName6(nav.objectName)) {
5753
+ if (nav.requiresObject && strName8(nav.objectName)) {
4750
5754
  check(
4751
- strName6(nav.objectName),
5755
+ strName8(nav.objectName),
4752
5756
  where,
4753
5757
  `${navPath}.objectName`,
4754
5758
  "navigation target",
@@ -4759,7 +5763,7 @@ function validateObjectReferences(stack) {
4759
5763
  }
4760
5764
  };
4761
5765
  walkNav(app.navigation, `apps[${ai}].navigation`);
4762
- const areas = asArray26(app.areas);
5766
+ const areas = asArray25(app.areas);
4763
5767
  for (let ri = 0; ri < areas.length; ri++) {
4764
5768
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4765
5769
  }
@@ -4769,13 +5773,13 @@ function validateObjectReferences(stack) {
4769
5773
 
4770
5774
  // src/validate-nav-target-refs.ts
4771
5775
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
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 });
5776
+ var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5777
+ function asArray26(v) {
5778
+ if (Array.isArray(v)) return v.filter(isRec12);
5779
+ if (isRec12(v)) return Object.entries(v).map(([name, def]) => isRec12(def) ? { name, ...def } : { name });
4776
5780
  return [];
4777
5781
  }
4778
- function strName7(v) {
5782
+ function strName9(v) {
4779
5783
  return typeof v === "string" && v.length > 0 ? v : void 0;
4780
5784
  }
4781
5785
  var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
@@ -4786,32 +5790,32 @@ var NAV_TARGETS = [
4786
5790
  ];
4787
5791
  function namesOf(collection) {
4788
5792
  const out = /* @__PURE__ */ new Set();
4789
- for (const entry of asArray27(collection)) {
4790
- const n = strName7(entry.name);
5793
+ for (const entry of asArray26(collection)) {
5794
+ const n = strName9(entry.name);
4791
5795
  if (n) out.add(n);
4792
5796
  }
4793
5797
  return out;
4794
5798
  }
4795
5799
  function validateNavTargetRefs(stack) {
4796
5800
  const findings = [];
4797
- if (!isRec8(stack)) return findings;
4798
- const apps = asArray27(stack.apps);
5801
+ if (!isRec12(stack)) return findings;
5802
+ const apps = asArray26(stack.apps);
4799
5803
  if (apps.length === 0) return findings;
4800
5804
  const declared = /* @__PURE__ */ new Map();
4801
5805
  for (const [, , collection] of NAV_TARGETS) {
4802
5806
  declared.set(collection, namesOf(stack[collection]));
4803
5807
  }
4804
5808
  for (const [ai, app] of apps.entries()) {
4805
- const appName = strName7(app.name) ?? `#${ai}`;
5809
+ const appName = strName9(app.name) ?? `#${ai}`;
4806
5810
  const walk = (items, basePath) => {
4807
5811
  if (!Array.isArray(items)) return;
4808
5812
  for (const [ni, raw] of items.entries()) {
4809
- if (!isRec8(raw)) continue;
5813
+ if (!isRec12(raw)) continue;
4810
5814
  const nav = raw;
4811
5815
  const navPath = `${basePath}[${ni}]`;
4812
5816
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
4813
5817
  if (nav.type !== type) continue;
4814
- const target = strName7(nav[prop]);
5818
+ const target = strName9(nav[prop]);
4815
5819
  if (!target || isInterpolated2(target)) continue;
4816
5820
  const known = declared.get(collection);
4817
5821
  if (known.has(target)) continue;
@@ -4819,7 +5823,7 @@ function validateNavTargetRefs(stack) {
4819
5823
  findings.push({
4820
5824
  severity: "warning",
4821
5825
  rule: NAV_TARGET_UNRESOLVED,
4822
- where: `app "${appName}" \xB7 nav "${strName7(nav.id) ?? strName7(nav.label) ?? `#${ni}`}"`,
5826
+ where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? strName9(nav.label) ?? `#${ni}`}"`,
4823
5827
  path: `${navPath}.${prop}`,
4824
5828
  message: `Navigation targets ${noun} '${target}', which this stack does not declare in \`${collection}\`. ` + (emptyCollection ? `The stack declares NO ${collection} at all, so \`defineStack\`'s own cross-reference check skipped this entry entirely (it is gated on \`${collection === "pages" ? "pageNames" : collection === "reports" ? "reportNames" : "dashboardNames"}.size > 0\`) \u2014 nothing else will report it. ` : "") + `The entry renders in the sidebar and resolves to nothing when clicked. If another package provides this ${noun}, this is expected and advisory only.`,
4825
5829
  hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
@@ -4829,7 +5833,7 @@ function validateNavTargetRefs(stack) {
4829
5833
  }
4830
5834
  };
4831
5835
  walk(app.navigation, `apps[${ai}].navigation`);
4832
- for (const [ari, area] of asArray27(app.areas).entries()) {
5836
+ for (const [ari, area] of asArray26(app.areas).entries()) {
4833
5837
  walk(area.items, `apps[${ai}].areas[${ari}].items`);
4834
5838
  walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
4835
5839
  }
@@ -4839,14 +5843,14 @@ function validateNavTargetRefs(stack) {
4839
5843
 
4840
5844
  // src/validate-action-name-refs.ts
4841
5845
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
4842
- function asArray28(v) {
5846
+ function asArray27(v) {
4843
5847
  if (Array.isArray(v)) return v;
4844
5848
  if (v && typeof v === "object") {
4845
5849
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4846
5850
  }
4847
5851
  return [];
4848
5852
  }
4849
- function strName8(v) {
5853
+ function strName10(v) {
4850
5854
  return typeof v === "string" && v.length > 0 ? v : void 0;
4851
5855
  }
4852
5856
  function strList(v) {
@@ -4883,14 +5887,14 @@ function suggest4(target, known) {
4883
5887
  }
4884
5888
  function collectActionNames(stack) {
4885
5889
  const names = /* @__PURE__ */ new Set();
4886
- for (const action of asArray28(stack.actions)) {
4887
- const n = strName8(action?.name);
5890
+ for (const action of asArray27(stack.actions)) {
5891
+ const n = strName10(action?.name);
4888
5892
  if (n) names.add(n);
4889
5893
  }
4890
- for (const obj of asArray28(stack.objects)) {
5894
+ for (const obj of asArray27(stack.objects)) {
4891
5895
  if (!obj || typeof obj !== "object") continue;
4892
- for (const action of asArray28(obj.actions)) {
4893
- const n = strName8(action?.name);
5896
+ for (const action of asArray27(obj.actions)) {
5897
+ const n = strName10(action?.name);
4894
5898
  if (n) names.add(n);
4895
5899
  }
4896
5900
  }
@@ -4933,7 +5937,7 @@ function validateActionNameRefs(stack) {
4933
5937
  if (!def || typeof def !== "object") continue;
4934
5938
  if (def.execution !== "aggregate") continue;
4935
5939
  if (def.actionDef !== void 0) continue;
4936
- const name = strName8(def.name);
5940
+ const name = strName10(def.name);
4937
5941
  if (!name) continue;
4938
5942
  check(
4939
5943
  name,
@@ -4944,11 +5948,11 @@ function validateActionNameRefs(stack) {
4944
5948
  );
4945
5949
  }
4946
5950
  };
4947
- const views = asArray28(stack.views);
5951
+ const views = asArray27(stack.views);
4948
5952
  for (let vi = 0; vi < views.length; vi++) {
4949
5953
  const view = views[vi];
4950
5954
  if (!view || typeof view !== "object") continue;
4951
- const viewName = strName8(view.name) ?? strName8(view.object) ?? `#${vi}`;
5955
+ const viewName = strName10(view.name) ?? strName10(view.object) ?? `#${vi}`;
4952
5956
  const owner = `view "${viewName}"`;
4953
5957
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
4954
5958
  const listViews = view.listViews;
@@ -4958,22 +5962,22 @@ function validateActionNameRefs(stack) {
4958
5962
  }
4959
5963
  }
4960
5964
  }
4961
- const objects = asArray28(stack.objects);
5965
+ const objects = asArray27(stack.objects);
4962
5966
  for (let oi = 0; oi < objects.length; oi++) {
4963
5967
  const obj = objects[oi];
4964
5968
  if (!obj || typeof obj !== "object") continue;
4965
5969
  const objListViews = obj.listViews;
4966
5970
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
4967
- const owner = `object "${strName8(obj.name) ?? `#${oi}`}"`;
5971
+ const owner = `object "${strName10(obj.name) ?? `#${oi}`}"`;
4968
5972
  for (const [key, lv] of Object.entries(objListViews)) {
4969
5973
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
4970
5974
  }
4971
5975
  }
4972
- const pages = asArray28(stack.pages);
5976
+ const pages = asArray27(stack.pages);
4973
5977
  for (let pi = 0; pi < pages.length; pi++) {
4974
5978
  const page = pages[pi];
4975
5979
  if (!page || typeof page !== "object") continue;
4976
- const pageName = strName8(page.name) ?? `#${pi}`;
5980
+ const pageName = strName10(page.name) ?? `#${pi}`;
4977
5981
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4978
5982
  const props = component.properties;
4979
5983
  if (!props || typeof props !== "object") continue;
@@ -4981,39 +5985,48 @@ function validateActionNameRefs(stack) {
4981
5985
  for (let ai = 0; ai < names.length; ai++) {
4982
5986
  check(
4983
5987
  names[ai],
4984
- `page "${pageName}" \xB7 component "${strName8(component.type) ?? "?"}"`,
5988
+ `page "${pageName}" \xB7 component "${strName10(component.type) ?? "?"}"`,
4985
5989
  `${path}.properties.actionNames[${ai}]`,
4986
5990
  "Quick-actions bar"
4987
5991
  );
4988
5992
  }
4989
5993
  }
4990
5994
  }
4991
- const apps = asArray28(stack.apps);
5995
+ const apps = asArray27(stack.apps);
4992
5996
  for (let ai = 0; ai < apps.length; ai++) {
4993
5997
  const app = apps[ai];
4994
5998
  if (!app || typeof app !== "object") continue;
4995
- const appName = strName8(app.name) ?? `#${ai}`;
5999
+ const appName = strName10(app.name) ?? `#${ai}`;
4996
6000
  const walkNav = (items, basePath) => {
4997
- const navItems = asArray28(items);
6001
+ const navItems = asArray27(items);
4998
6002
  for (let ni = 0; ni < navItems.length; ni++) {
4999
6003
  const nav = navItems[ni];
5000
6004
  if (!nav || typeof nav !== "object") continue;
5001
6005
  const navPath = `${basePath}[${ni}]`;
5002
6006
  const actionDef = nav.actionDef;
5003
- const actionName = strName8(actionDef?.actionName);
6007
+ const actionName = strName10(actionDef?.actionName);
5004
6008
  if (nav.type === "action" && actionName) {
5005
6009
  check(
5006
6010
  actionName,
5007
- `app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
6011
+ `app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
5008
6012
  `${navPath}.actionDef.actionName`,
5009
6013
  "Navigation action item"
5010
6014
  );
5011
6015
  }
6016
+ const runAction = strName10(nav.runAction);
6017
+ if (nav.type === "object" && runAction) {
6018
+ check(
6019
+ runAction,
6020
+ `app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
6021
+ `${navPath}.runAction`,
6022
+ "Navigation deep-link auto-run"
6023
+ );
6024
+ }
5012
6025
  if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
5013
6026
  }
5014
6027
  };
5015
6028
  walkNav(app.navigation, `apps[${ai}].navigation`);
5016
- const areas = asArray28(app.areas);
6029
+ const areas = asArray27(app.areas);
5017
6030
  for (let ri = 0; ri < areas.length; ri++) {
5018
6031
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
5019
6032
  }
@@ -5023,14 +6036,14 @@ function validateActionNameRefs(stack) {
5023
6036
 
5024
6037
  // src/validate-action-locations.ts
5025
6038
  var ACTION_NO_PLACEMENT = "action-no-placement";
5026
- function asArray29(v) {
6039
+ function asArray28(v) {
5027
6040
  if (Array.isArray(v)) return v;
5028
6041
  if (v && typeof v === "object") {
5029
6042
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5030
6043
  }
5031
6044
  return [];
5032
6045
  }
5033
- function strName9(v) {
6046
+ function strName11(v) {
5034
6047
  return typeof v === "string" && v.length > 0 ? v : void 0;
5035
6048
  }
5036
6049
  function strList2(v) {
@@ -5044,8 +6057,8 @@ function collectNamePlacedActions(stack) {
5044
6057
  for (const key of ["rowActions", "bulkActions"]) {
5045
6058
  for (const n of strList2(list3[key])) placed.add(n);
5046
6059
  }
5047
- for (const def of asArray29(list3.bulkActionDefs)) {
5048
- const n = strName9(def?.name);
6060
+ for (const def of asArray28(list3.bulkActionDefs)) {
6061
+ const n = strName11(def?.name);
5049
6062
  if (n) placed.add(n);
5050
6063
  }
5051
6064
  };
@@ -5053,12 +6066,12 @@ function collectNamePlacedActions(stack) {
5053
6066
  if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
5054
6067
  for (const lv of Object.values(listViews)) harvest(lv);
5055
6068
  };
5056
- for (const view of asArray29(stack.views)) {
6069
+ for (const view of asArray28(stack.views)) {
5057
6070
  if (!view || typeof view !== "object") continue;
5058
6071
  harvest(view.list);
5059
6072
  harvestListViews(view.listViews);
5060
6073
  }
5061
- for (const obj of asArray29(stack.objects)) {
6074
+ for (const obj of asArray28(stack.objects)) {
5062
6075
  if (!obj || typeof obj !== "object") continue;
5063
6076
  harvestListViews(obj.listViews);
5064
6077
  }
@@ -5071,7 +6084,7 @@ function validateActionLocations(stack) {
5071
6084
  const check = (action, path) => {
5072
6085
  if (!action || typeof action !== "object") return;
5073
6086
  if ("locations" in action) return;
5074
- const name = strName9(action.name);
6087
+ const name = strName11(action.name);
5075
6088
  if (!name) return;
5076
6089
  if (namePlaced.has(name)) return;
5077
6090
  findings.push({
@@ -5080,16 +6093,16 @@ function validateActionLocations(stack) {
5080
6093
  where: `action "${name}"`,
5081
6094
  path,
5082
6095
  message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
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."
6096
+ hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); 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."
5084
6097
  });
5085
6098
  };
5086
- const actions = asArray29(stack.actions);
6099
+ const actions = asArray28(stack.actions);
5087
6100
  for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
5088
- const objects = asArray29(stack.objects);
6101
+ const objects = asArray28(stack.objects);
5089
6102
  for (let oi = 0; oi < objects.length; oi++) {
5090
6103
  const obj = objects[oi];
5091
6104
  if (!obj || typeof obj !== "object") continue;
5092
- const own = asArray29(obj.actions);
6105
+ const own = asArray28(obj.actions);
5093
6106
  for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
5094
6107
  }
5095
6108
  return findings;
@@ -5100,13 +6113,13 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
5100
6113
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
5101
6114
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5102
6115
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
5103
- function isRec9(v) {
6116
+ function isRec13(v) {
5104
6117
  return !!v && typeof v === "object" && !Array.isArray(v);
5105
6118
  }
5106
- function strName10(v) {
6119
+ function strName12(v) {
5107
6120
  return typeof v === "string" && v.length > 0 ? v : void 0;
5108
6121
  }
5109
- function asArray30(v) {
6122
+ function asArray29(v) {
5110
6123
  if (Array.isArray(v)) return v;
5111
6124
  if (v && typeof v === "object") {
5112
6125
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5117,23 +6130,23 @@ var PROPS_SCHEMAS = ComponentPropsMap;
5117
6130
  var DATASOURCE_SUPPLIED_PROP = "object";
5118
6131
  function suppliedByDataSource(issue, component) {
5119
6132
  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;
6133
+ const dataSource = isRec13(component.dataSource) ? component.dataSource : void 0;
6134
+ return strName12(dataSource?.object) !== void 0;
5122
6135
  }
5123
6136
  function validateComponentProps(stack) {
5124
6137
  const findings = [];
5125
- if (!isRec9(stack)) return findings;
5126
- const pages = asArray30(stack.pages);
6138
+ if (!isRec13(stack)) return findings;
6139
+ const pages = asArray29(stack.pages);
5127
6140
  for (let pi = 0; pi < pages.length; pi++) {
5128
6141
  const page = pages[pi];
5129
- if (!isRec9(page)) continue;
5130
- const pageName = strName10(page.name) ?? `#${pi}`;
6142
+ if (!isRec13(page)) continue;
6143
+ const pageName = strName12(page.name) ?? `#${pi}`;
5131
6144
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5132
- const type = strName10(component.type);
6145
+ const type = strName12(component.type);
5133
6146
  if (!type) continue;
5134
6147
  const schema = PROPS_SCHEMAS[type];
5135
6148
  if (!schema) continue;
5136
- const props = isRec9(component.properties) ? component.properties : void 0;
6149
+ const props = isRec13(component.properties) ? component.properties : void 0;
5137
6150
  if (!props) continue;
5138
6151
  const where = `page "${pageName}" \xB7 ${type}`;
5139
6152
  const base = `${path}.properties`;
@@ -5184,20 +6197,20 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
5184
6197
  var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
5185
6198
  var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
5186
6199
  var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
5187
- function asArray31(v) {
6200
+ function asArray30(v) {
5188
6201
  if (Array.isArray(v)) return v;
5189
6202
  if (v && typeof v === "object") {
5190
6203
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5191
6204
  }
5192
6205
  return [];
5193
6206
  }
5194
- function strName11(v) {
6207
+ function strName13(v) {
5195
6208
  return typeof v === "string" && v.length > 0 ? v : void 0;
5196
6209
  }
5197
6210
  function strList3(v) {
5198
6211
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
5199
6212
  }
5200
- function isRec10(v) {
6213
+ function isRec14(v) {
5201
6214
  return !!v && typeof v === "object" && !Array.isArray(v);
5202
6215
  }
5203
6216
  function distance4(a, b) {
@@ -5235,17 +6248,17 @@ function list2(names) {
5235
6248
  }
5236
6249
  function indexDatasets(stack) {
5237
6250
  const out = /* @__PURE__ */ new Map();
5238
- for (const ds of asArray31(stack.datasets)) {
5239
- const name = strName11(ds.name);
6251
+ for (const ds of asArray30(stack.datasets)) {
6252
+ const name = strName13(ds.name);
5240
6253
  if (!name) continue;
5241
6254
  const dimensions = /* @__PURE__ */ new Set();
5242
- for (const d of asArray31(ds.dimensions)) {
5243
- const n = strName11(d.name);
6255
+ for (const d of asArray30(ds.dimensions)) {
6256
+ const n = strName13(d.name);
5244
6257
  if (n) dimensions.add(n);
5245
6258
  }
5246
6259
  const measures = /* @__PURE__ */ new Set();
5247
- for (const m of asArray31(ds.measures)) {
5248
- const n = strName11(m.name);
6260
+ for (const m of asArray30(ds.measures)) {
6261
+ const n = strName13(m.name);
5249
6262
  if (n) measures.add(n);
5250
6263
  }
5251
6264
  out.set(name, { dimensions, measures });
@@ -5323,29 +6336,29 @@ function validateChartBindings(stack) {
5323
6336
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
5324
6337
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
5325
6338
  };
5326
- const reports = asArray31(stack.reports);
6339
+ const reports = asArray30(stack.reports);
5327
6340
  for (let ri = 0; ri < reports.length; ri++) {
5328
6341
  const report = reports[ri];
5329
- if (!isRec10(report)) continue;
5330
- const reportName = strName11(report.name) ?? `#${ri}`;
6342
+ if (!isRec14(report)) continue;
6343
+ const reportName = strName13(report.name) ?? `#${ri}`;
5331
6344
  const checkReportChart = (chart, dataset, values, where, path) => {
5332
- if (!isRec10(chart)) return;
6345
+ if (!isRec14(chart)) return;
5333
6346
  check({
5334
6347
  dataset,
5335
6348
  // `values` is the report's measure SELECTION, not a chart ref; feeding
5336
6349
  // it in lets the yAxis "declared but not selected" check work without
5337
6350
  // reporting the selection itself twice.
5338
6351
  values: { names: values, path: `${path}.values` },
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),
6352
+ xAxis: strName13(chart.xAxis) ? { name: strName13(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
6353
+ yAxis: strName13(chart.yAxis) ? { name: strName13(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
6354
+ series: asArray30(chart.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
5342
6355
  where,
5343
6356
  path: `${path}.chart`
5344
6357
  });
5345
6358
  };
5346
6359
  checkReportChart(
5347
6360
  report.chart,
5348
- strName11(report.dataset),
6361
+ strName13(report.dataset),
5349
6362
  strList3(report.values),
5350
6363
  `report "${reportName}" \xB7 chart`,
5351
6364
  `reports[${ri}]`
@@ -5353,45 +6366,45 @@ function validateChartBindings(stack) {
5353
6366
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
5354
6367
  for (let bi = 0; bi < blocks.length; bi++) {
5355
6368
  const block = blocks[bi];
5356
- if (!isRec10(block)) continue;
6369
+ if (!isRec14(block)) continue;
5357
6370
  checkReportChart(
5358
6371
  block.chart,
5359
- strName11(block.dataset),
6372
+ strName13(block.dataset),
5360
6373
  strList3(block.values),
5361
- `report "${reportName}" \xB7 block "${strName11(block.name) ?? `#${bi}`}" chart`,
6374
+ `report "${reportName}" \xB7 block "${strName13(block.name) ?? `#${bi}`}" chart`,
5362
6375
  `reports[${ri}].blocks[${bi}]`
5363
6376
  );
5364
6377
  }
5365
6378
  }
5366
6379
  const checkListChart = (container, where, path) => {
5367
- if (!isRec10(container)) return;
6380
+ if (!isRec14(container)) return;
5368
6381
  const chart = container.chart;
5369
- if (!isRec10(chart)) return;
6382
+ if (!isRec14(chart)) return;
5370
6383
  check({
5371
- dataset: strName11(chart.dataset),
6384
+ dataset: strName13(chart.dataset),
5372
6385
  dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
5373
6386
  values: { names: strList3(chart.values), path: `${path}.chart.values` },
5374
6387
  where,
5375
6388
  path: `${path}.chart`
5376
6389
  });
5377
6390
  };
5378
- const views = asArray31(stack.views);
6391
+ const views = asArray30(stack.views);
5379
6392
  for (let vi = 0; vi < views.length; vi++) {
5380
6393
  const view = views[vi];
5381
- if (!isRec10(view)) continue;
5382
- const viewName = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
6394
+ if (!isRec14(view)) continue;
6395
+ const viewName = strName13(view.name) ?? strName13(view.objectName) ?? `#${vi}`;
5383
6396
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
5384
- if (isRec10(view.listViews)) {
6397
+ if (isRec14(view.listViews)) {
5385
6398
  for (const [key, lv] of Object.entries(view.listViews)) {
5386
6399
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
5387
6400
  }
5388
6401
  }
5389
6402
  }
5390
- const objects = asArray31(stack.objects);
6403
+ const objects = asArray30(stack.objects);
5391
6404
  for (let oi = 0; oi < objects.length; oi++) {
5392
6405
  const obj = objects[oi];
5393
- if (!isRec10(obj) || !isRec10(obj.listViews)) continue;
5394
- const objName = strName11(obj.name) ?? `#${oi}`;
6406
+ if (!isRec14(obj) || !isRec14(obj.listViews)) continue;
6407
+ const objName = strName13(obj.name) ?? `#${oi}`;
5395
6408
  for (const [key, lv] of Object.entries(obj.listViews)) {
5396
6409
  checkListChart(
5397
6410
  lv,
@@ -5400,22 +6413,22 @@ function validateChartBindings(stack) {
5400
6413
  );
5401
6414
  }
5402
6415
  }
5403
- const pages = asArray31(stack.pages);
6416
+ const pages = asArray30(stack.pages);
5404
6417
  for (let pi = 0; pi < pages.length; pi++) {
5405
6418
  const page = pages[pi];
5406
- if (!isRec10(page)) continue;
5407
- const pageName = strName11(page.name) ?? `#${pi}`;
6419
+ if (!isRec14(page)) continue;
6420
+ const pageName = strName13(page.name) ?? `#${pi}`;
5408
6421
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
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);
6422
+ const props = isRec14(component.properties) ? component.properties : void 0;
6423
+ if (!props || !strName13(props.dataset)) continue;
6424
+ const axisRefs = asArray30(props.yAxis).map((a, ai) => ({ name: strName13(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
6425
+ const seriesRefs = asArray30(props.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
5413
6426
  check({
5414
- dataset: strName11(props.dataset),
6427
+ dataset: strName13(props.dataset),
5415
6428
  dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
5416
6429
  values: { names: strList3(props.values), path: `${path}.properties.values` },
5417
6430
  series: [...axisRefs, ...seriesRefs],
5418
- where: `page "${pageName}" \xB7 ${strName11(component.type) ?? "chart"}`,
6431
+ where: `page "${pageName}" \xB7 ${strName13(component.type) ?? "chart"}`,
5419
6432
  path: `${path}.properties`
5420
6433
  });
5421
6434
  }
@@ -5424,15 +6437,15 @@ function validateChartBindings(stack) {
5424
6437
  }
5425
6438
 
5426
6439
  // src/validate-rule-compilability.ts
5427
- import { createRequire as createRequire3 } from "module";
6440
+ import { createRequire as createRequire4 } from "module";
5428
6441
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
5429
6442
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
5430
6443
  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 }));
6444
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6445
+ function asArray31(v) {
6446
+ if (Array.isArray(v)) return v.filter(isRec15);
6447
+ if (isRec15(v)) {
6448
+ return Object.entries(v).filter(([, def]) => isRec15(def)).map(([name, def]) => ({ name, ...def }));
5436
6449
  }
5437
6450
  return [];
5438
6451
  }
@@ -5443,13 +6456,13 @@ function loadAjv() {
5443
6456
  const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5444
6457
  let mod;
5445
6458
  try {
5446
- mod = createRequire3(anchor)("ajv");
6459
+ mod = createRequire4(anchor)("ajv");
5447
6460
  } catch (err) {
5448
6461
  throw new Error(
5449
6462
  `@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
6463
  );
5451
6464
  }
5452
- const ctor = isRec11(mod) && "default" in mod ? mod.default : mod;
6465
+ const ctor = isRec15(mod) && "default" in mod ? mod.default : mod;
5453
6466
  cachedAjv = ctor;
5454
6467
  return ctor;
5455
6468
  }
@@ -5458,13 +6471,13 @@ function loadAddFormats() {
5458
6471
  const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
5459
6472
  let mod;
5460
6473
  try {
5461
- mod = createRequire3(anchor)("ajv-formats");
6474
+ mod = createRequire4(anchor)("ajv-formats");
5462
6475
  } catch (err) {
5463
6476
  throw new Error(
5464
6477
  `@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
6478
  );
5466
6479
  }
5467
- const plugin = isRec11(mod) && "default" in mod ? mod.default : mod;
6480
+ const plugin = isRec15(mod) && "default" in mod ? mod.default : mod;
5468
6481
  cachedAddFormats = plugin;
5469
6482
  return plugin;
5470
6483
  }
@@ -5494,17 +6507,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
5494
6507
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
5495
6508
  for (const branch of ["then", "otherwise"]) {
5496
6509
  const nested = rule[branch];
5497
- if (isRec11(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6510
+ if (isRec15(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
5498
6511
  }
5499
6512
  return out;
5500
6513
  }
5501
6514
  function walkObjectValidationRules(stack) {
5502
6515
  const walked = [];
5503
- if (!isRec11(stack)) return walked;
5504
- for (const obj of asArray32(stack.objects)) {
6516
+ if (!isRec15(stack)) return walked;
6517
+ for (const obj of asArray31(stack.objects)) {
5505
6518
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
5506
6519
  const validations = obj.validations;
5507
- for (const authored of asArray32(validations)) {
6520
+ for (const authored of asArray31(validations)) {
5508
6521
  for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
5509
6522
  walked.push({
5510
6523
  rule,
@@ -5535,7 +6548,7 @@ function validateRuleCompilability(stack) {
5535
6548
  });
5536
6549
  }
5537
6550
  }
5538
- if (rule.type === "json_schema" && isRec11(rule.schema)) {
6551
+ if (rule.type === "json_schema" && isRec15(rule.schema)) {
5539
6552
  try {
5540
6553
  createRuntimeAjv().compile(rule.schema);
5541
6554
  } catch (err) {
@@ -5555,7 +6568,7 @@ function validateRuleCompilability(stack) {
5555
6568
 
5556
6569
  // src/validate-rule-schema-formats.ts
5557
6570
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
5558
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6571
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5559
6572
  var SUBSCHEMA_KEYS = [
5560
6573
  "additionalItems",
5561
6574
  "additionalProperties",
@@ -5579,7 +6592,7 @@ var SUBSCHEMA_MAP_KEYS = [
5579
6592
  var MAX_SCHEMA_WALK_DEPTH = 32;
5580
6593
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
5581
6594
  function collectFormatUses(schema, pointer, out, depth) {
5582
- if (!isRec12(schema)) return;
6595
+ if (!isRec16(schema)) return;
5583
6596
  if (typeof schema.format === "string") {
5584
6597
  out.push({ pointer: `${pointer}/format`, name: schema.format });
5585
6598
  }
@@ -5598,7 +6611,7 @@ function collectFormatUses(schema, pointer, out, depth) {
5598
6611
  }
5599
6612
  for (const key of SUBSCHEMA_MAP_KEYS) {
5600
6613
  const value = schema[key];
5601
- if (!isRec12(value)) continue;
6614
+ if (!isRec16(value)) continue;
5602
6615
  for (const [name, entry] of Object.entries(value)) {
5603
6616
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
5604
6617
  }
@@ -5606,13 +6619,13 @@ function collectFormatUses(schema, pointer, out, depth) {
5606
6619
  const items = schema.items;
5607
6620
  if (Array.isArray(items)) {
5608
6621
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
5609
- } else if (isRec12(items)) {
6622
+ } else if (isRec16(items)) {
5610
6623
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
5611
6624
  }
5612
6625
  const dependencies = schema.dependencies;
5613
- if (isRec12(dependencies)) {
6626
+ if (isRec16(dependencies)) {
5614
6627
  for (const [name, entry] of Object.entries(dependencies)) {
5615
- if (!isRec12(entry)) continue;
6628
+ if (!isRec16(entry)) continue;
5616
6629
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
5617
6630
  }
5618
6631
  }
@@ -5651,7 +6664,7 @@ function validateRuleSchemaFormats(stack) {
5651
6664
  const findings = [];
5652
6665
  const pending = [];
5653
6666
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
5654
- if (rule.type !== "json_schema" || !isRec12(rule.schema)) continue;
6667
+ if (rule.type !== "json_schema" || !isRec16(rule.schema)) continue;
5655
6668
  const uses = [];
5656
6669
  collectFormatUses(rule.schema, "", uses, 0);
5657
6670
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -5679,7 +6692,7 @@ function validateRuleSchemaFormats(stack) {
5679
6692
  import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
5680
6693
 
5681
6694
  // src/build-access-matrix.ts
5682
- function asArray33(v) {
6695
+ function asArray32(v) {
5683
6696
  if (Array.isArray(v)) return v;
5684
6697
  if (v && typeof v === "object") {
5685
6698
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5690,13 +6703,13 @@ function buildAccessMatrix(stack) {
5690
6703
  const entries = [];
5691
6704
  if (!stack || typeof stack !== "object") return { version: 1, entries };
5692
6705
  const owdByObject = /* @__PURE__ */ new Map();
5693
- for (const obj of asArray33(stack.objects)) {
6706
+ for (const obj of asArray32(stack.objects)) {
5694
6707
  const name = typeof obj.name === "string" ? obj.name : "";
5695
6708
  if (!name) continue;
5696
6709
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
5697
6710
  if (typeof owd === "string") owdByObject.set(name, owd);
5698
6711
  }
5699
- for (const ps of asArray33(stack.permissions)) {
6712
+ for (const ps of asArray32(stack.permissions)) {
5700
6713
  const psName = typeof ps.name === "string" ? ps.name : "";
5701
6714
  if (!psName) continue;
5702
6715
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -5769,34 +6782,34 @@ function diffAccessMatrix(before, after) {
5769
6782
 
5770
6783
  // src/validate-nav-access.ts
5771
6784
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
5772
- function asArray34(v) {
6785
+ function asArray33(v) {
5773
6786
  if (Array.isArray(v)) return v;
5774
6787
  if (v && typeof v === "object") {
5775
6788
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5776
6789
  }
5777
6790
  return [];
5778
6791
  }
5779
- function strName12(v) {
6792
+ function strName14(v) {
5780
6793
  return typeof v === "string" && v.length > 0 ? v : void 0;
5781
6794
  }
5782
6795
  function collectNavExposures(stack) {
5783
6796
  const out = [];
5784
- const apps = asArray34(stack.apps);
6797
+ const apps = asArray33(stack.apps);
5785
6798
  for (let ai = 0; ai < apps.length; ai++) {
5786
6799
  const app = apps[ai];
5787
6800
  if (!app || typeof app !== "object") continue;
5788
- const appName = strName12(app.name) ?? `#${ai}`;
6801
+ const appName = strName14(app.name) ?? `#${ai}`;
5789
6802
  const walk = (items, basePath) => {
5790
- const navItems = asArray34(items);
6803
+ const navItems = asArray33(items);
5791
6804
  for (let ni = 0; ni < navItems.length; ni++) {
5792
6805
  const nav = navItems[ni];
5793
6806
  if (!nav || typeof nav !== "object") continue;
5794
6807
  const navPath = `${basePath}[${ni}]`;
5795
- const objectName = strName12(nav.objectName);
6808
+ const objectName = strName14(nav.objectName);
5796
6809
  if (nav.type === "object" && objectName) {
5797
6810
  out.push({
5798
6811
  objectName,
5799
- where: `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
6812
+ where: `app "${appName}" \xB7 nav "${strName14(nav.id) ?? `#${ni}`}"`,
5800
6813
  path: `${navPath}.objectName`
5801
6814
  });
5802
6815
  }
@@ -5804,7 +6817,7 @@ function collectNavExposures(stack) {
5804
6817
  }
5805
6818
  };
5806
6819
  walk(app.navigation, `apps[${ai}].navigation`);
5807
- const areas = asArray34(app.areas);
6820
+ const areas = asArray33(app.areas);
5808
6821
  for (let ri = 0; ri < areas.length; ri++) {
5809
6822
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
5810
6823
  }
@@ -5814,13 +6827,13 @@ function collectNavExposures(stack) {
5814
6827
  function validateNavAccess(stack) {
5815
6828
  const findings = [];
5816
6829
  if (!stack || typeof stack !== "object") return findings;
5817
- const permissionSets = asArray34(stack.permissions);
6830
+ const permissionSets = asArray33(stack.permissions);
5818
6831
  if (permissionSets.length === 0) return findings;
5819
6832
  const exposures = collectNavExposures(stack);
5820
6833
  if (exposures.length === 0) return findings;
5821
6834
  const ownObjects = /* @__PURE__ */ new Set();
5822
- for (const obj of asArray34(stack.objects)) {
5823
- const n = strName12(obj.name);
6835
+ for (const obj of asArray33(stack.objects)) {
6836
+ const n = strName14(obj.name);
5824
6837
  if (n) ownObjects.add(n);
5825
6838
  }
5826
6839
  const readable = /* @__PURE__ */ new Set();
@@ -5849,18 +6862,19 @@ function validateNavAccess(stack) {
5849
6862
  }
5850
6863
 
5851
6864
  // src/validate-translation-references.ts
6865
+ import { expandViewContainer } from "@objectstack/spec";
5852
6866
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
5853
6867
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
5854
6868
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
5855
- function isRec13(v) {
6869
+ function isRec17(v) {
5856
6870
  return !!v && typeof v === "object" && !Array.isArray(v);
5857
6871
  }
5858
- function asArray35(v) {
6872
+ function asArray34(v) {
5859
6873
  if (Array.isArray(v)) return v;
5860
- if (isRec13(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec13(def) ? def : {} }));
6874
+ if (isRec17(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec17(def) ? def : {} }));
5861
6875
  return [];
5862
6876
  }
5863
- function strName13(v) {
6877
+ function strName15(v) {
5864
6878
  return typeof v === "string" && v.length > 0 ? v : void 0;
5865
6879
  }
5866
6880
  function distance5(a, b) {
@@ -5920,30 +6934,52 @@ function collectViewRecord(view, factsFor) {
5920
6934
  };
5921
6935
  const addSections = (container, binding) => {
5922
6936
  if (!binding) return;
5923
- for (const section of asArray35(container.sections)) {
5924
- const sectionName = strName13(section.name);
6937
+ for (const section of asArray34(container.sections)) {
6938
+ const sectionName = strName15(section.name);
5925
6939
  if (sectionName) factsFor(binding).sections.add(sectionName);
5926
6940
  }
5927
6941
  };
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));
5931
- for (const key of ["listViews", "formViews"]) {
5932
- const container = view[key];
5933
- if (!isRec13(container)) continue;
5934
- for (const [subKey, sub] of Object.entries(container)) {
5935
- if (!isRec13(sub)) continue;
6942
+ const listBinding = isRec17(view.list) ? bindingOf(view.list) : void 0;
6943
+ if (isRec17(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
6944
+ addView(recordObject ?? listBinding, strName15(view.name));
6945
+ const named = namedViewKeys(view);
6946
+ for (const family of ["listViews", "formViews"]) {
6947
+ const container = view[family];
6948
+ if (!isRec17(container)) continue;
6949
+ const registryKeys = family === "listViews" ? named.list : named.form;
6950
+ let at = 0;
6951
+ for (const sub of Object.values(container)) {
6952
+ if (!sub || typeof sub !== "object") continue;
6953
+ const registryKey = registryKeys[at++];
6954
+ if (!isRec17(sub)) continue;
5936
6955
  const binding = bindingOf(sub) ?? listBinding;
5937
- addView(binding, subKey);
5938
- addView(binding, strName13(sub.name));
6956
+ addView(binding, registryKey);
5939
6957
  addSections(sub, binding);
5940
6958
  }
5941
6959
  }
5942
- if (isRec13(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
6960
+ if (isRec17(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
5943
6961
  addSections(view, recordObject ?? listBinding);
5944
6962
  }
5945
- function viewObjectName(view) {
5946
- return strName13(view.objectName) ?? strName13(view.object) ?? (isRec13(view.data) ? strName13(view.data.object) : void 0);
6963
+ function defaultListViewKey(object, container) {
6964
+ if (!object || !isRec17(container.list)) return void 0;
6965
+ const item = expandViewContainer(object, container).find(
6966
+ (i) => i.viewKind === "list" && i.isDefault
6967
+ );
6968
+ if (!item) return void 0;
6969
+ const prefix = `${object}.`;
6970
+ return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
6971
+ }
6972
+ function namedViewKeys(container) {
6973
+ const object = "probe";
6974
+ const prefix = `${object}.`;
6975
+ const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
6976
+ const countEntries = (v) => isRec17(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
6977
+ const listCount = countEntries(container.listViews);
6978
+ const formCount = countEntries(container.formViews);
6979
+ if (!listCount && !formCount) return { list: [], form: [] };
6980
+ const items = expandViewContainer(object, container);
6981
+ const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
6982
+ return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
5947
6983
  }
5948
6984
  function readOptions(field) {
5949
6985
  const raw = field.options;
@@ -5955,14 +6991,14 @@ function readOptions(field) {
5955
6991
  values.add(opt);
5956
6992
  continue;
5957
6993
  }
5958
- if (!isRec13(opt)) continue;
5959
- const value = strName13(opt.value);
6994
+ if (!isRec17(opt)) continue;
6995
+ const value = strName15(opt.value);
5960
6996
  if (!value) continue;
5961
6997
  values.add(value);
5962
- const label2 = strName13(opt.label);
6998
+ const label2 = strName15(opt.label);
5963
6999
  if (label2) byLabel.set(label2.toLowerCase(), value);
5964
7000
  }
5965
- } else if (isRec13(raw)) {
7001
+ } else if (isRec17(raw)) {
5966
7002
  for (const [value, label2] of Object.entries(raw)) {
5967
7003
  values.add(value);
5968
7004
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -5982,48 +7018,48 @@ function buildUniverse(stack) {
5982
7018
  }
5983
7019
  return facts;
5984
7020
  };
5985
- for (const obj of asArray35(stack.objects)) {
5986
- const objectName = strName13(obj.name);
7021
+ for (const obj of asArray34(stack.objects)) {
7022
+ const objectName = strName15(obj.name);
5987
7023
  if (!objectName) continue;
5988
7024
  const facts = factsFor(objectName);
5989
- for (const field of asArray35(obj.fields)) {
5990
- const fieldName = strName13(field.name);
7025
+ for (const field of asArray34(obj.fields)) {
7026
+ const fieldName = strName15(field.name);
5991
7027
  if (fieldName) facts.fields.set(fieldName, field);
5992
7028
  }
5993
- for (const action of asArray35(obj.actions)) {
5994
- const actionName = strName13(action.name);
7029
+ for (const action of asArray34(obj.actions)) {
7030
+ const actionName = strName15(action.name);
5995
7031
  if (actionName) facts.actions.set(actionName, action);
5996
7032
  }
5997
- for (const view of asArray35(obj.views)) {
5998
- collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
7033
+ for (const view of asArray34(obj.views)) {
7034
+ collectViewRecord({ ...view, object: strName15(view.object) ?? objectName }, factsFor);
5999
7035
  }
6000
7036
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
6001
- for (const group of asArray35(obj.fieldGroups)) {
6002
- const key = strName13(group.key) ?? strName13(group.name);
7037
+ for (const group of asArray34(obj.fieldGroups)) {
7038
+ const key = strName15(group.key) ?? strName15(group.name);
6003
7039
  if (key) facts.sections.add(key);
6004
7040
  }
6005
7041
  }
6006
- for (const view of asArray35(stack.views)) {
7042
+ for (const view of asArray34(stack.views)) {
6007
7043
  collectViewRecord(view, factsFor);
6008
7044
  }
6009
- const pages = asArray35(stack.pages);
7045
+ const pages = asArray34(stack.pages);
6010
7046
  for (let pi = 0; pi < pages.length; pi++) {
6011
7047
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
6012
7048
  if (!walked.objectName) continue;
6013
- const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
7049
+ const props = isRec17(walked.component.properties) ? walked.component.properties : void 0;
6014
7050
  if (!props) continue;
6015
- for (const section of asArray35(props.sections)) {
6016
- const sectionName = strName13(section.name);
7051
+ for (const section of asArray34(props.sections)) {
7052
+ const sectionName = strName15(section.name);
6017
7053
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
6018
7054
  }
6019
7055
  }
6020
7056
  }
6021
7057
  const globalActions = /* @__PURE__ */ new Map();
6022
7058
  const actionOwners = /* @__PURE__ */ new Map();
6023
- for (const action of asArray35(stack.actions)) {
6024
- const actionName = strName13(action.name);
7059
+ for (const action of asArray34(stack.actions)) {
7060
+ const actionName = strName15(action.name);
6025
7061
  if (!actionName) continue;
6026
- const owner = strName13(action.objectName) ?? strName13(action.object);
7062
+ const owner = strName15(action.objectName) ?? strName15(action.object);
6027
7063
  if (owner) {
6028
7064
  factsFor(owner).actions.set(actionName, action);
6029
7065
  actionOwners.set(actionName, owner);
@@ -6037,41 +7073,41 @@ function buildUniverse(stack) {
6037
7073
  }
6038
7074
  }
6039
7075
  const apps = /* @__PURE__ */ new Map();
6040
- for (const app of asArray35(stack.apps)) {
6041
- const appName = strName13(app.name);
7076
+ for (const app of asArray34(stack.apps)) {
7077
+ const appName = strName15(app.name);
6042
7078
  if (!appName) continue;
6043
7079
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
6044
7080
  const walkNav = (items) => {
6045
- for (const item of asArray35(items)) {
6046
- const id = strName13(item.id);
7081
+ for (const item of asArray34(items)) {
7082
+ const id = strName15(item.id);
6047
7083
  if (id) navIds.add(id);
6048
7084
  if (item.children) walkNav(item.children);
6049
7085
  }
6050
7086
  };
6051
7087
  walkNav(app.navigation);
6052
- for (const area of asArray35(app.areas)) {
6053
- const areaId = strName13(area.id);
7088
+ for (const area of asArray34(app.areas)) {
7089
+ const areaId = strName15(area.id);
6054
7090
  if (areaId) navIds.add(areaId);
6055
7091
  walkNav(area.navigation);
6056
7092
  }
6057
7093
  apps.set(appName, navIds);
6058
7094
  }
6059
7095
  const dashboards = /* @__PURE__ */ new Map();
6060
- for (const dash of asArray35(stack.dashboards)) {
6061
- const dashName = strName13(dash.name);
7096
+ for (const dash of asArray34(stack.dashboards)) {
7097
+ const dashName = strName15(dash.name);
6062
7098
  if (!dashName) continue;
6063
7099
  const widgets = /* @__PURE__ */ new Set();
6064
- for (const widget of asArray35(dash.widgets)) {
6065
- const id = strName13(widget.id) ?? strName13(widget.name);
7100
+ for (const widget of asArray34(dash.widgets)) {
7101
+ const id = strName15(widget.id) ?? strName15(widget.name);
6066
7102
  if (id) widgets.add(id);
6067
7103
  }
6068
7104
  const actions = /* @__PURE__ */ new Set();
6069
7105
  const headerActions = [
6070
- ...asArray35(isRec13(dash.header) ? dash.header.actions : void 0),
6071
- ...asArray35(dash.actions)
7106
+ ...asArray34(isRec17(dash.header) ? dash.header.actions : void 0),
7107
+ ...asArray34(dash.actions)
6072
7108
  ];
6073
7109
  for (const action of headerActions) {
6074
- const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
7110
+ const key = strName15(action.actionUrl) ?? strName15(action.url) ?? strName15(action.name);
6075
7111
  if (key) actions.add(key);
6076
7112
  }
6077
7113
  dashboards.set(dashName, { widgets, actions });
@@ -6083,7 +7119,7 @@ function localePath(bundleIndex, locale) {
6083
7119
  }
6084
7120
  function validateTranslationReferences(stack) {
6085
7121
  const findings = [];
6086
- if (!isRec13(stack)) return findings;
7122
+ if (!isRec17(stack)) return findings;
6087
7123
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
6088
7124
  if (bundles.length === 0) return findings;
6089
7125
  const universe = buildUniverse(stack);
@@ -6092,13 +7128,13 @@ function validateTranslationReferences(stack) {
6092
7128
  };
6093
7129
  for (let bi = 0; bi < bundles.length; bi++) {
6094
7130
  const bundle = bundles[bi];
6095
- if (!isRec13(bundle)) continue;
7131
+ if (!isRec17(bundle)) continue;
6096
7132
  for (const [locale, rawData] of Object.entries(bundle)) {
6097
- if (!isRec13(rawData)) continue;
7133
+ if (!isRec17(rawData)) continue;
6098
7134
  const base = localePath(bi, locale);
6099
7135
  const inLocale = `locale "${locale}"`;
6100
7136
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
6101
- if (!isRec13(rawNode)) continue;
7137
+ if (!isRec17(rawNode)) continue;
6102
7138
  const objPath = `${base}.objects.${objectName}`;
6103
7139
  const facts = universe.objects.get(objectName);
6104
7140
  if (!facts) {
@@ -6124,7 +7160,7 @@ function validateTranslationReferences(stack) {
6124
7160
  );
6125
7161
  continue;
6126
7162
  }
6127
- if (!isRec13(rawField)) continue;
7163
+ if (!isRec17(rawField)) continue;
6128
7164
  checkOptionKeys(findings, {
6129
7165
  optionMap: rawField.options,
6130
7166
  field,
@@ -6206,7 +7242,7 @@ function validateTranslationReferences(stack) {
6206
7242
  );
6207
7243
  continue;
6208
7244
  }
6209
- if (!isRec13(rawApp)) continue;
7245
+ if (!isRec17(rawApp)) continue;
6210
7246
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
6211
7247
  if (navIds.has(navId)) continue;
6212
7248
  orphan(
@@ -6229,7 +7265,7 @@ function validateTranslationReferences(stack) {
6229
7265
  );
6230
7266
  continue;
6231
7267
  }
6232
- if (!isRec13(rawDash)) continue;
7268
+ if (!isRec17(rawDash)) continue;
6233
7269
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
6234
7270
  if (dash.widgets.has(widgetId)) continue;
6235
7271
  orphan(
@@ -6254,7 +7290,7 @@ function validateTranslationReferences(stack) {
6254
7290
  return findings;
6255
7291
  }
6256
7292
  function asRecord(v) {
6257
- return isRec13(v) ? v : {};
7293
+ return isRec17(v) ? v : {};
6258
7294
  }
6259
7295
  function checkOptionKeys(findings, ctx) {
6260
7296
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -6266,7 +7302,7 @@ function checkOptionKeys(findings, ctx) {
6266
7302
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
6267
7303
  where: ctx.where,
6268
7304
  path: ctx.path,
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.`,
7305
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName15(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
6270
7306
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
6271
7307
  });
6272
7308
  return;
@@ -6285,11 +7321,11 @@ function checkOptionKeys(findings, ctx) {
6285
7321
  }
6286
7322
  }
6287
7323
  function checkActionParams(findings, ctx) {
6288
- const rawParams = Object.keys(asRecord(isRec13(ctx.rawAction) ? ctx.rawAction.params : void 0));
7324
+ const rawParams = Object.keys(asRecord(isRec17(ctx.rawAction) ? ctx.rawAction.params : void 0));
6289
7325
  if (rawParams.length === 0) return;
6290
7326
  const declared = /* @__PURE__ */ new Set();
6291
- for (const param of asArray35(ctx.action.params)) {
6292
- const name = strName13(param.name) ?? strName13(param.field);
7327
+ for (const param of asArray34(ctx.action.params)) {
7328
+ const name = strName15(param.name) ?? strName15(param.field);
6293
7329
  if (name) declared.add(name);
6294
7330
  }
6295
7331
  for (const paramName of rawParams) {
@@ -6307,76 +7343,40 @@ function checkActionParams(findings, ctx) {
6307
7343
 
6308
7344
  // src/validate-translatable-sections.ts
6309
7345
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
6310
- function isRec14(v) {
7346
+ function isRec18(v) {
6311
7347
  return !!v && typeof v === "object" && !Array.isArray(v);
6312
7348
  }
6313
- function strName14(v) {
7349
+ function strName16(v) {
6314
7350
  return typeof v === "string" && v.length > 0 ? v : void 0;
6315
7351
  }
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
7352
  function viewLabel(view) {
6333
- const name = strName14(view.name);
7353
+ const name = strName16(view.name);
6334
7354
  return name ? `view "${name}"` : "";
6335
7355
  }
6336
7356
  function joinWhere(...parts) {
6337
7357
  return parts.filter((p) => p.length > 0).join(" \xB7 ");
6338
7358
  }
6339
7359
  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)) {
7360
+ const recordObject = viewObjectName(view);
7361
+ const listBinding = isRec18(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
7362
+ for (const site of viewContainerSites(view, basePath)) {
6350
7363
  sites.push({
6351
- path: `${basePath}.form.sections`,
6352
- surface: joinWhere(label2, "form"),
6353
- objectName: bindingOf(view.form) ?? listBinding,
6354
- sections: view.form.sections
7364
+ path: `${site.path}.sections`,
7365
+ surface: joinWhere(label2, site.surface),
7366
+ objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
7367
+ sections: site.view.sections
6355
7368
  });
6356
7369
  }
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
7370
  }
6371
7371
  function translatedObjectNames(stack) {
6372
7372
  const out = /* @__PURE__ */ new Set();
6373
7373
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
6374
7374
  for (const bundle of bundles) {
6375
- if (!isRec14(bundle)) continue;
7375
+ if (!isRec18(bundle)) continue;
6376
7376
  for (const data of Object.values(bundle)) {
6377
- if (!isRec14(data) || !isRec14(data.objects)) continue;
7377
+ if (!isRec18(data) || !isRec18(data.objects)) continue;
6378
7378
  for (const [objectName, node] of Object.entries(data.objects)) {
6379
- if (isRec14(node)) out.add(objectName);
7379
+ if (isRec18(node)) out.add(objectName);
6380
7380
  }
6381
7381
  }
6382
7382
  }
@@ -6388,22 +7388,22 @@ function suggestedName(label2) {
6388
7388
  }
6389
7389
  function validateTranslatableSections(stack) {
6390
7390
  const findings = [];
6391
- if (!isRec14(stack)) return findings;
7391
+ if (!isRec18(stack)) return findings;
6392
7392
  const translated = translatedObjectNames(stack);
6393
7393
  if (translated.size === 0) return findings;
6394
7394
  const sites = [];
6395
7395
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
6396
- const objectName = strName14(obj.name);
7396
+ const objectName = strName16(obj.name);
6397
7397
  if (!objectName) continue;
6398
7398
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
6399
7399
  collectViewSites(
6400
- { ...view, object: strName14(view.object) ?? objectName },
7400
+ { ...view, object: strName16(view.object) ?? objectName },
6401
7401
  path,
6402
7402
  viewLabel(view),
6403
7403
  sites
6404
7404
  );
6405
7405
  }
6406
- if (isRec14(obj.listViews)) {
7406
+ if (isRec18(obj.listViews)) {
6407
7407
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
6408
7408
  }
6409
7409
  }
@@ -6411,13 +7411,13 @@ function validateTranslatableSections(stack) {
6411
7411
  collectViewSites(view, path, viewLabel(view), sites);
6412
7412
  }
6413
7413
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
6414
- const pageName = strName14(page.name);
7414
+ const pageName = strName16(page.name);
6415
7415
  const pageLabel = pageName ? `page "${pageName}"` : "";
6416
7416
  for (const walked of walkPageComponents(page, pagePath)) {
6417
7417
  if (!walked.objectName) continue;
6418
- const props = isRec14(walked.component.properties) ? walked.component.properties : void 0;
7418
+ const props = isRec18(walked.component.properties) ? walked.component.properties : void 0;
6419
7419
  if (!props) continue;
6420
- const type = strName14(walked.component.type) ?? "component";
7420
+ const type = strName16(walked.component.type) ?? "component";
6421
7421
  sites.push({
6422
7422
  path: `${walked.path}.properties.sections`,
6423
7423
  surface: joinWhere(pageLabel, type),
@@ -6432,9 +7432,9 @@ function validateTranslatableSections(stack) {
6432
7432
  if (!Array.isArray(site.sections)) continue;
6433
7433
  for (let i = 0; i < site.sections.length; i++) {
6434
7434
  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);
7435
+ if (!isRec18(section)) continue;
7436
+ if (strName16(section.name)) continue;
7437
+ const heading = strName16(section.label);
6438
7438
  if (!heading) continue;
6439
7439
  const slug = suggestedName(heading);
6440
7440
  findings.push({
@@ -6452,14 +7452,14 @@ function validateTranslatableSections(stack) {
6452
7452
 
6453
7453
  // src/validate-ai-surface-affinity.ts
6454
7454
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
6455
- function asArray36(v) {
7455
+ function asArray35(v) {
6456
7456
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
6457
7457
  if (v && typeof v === "object") {
6458
7458
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6459
7459
  }
6460
7460
  return [];
6461
7461
  }
6462
- function strName15(v) {
7462
+ function strName17(v) {
6463
7463
  return typeof v === "string" && v.length > 0 ? v : void 0;
6464
7464
  }
6465
7465
  function surfaceOf(v) {
@@ -6469,18 +7469,18 @@ function validateAiSurfaceAffinity(stack) {
6469
7469
  const findings = [];
6470
7470
  if (!stack || typeof stack !== "object") return findings;
6471
7471
  const skillsByName = /* @__PURE__ */ new Map();
6472
- for (const skill of asArray36(stack.skills)) {
6473
- const n = strName15(skill.name);
7472
+ for (const skill of asArray35(stack.skills)) {
7473
+ const n = strName17(skill.name);
6474
7474
  if (n) skillsByName.set(n, skill);
6475
7475
  }
6476
- const agents = asArray36(stack.agents);
7476
+ const agents = asArray35(stack.agents);
6477
7477
  for (let ai = 0; ai < agents.length; ai++) {
6478
7478
  const agent = agents[ai];
6479
- const agentName = strName15(agent.name) ?? `#${ai}`;
7479
+ const agentName = strName17(agent.name) ?? `#${ai}`;
6480
7480
  const agentSurface = surfaceOf(agent.surface);
6481
7481
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
6482
7482
  for (let si = 0; si < skillRefs.length; si++) {
6483
- const ref = strName15(skillRefs[si]);
7483
+ const ref = strName17(skillRefs[si]);
6484
7484
  if (!ref) continue;
6485
7485
  const skill = skillsByName.get(ref);
6486
7486
  if (!skill) continue;
@@ -6502,14 +7502,14 @@ function validateAiSurfaceAffinity(stack) {
6502
7502
  // src/validate-ai-tool-references.ts
6503
7503
  import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
6504
7504
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
6505
- function asArray37(v) {
7505
+ function asArray36(v) {
6506
7506
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
6507
7507
  if (v && typeof v === "object") {
6508
7508
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6509
7509
  }
6510
7510
  return [];
6511
7511
  }
6512
- function strName16(v) {
7512
+ function strName18(v) {
6513
7513
  return typeof v === "string" && v.length > 0 ? v : void 0;
6514
7514
  }
6515
7515
  function distance6(a, b) {
@@ -6550,26 +7550,26 @@ function materialisesAsTool(action) {
6550
7550
  if (!ai || typeof ai !== "object") return false;
6551
7551
  const aiRec = ai;
6552
7552
  if (aiRec.exposed !== true) return false;
6553
- if (!strName16(aiRec.description)) return false;
6554
- const type = strName16(action.type);
7553
+ if (!strName18(aiRec.description)) return false;
7554
+ const type = strName18(action.type);
6555
7555
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
6556
7556
  if (type === "script") return Boolean(action.target || action.body);
6557
7557
  return Boolean(action.target);
6558
7558
  }
6559
7559
  function collectToolUniverse(stack) {
6560
7560
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
6561
- for (const tool of asArray37(stack.tools)) {
6562
- const n = strName16(tool.name);
7561
+ for (const tool of asArray36(stack.tools)) {
7562
+ const n = strName18(tool.name);
6563
7563
  if (n) universe.add(n);
6564
7564
  }
6565
7565
  const addActionFamily = (actions) => {
6566
- for (const action of asArray37(actions)) {
6567
- const n = strName16(action.name);
7566
+ for (const action of asArray36(actions)) {
7567
+ const n = strName18(action.name);
6568
7568
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
6569
7569
  }
6570
7570
  };
6571
7571
  addActionFamily(stack.actions);
6572
- for (const obj of asArray37(stack.objects)) {
7572
+ for (const obj of asArray36(stack.objects)) {
6573
7573
  addActionFamily(obj.actions);
6574
7574
  }
6575
7575
  return universe;
@@ -6577,13 +7577,13 @@ function collectToolUniverse(stack) {
6577
7577
  function collectUnexposedActionNames(stack) {
6578
7578
  const names = /* @__PURE__ */ new Set();
6579
7579
  const scan = (actions) => {
6580
- for (const action of asArray37(actions)) {
6581
- const n = strName16(action.name);
7580
+ for (const action of asArray36(actions)) {
7581
+ const n = strName18(action.name);
6582
7582
  if (n && !materialisesAsTool(action)) names.add(n);
6583
7583
  }
6584
7584
  };
6585
7585
  scan(stack.actions);
6586
- for (const obj of asArray37(stack.objects)) scan(obj.actions);
7586
+ for (const obj of asArray36(stack.objects)) scan(obj.actions);
6587
7587
  return names;
6588
7588
  }
6589
7589
  function validateAiToolReferences(stack) {
@@ -6601,13 +7601,13 @@ function validateAiToolReferences(stack) {
6601
7601
  }
6602
7602
  return universe.has(ref);
6603
7603
  };
6604
- const skills = asArray37(stack.skills);
7604
+ const skills = asArray36(stack.skills);
6605
7605
  for (let si = 0; si < skills.length; si++) {
6606
7606
  const skill = skills[si];
6607
- const skillName = strName16(skill.name) ?? `#${si}`;
7607
+ const skillName = strName18(skill.name) ?? `#${si}`;
6608
7608
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
6609
7609
  for (let ti = 0; ti < refs.length; ti++) {
6610
- const ref = strName16(refs[ti]);
7610
+ const ref = strName18(refs[ti]);
6611
7611
  if (!ref || resolves(ref)) continue;
6612
7612
  const isPattern = ref.endsWith("*");
6613
7613
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -6626,24 +7626,25 @@ function validateAiToolReferences(stack) {
6626
7626
 
6627
7627
  // src/validate-ai-agent-authoring.ts
6628
7628
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
6629
- function asArray38(v) {
7629
+ var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
7630
+ function asArray37(v) {
6630
7631
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
6631
7632
  if (v && typeof v === "object") {
6632
7633
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6633
7634
  }
6634
7635
  return [];
6635
7636
  }
6636
- function strName17(v) {
7637
+ function strName19(v) {
6637
7638
  return typeof v === "string" && v.length > 0 ? v : void 0;
6638
7639
  }
6639
7640
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
6640
7641
  function validateAiAgentAuthoring(stack) {
6641
7642
  const findings = [];
6642
7643
  if (!stack || typeof stack !== "object") return findings;
6643
- const agents = asArray38(stack.agents);
7644
+ const agents = asArray37(stack.agents);
6644
7645
  for (let ai = 0; ai < agents.length; ai++) {
6645
7646
  const agent = agents[ai];
6646
- const name = strName17(agent.name) ?? `#${ai}`;
7647
+ const name = strName19(agent.name) ?? `#${ai}`;
6647
7648
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
6648
7649
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
6649
7650
  findings.push({
@@ -6655,24 +7656,40 @@ function validateAiAgentAuthoring(stack) {
6655
7656
  hint: isPlatformName ? `Remove the declaration; the platform owns "${name}". Extend it with skills instead.` : `Delete the agent and express its capability as skills. Everything an agent carried that a skill does not is persona text: move the useful parts of \`instructions\` into the skills' own instructions.` + (skillCount > 0 ? ` The ${skillCount} skill${skillCount === 1 ? "" : "s"} this agent references already carry the capability \u2014 they attach to the platform agent by \`surface\` affinity, so nothing is lost by dropping the persona.` : ``)
6656
7657
  });
6657
7658
  }
7659
+ const roster = [...PLATFORM_AGENT_NAMES].join(", ");
7660
+ const apps = asArray37(stack.apps);
7661
+ for (let appIdx = 0; appIdx < apps.length; appIdx++) {
7662
+ const app = apps[appIdx];
7663
+ const defaultAgent = strName19(app.defaultAgent);
7664
+ if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
7665
+ const appName = strName19(app.name) ?? `#${appIdx}`;
7666
+ findings.push({
7667
+ severity: "warning",
7668
+ rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
7669
+ where: `app "${appName}".defaultAgent`,
7670
+ path: `apps[${appIdx}].defaultAgent`,
7671
+ message: `app "${appName}" pins \`defaultAgent\` to "${defaultAgent}", which is not in the platform agent roster (${roster}). The kernel ships exactly two agents (ADR-0063 \xA72) and resolves this key against them and their legacy aliases only \u2014 an unrecognized name is not rejected, it silently falls back to the platform default at runtime, so the pin has no effect and the value drifts from what actually serves the app.`,
7672
+ hint: `Set \`defaultAgent\` to one of the platform agent names: ${roster}. If the goal is a dedicated persona or capability, express it as skills instead \u2014 they attach to "ask" / "build" by surface affinity, not as a custom \`defaultAgent\` value.`
7673
+ });
7674
+ }
6658
7675
  return findings;
6659
7676
  }
6660
7677
 
6661
7678
  // src/validate-hook-body-writes.ts
6662
- import { createRequire as createRequire4 } from "module";
6663
- import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
6664
- var cachedTs2 = null;
6665
- function loadTypeScript2() {
6666
- if (cachedTs2) return cachedTs2;
7679
+ import { createRequire as createRequire5 } from "module";
7680
+ import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
7681
+ var cachedTs3 = null;
7682
+ function loadTypeScript3() {
7683
+ if (cachedTs3) return cachedTs3;
6667
7684
  const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
6668
7685
  try {
6669
- cachedTs2 = createRequire4(anchor)("typescript");
7686
+ cachedTs3 = createRequire5(anchor)("typescript");
6670
7687
  } catch (err) {
6671
7688
  throw new Error(
6672
7689
  `@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.`
6673
7690
  );
6674
7691
  }
6675
- return cachedTs2;
7692
+ return cachedTs3;
6676
7693
  }
6677
7694
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
6678
7695
  var HOOK_BODY_WRITE_PATTERNS = [
@@ -6747,24 +7764,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
6747
7764
  "owner",
6748
7765
  "record_type"
6749
7766
  ]);
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)) {
7767
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7768
+ function asArray38(v) {
7769
+ if (Array.isArray(v)) return v.filter((x) => isRec19(x));
7770
+ if (isRec19(v)) {
6754
7771
  return Object.entries(v).map(([name, def]) => ({
6755
7772
  name,
6756
- ...isRec15(def) ? def : {}
7773
+ ...isRec19(def) ? def : {}
6757
7774
  }));
6758
7775
  }
6759
7776
  return [];
6760
7777
  }
6761
7778
  function indexObjectFields2(stack) {
6762
7779
  const out = /* @__PURE__ */ new Map();
6763
- for (const obj of asArray39(stack.objects)) {
7780
+ for (const obj of asArray38(stack.objects)) {
6764
7781
  const name = typeof obj.name === "string" ? obj.name : void 0;
6765
7782
  if (!name) continue;
6766
7783
  const names = /* @__PURE__ */ new Set();
6767
- for (const f of asArray39(obj.fields)) {
7784
+ for (const f of asArray38(obj.fields)) {
6768
7785
  if (typeof f.name === "string" && f.name) names.add(f.name);
6769
7786
  }
6770
7787
  out.set(name, names);
@@ -6783,7 +7800,7 @@ function extractHookBodyWriteSet(source) {
6783
7800
  if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
6784
7801
  return { writes: [], ctxRecordEscapes: false };
6785
7802
  }
6786
- const tsc = loadTypeScript2();
7803
+ const tsc = loadTypeScript3();
6787
7804
  const sf = tsc.createSourceFile(
6788
7805
  "hook-body.ts",
6789
7806
  `async function __body(ctx) {
@@ -6894,12 +7911,12 @@ ${source}
6894
7911
  }
6895
7912
  function validateHookBodyWrites(stack) {
6896
7913
  const findings = [];
6897
- const hooks = asArray39(stack.hooks);
7914
+ const hooks = asArray38(stack.hooks);
6898
7915
  if (hooks.length === 0) return findings;
6899
7916
  let objectFields = null;
6900
7917
  hooks.forEach((hook, hookIndex) => {
6901
7918
  const body = hook.body;
6902
- if (!isRec15(body) || body.language !== "js") return;
7919
+ if (!isRec19(body) || body.language !== "js") return;
6903
7920
  const source = body.source;
6904
7921
  if (typeof source !== "string" || source.trim() === "") return;
6905
7922
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -6956,12 +7973,12 @@ function unionCandidates(targetSets) {
6956
7973
  return [...out];
6957
7974
  }
6958
7975
  function fixHint(field, declared) {
6959
- const suggestion = formatSuggestion(findClosestMatches(field, [...declared, ...IMPLICIT_FIELDS2]));
7976
+ const suggestion = formatSuggestion2(findClosestMatches2(field, [...declared, ...IMPLICIT_FIELDS2]));
6960
7977
  return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in HOOK_BODY_WRITE_PATTERNS are checked \u2014 computed keys, spreads and aliased input are not \u2014 and this warning never blocks a build.`;
6961
7978
  }
6962
7979
 
6963
7980
  // src/validate-action-body-writes.ts
6964
- import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
7981
+ import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
6965
7982
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
6966
7983
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
6967
7984
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
@@ -6980,13 +7997,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
6980
7997
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
6981
7998
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
6982
7999
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
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)) {
8000
+ var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8001
+ function asArray39(v) {
8002
+ if (Array.isArray(v)) return v.filter((x) => isRec20(x));
8003
+ if (isRec20(v)) {
6987
8004
  return Object.entries(v).map(([name, def]) => ({
6988
8005
  name,
6989
- ...isRec16(def) ? def : {}
8006
+ ...isRec20(def) ? def : {}
6990
8007
  }));
6991
8008
  }
6992
8009
  return [];
@@ -7000,11 +8017,11 @@ function collectActionBodies(stack) {
7000
8017
  const sites = [];
7001
8018
  const seen = /* @__PURE__ */ new Set();
7002
8019
  const collect = (actions, pathPrefix, parentObject) => {
7003
- asArray40(actions).forEach((action, index) => {
8020
+ asArray39(actions).forEach((action, index) => {
7004
8021
  const type = typeof action.type === "string" ? action.type : "script";
7005
8022
  if (type !== "script") return;
7006
8023
  const body = action.body;
7007
- if (!isRec16(body) || body.language !== "js") return;
8024
+ if (!isRec20(body) || body.language !== "js") return;
7008
8025
  const source = body.source;
7009
8026
  if (typeof source !== "string" || source.trim() === "") return;
7010
8027
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -7015,7 +8032,7 @@ function collectActionBodies(stack) {
7015
8032
  });
7016
8033
  };
7017
8034
  collect(stack.actions, "actions");
7018
- asArray40(stack.objects).forEach((obj, objIndex) => {
8035
+ asArray39(stack.objects).forEach((obj, objIndex) => {
7019
8036
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
7020
8037
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
7021
8038
  });
@@ -7023,7 +8040,7 @@ function collectActionBodies(stack) {
7023
8040
  }
7024
8041
  function validateActionBodyWrites(stack) {
7025
8042
  const findings = [];
7026
- if (!isRec16(stack)) return findings;
8043
+ if (!isRec20(stack)) return findings;
7027
8044
  const sites = collectActionBodies(stack);
7028
8045
  if (sites.length === 0) return findings;
7029
8046
  let objectFields = null;
@@ -7073,22 +8090,22 @@ function validateActionBodyWrites(stack) {
7073
8090
  return findings;
7074
8091
  }
7075
8092
  function fixHint2(field, declared) {
7076
- const suggestion = formatSuggestion2(findClosestMatches2(field, [...declared, ...IMPLICIT_FIELDS2]));
8093
+ const suggestion = formatSuggestion3(findClosestMatches3(field, [...declared, ...IMPLICIT_FIELDS2]));
7077
8094
  return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in ACTION_BODY_WRITE_PATTERNS are checked \u2014 an action's ctx.input is its params bag, so it is not a record-write surface and is never resolved against fields \u2014 and this warning never blocks a build.`;
7078
8095
  }
7079
8096
 
7080
8097
  // src/validate-flow-node-writes.ts
7081
- import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
8098
+ import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec/shared";
7082
8099
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
7083
8100
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
7084
8101
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
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)) {
8102
+ var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8103
+ function asArray40(v) {
8104
+ if (Array.isArray(v)) return v.filter((x) => isRec21(x));
8105
+ if (isRec21(v)) {
7089
8106
  return Object.entries(v).map(([name, def]) => ({
7090
8107
  name,
7091
- ...isRec17(def) ? def : {}
8108
+ ...isRec21(def) ? def : {}
7092
8109
  }));
7093
8110
  }
7094
8111
  return [];
@@ -7101,8 +8118,8 @@ function readLiteralObjectName2(config) {
7101
8118
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
7102
8119
  function validateFlowNodeWrites(stack) {
7103
8120
  const findings = [];
7104
- if (!isRec17(stack)) return findings;
7105
- const flows = asArray41(stack.flows);
8121
+ if (!isRec21(stack)) return findings;
8122
+ const flows = asArray40(stack.flows);
7106
8123
  if (flows.length === 0) return findings;
7107
8124
  let objectFields = null;
7108
8125
  flows.forEach((flow, flowIndex) => {
@@ -7110,10 +8127,10 @@ function validateFlowNodeWrites(stack) {
7110
8127
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
7111
8128
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
7112
8129
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
7113
- const config = isRec17(node.config) ? node.config : void 0;
8130
+ const config = isRec21(node.config) ? node.config : void 0;
7114
8131
  if (!config) return;
7115
8132
  const fields = config.fields;
7116
- if (!isRec17(fields)) return;
8133
+ if (!isRec21(fields)) return;
7117
8134
  const written = Object.keys(fields);
7118
8135
  if (written.length === 0) return;
7119
8136
  const objectName = readLiteralObjectName2(config);
@@ -7140,7 +8157,7 @@ function validateFlowNodeWrites(stack) {
7140
8157
  return findings;
7141
8158
  }
7142
8159
  function fixHint3(field, declared) {
7143
- const suggestion = formatSuggestion3(findClosestMatches3(field, [...declared, ...IMPLICIT_FIELDS2]));
8160
+ const suggestion = formatSuggestion4(findClosestMatches4(field, [...declared, ...IMPLICIT_FIELDS2]));
7144
8161
  return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. This gates the build rather than warning: the key is literal and so is the object, so unlike the hook/action body rules there is nothing here that could have been mis-extracted.`;
7145
8162
  }
7146
8163
 
@@ -7258,7 +8275,8 @@ import {
7258
8275
  APPROVAL_REVISE_NODE_TYPE,
7259
8276
  collectFlowGraphs as collectFlowGraphs2
7260
8277
  } from "@objectstack/spec/automation";
7261
- function asArray42(v) {
8278
+ import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
8279
+ function asArray41(v) {
7262
8280
  if (Array.isArray(v)) return v;
7263
8281
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7264
8282
  return [];
@@ -7532,7 +8550,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
7532
8550
  function filterCarriesNoCondition(filter) {
7533
8551
  if (filter === void 0 || filter === null) return true;
7534
8552
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
7535
- return Object.keys(filter).length === 0;
8553
+ return reduceFilterVerdict2(filter) === "true";
8554
+ }
8555
+ function describeUnboundedFilter(filter) {
8556
+ if (filter === void 0 || filter === null) return "no `filter` key";
8557
+ if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
8558
+ return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
8559
+ }
8560
+ function previewFilter(filter) {
8561
+ try {
8562
+ const json = JSON.stringify(filter);
8563
+ if (typeof json !== "string") return typeof filter;
8564
+ return json.length > 80 ? `${json.slice(0, 77)}...` : json;
8565
+ } catch {
8566
+ return typeof filter;
8567
+ }
7536
8568
  }
7537
8569
  function scanUnboundedBulkWrites(at, nodes, findings) {
7538
8570
  for (const node of nodes) {
@@ -7543,10 +8575,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
7543
8575
  if (cfg.multi !== true) continue;
7544
8576
  if (!filterCarriesNoCondition(cfg.filter)) continue;
7545
8577
  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`";
8578
+ const filterState = describeUnboundedFilter(cfg.filter);
7547
8579
  findings.push({
7548
8580
  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.`,
8581
+ 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 the filter as \`where\` (an absent key becomes \`{}\`) plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` bounded by nothing \u2014 a filter that reduces to TRUE constrains no row. 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
8582
  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
8583
  // Warning, not `error`: see the severity policy at the top of this file.
7552
8584
  // The shape has a legitimate reading the engine grants on purpose, so it is
@@ -7628,7 +8660,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
7628
8660
  }
7629
8661
  function lintFlowPatterns(stack) {
7630
8662
  const findings = [];
7631
- for (const flow of asArray42(stack.flows)) {
8663
+ for (const flow of asArray41(stack.flows)) {
7632
8664
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
7633
8665
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
7634
8666
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
@@ -7720,19 +8752,19 @@ function lintFlowPatterns(stack) {
7720
8752
  }
7721
8753
 
7722
8754
  // src/lint-liveness-properties.ts
7723
- import { createRequire as createRequire5 } from "module";
8755
+ import { createRequire as createRequire6 } from "module";
7724
8756
  import { dirname, join } from "path";
7725
8757
  import { existsSync, readFileSync } from "fs";
7726
8758
  var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
7727
8759
  var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
7728
- function asArray43(v) {
8760
+ function asArray42(v) {
7729
8761
  if (Array.isArray(v)) return v;
7730
8762
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7731
8763
  return [];
7732
8764
  }
7733
8765
  function resolveLivenessDir() {
7734
8766
  try {
7735
- const require2 = createRequire5(import.meta.url);
8767
+ const require2 = createRequire6(import.meta.url);
7736
8768
  const pkgJson = require2.resolve("@objectstack/spec/package.json");
7737
8769
  const dir = join(dirname(pkgJson), "liveness");
7738
8770
  return existsSync(dir) ? dir : null;
@@ -7847,6 +8879,13 @@ var TYPE_COLLECTIONS = [
7847
8879
  // checks every widget on the dashboard. Registering it here is not optional
7848
8880
  // bookkeeping: without it the ledger would be newly correct and newly
7849
8881
  // silent, which is the shape this lint exists to prevent.
8882
+ //
8883
+ // As of #6774 the dashboard ledger warns on NOTHING — four of those five were
8884
+ // retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
8885
+ // gave it a renderer. The type STAYS listed, the resolved state `webhook` and
8886
+ // `email_template` already sit in: a zero-warn entry costs one empty map
8887
+ // lookup, and it means a future regression that re-deadens a widget key warns
8888
+ // on its own instead of waiting for someone to notice this list again.
7850
8889
  { type: "dashboard", key: "dashboards" }
7851
8890
  ];
7852
8891
  function lintLivenessProperties(stack) {
@@ -7855,11 +8894,11 @@ function lintLivenessProperties(stack) {
7855
8894
  const findings = [];
7856
8895
  const objectWarn = loadWarnMap(dir, "object");
7857
8896
  const fieldWarn = loadWarnMap(dir, "field");
7858
- for (const obj of asArray43(stack.objects)) {
8897
+ for (const obj of asArray42(stack.objects)) {
7859
8898
  const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7860
8899
  if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
7861
8900
  if (fieldWarn.size > 0) {
7862
- for (const field of asArray43(obj.fields)) {
8901
+ for (const field of asArray42(obj.fields)) {
7863
8902
  const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
7864
8903
  checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
7865
8904
  }
@@ -7868,7 +8907,7 @@ function lintLivenessProperties(stack) {
7868
8907
  for (const { type, key } of TYPE_COLLECTIONS) {
7869
8908
  const warnMap = loadWarnMap(dir, type);
7870
8909
  if (warnMap.size === 0) continue;
7871
- for (const item of asArray43(stack[key])) {
8910
+ for (const item of asArray42(stack[key])) {
7872
8911
  const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
7873
8912
  checkItem(type, item, `${type} '${name}'`, warnMap, findings);
7874
8913
  }
@@ -7882,7 +8921,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7882
8921
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7883
8922
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
7884
8923
  var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
7885
- function asArray44(v) {
8924
+ function asArray43(v) {
7886
8925
  if (Array.isArray(v)) return v;
7887
8926
  if (v && typeof v === "object") {
7888
8927
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7891,9 +8930,9 @@ function asArray44(v) {
7891
8930
  }
7892
8931
  function lintAutonumberFormats(stack) {
7893
8932
  const findings = [];
7894
- for (const obj of asArray44(stack.objects)) {
8933
+ for (const obj of asArray43(stack.objects)) {
7895
8934
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7896
- const fields = asArray44(obj.fields);
8935
+ const fields = asArray43(obj.fields);
7897
8936
  const fieldMeta = /* @__PURE__ */ new Map();
7898
8937
  for (const f of fields) {
7899
8938
  if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
@@ -7959,7 +8998,7 @@ function lintAutonumberFormats(stack) {
7959
8998
 
7960
8999
  // src/lint-view-refs.ts
7961
9000
  import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
7962
- function asArray45(v) {
9001
+ function asArray44(v) {
7963
9002
  if (Array.isArray(v)) return v;
7964
9003
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7965
9004
  return [];
@@ -7987,7 +9026,7 @@ function lintViewRefs(stack) {
7987
9026
  s.add(kind);
7988
9027
  };
7989
9028
  const containers = [];
7990
- for (const v of asArray45(stack.views)) {
9029
+ for (const v of asArray44(stack.views)) {
7991
9030
  if (v.viewKind) {
7992
9031
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7993
9032
  continue;
@@ -7996,7 +9035,7 @@ function lintViewRefs(stack) {
7996
9035
  const object = viewContainerObjectName(v);
7997
9036
  if (object) containers.push({ object, container: v });
7998
9037
  }
7999
- for (const obj of asArray45(stack.objects)) {
9038
+ for (const obj of asArray44(stack.objects)) {
8000
9039
  const object = typeof obj.name === "string" ? obj.name : void 0;
8001
9040
  if (!object) continue;
8002
9041
  if (obj.list || obj.form || obj.listViews || obj.formViews) {
@@ -8050,11 +9089,11 @@ function lintViewRefs(stack) {
8050
9089
  });
8051
9090
  }
8052
9091
  };
8053
- for (const obj of asArray45(stack.objects)) {
9092
+ for (const obj of asArray44(stack.objects)) {
8054
9093
  const object = typeof obj.name === "string" ? obj.name : void 0;
8055
- for (const action of asArray45(obj.actions)) checkAction(action, object);
9094
+ for (const action of asArray44(obj.actions)) checkAction(action, object);
8056
9095
  }
8057
- for (const action of asArray45(stack.actions)) checkAction(action);
9096
+ for (const action of asArray44(stack.actions)) checkAction(action);
8058
9097
  return findings;
8059
9098
  }
8060
9099
 
@@ -8242,13 +9281,14 @@ function lintDataModel(objects) {
8242
9281
  if (!obj?.name) continue;
8243
9282
  const objPath = `objects[${i}]`;
8244
9283
  const fields = fieldEntries2(obj.fields);
8245
- const hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
9284
+ const hasNameField = !!obj.nameField || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
8246
9285
  if (fields.length > 0 && !hasNameField) {
8247
9286
  issues.push({
8248
9287
  severity: "suggestion",
8249
9288
  rule: "object/missing-name-field",
8250
- message: `Object "${obj.name}" has no name/title field or primaryField \u2014 records will display as raw IDs`,
8251
- path: `${objPath}.fields`
9289
+ message: `Object "${obj.name}" has no nameField and no name-like field \u2014 records will display as raw IDs`,
9290
+ path: `${objPath}.fields`,
9291
+ fix: `Set \`nameField: '<field>'\` \u2014 ADR-0079's canonical primary-title pointer \u2014 to a stored text/autonumber field, or to a formula field with \`returnType: 'text'\` for a composite title. A \`titleFormat\` template does NOT count: it is retired (ADR-0079) and render-only, so the server can neither return nor query the title it renders.`
8252
9292
  });
8253
9293
  }
8254
9294
  for (const { name: fieldName, def } of fields) {
@@ -8357,6 +9397,7 @@ var CLI_ONLY = ["cli"];
8357
9397
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
8358
9398
  var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
8359
9399
  var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
9400
+ var RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY = "Deliberate, and not a snapshot limitation: this rule needs only the written item, but every other rule on the `views[]` visibility-predicate surface (validate-visibility-predicates.ts) is CLI-only. Gating one of three sibling verdicts about the same predicate at the Studio door is less predictable than gating none; move the family together, as one measured edit.";
8360
9401
  var RUNTIME_OBJECT_WRITES_P2 = "P2 (#4463): judges an object/field declaration. Object writes are the hottest metadata path in the product, so P1 gates `flow` first and widens once the gate has real traffic behind it.";
8361
9402
  var EXPRESSION_INVALID = "expression-invalid";
8362
9403
  var AUTHORING_RULES = [
@@ -8386,8 +9427,12 @@ var AUTHORING_RULES = [
8386
9427
  }))
8387
9428
  },
8388
9429
  // ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
8389
- // mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
8390
- // read the pre-parse tier or the evidence is already gone.
9430
+ // mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
9431
+ // is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
9432
+ // refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
9433
+ // THROWS on both. `normalized` here therefore means "needs no parsed stack"
9434
+ // (so `os lint`, which never parses, can run it), not "sees evidence the
9435
+ // parse would have eaten".
8391
9436
  {
8392
9437
  name: "validateListViewMode",
8393
9438
  tier: "gating",
@@ -8418,9 +9463,13 @@ var AUTHORING_RULES = [
8418
9463
  surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8419
9464
  run: (stack) => validateFunctionalCompleteness(stack)
8420
9465
  },
8421
- // A flat list-view object in `views: []` parses to an EMPTY container
8422
- // (ViewSchema strips unknown keys): the schema step passes, zero views
8423
- // register, and the Console renders nothing. Pre-parse for the same reason.
9466
+ // A view container in `views: []` that registers zero views: nothing appears
9467
+ // in the Console, and the schema step cannot tell it from an intentionally
9468
+ // empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
9469
+ // went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
9470
+ // data }` by name with the wrap-it hint (measured under #6073); the arm that
9471
+ // still needs a rule is the all-slots-empty container, whose keys are all
9472
+ // declared and which survives the parse untouched.
8424
9473
  {
8425
9474
  name: "validateViewContainers",
8426
9475
  tier: "gating",
@@ -8470,6 +9519,31 @@ var AUTHORING_RULES = [
8470
9519
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8471
9520
  run: (stack) => validateFilterTokens(stack)
8472
9521
  },
9522
+ // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
9523
+ // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
9524
+ // this rule does not touch it: it refuses the literal SPELLINGS at authoring
9525
+ // time with a per-shape prescription, which is Prime Directive #12's standard
9526
+ // shape (reject at the producer, never tolerate at the consumer) and #5240's
9527
+ // same-direction precedent one shape over.
9528
+ {
9529
+ name: "validateEmptyCombinators",
9530
+ tier: "gating",
9531
+ input: "parsed",
9532
+ commands: ALL,
9533
+ source: "packages/lint/src/validate-empty-combinators.ts",
9534
+ // The one type #4463's P1 slice opened, and the one this rule most needs:
9535
+ // a flow CRUD node's `config.filter` is where an empty combinator has the
9536
+ // largest blast radius, and the write path is the only door an AI author
9537
+ // uses. This rule needs NO resolution context at all — it judges the filter
9538
+ // literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
9539
+ // it, and widening to the other filter-carrying types (`object`, `view`,
9540
+ // `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
9541
+ // opens them at the gate. Making that call here would widen the gate's
9542
+ // dispatch surface on this rule's authority, which is P2's decision.
9543
+ surfaces: CLI_AND_RUNTIME,
9544
+ runtimeTypes: ["flow"],
9545
+ run: (stack) => validateEmptyCombinators(stack)
9546
+ },
8473
9547
  // The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
8474
9548
  // rules that answer "does this name resolve to anything?". It reached all
8475
9549
  // three commands before this file existed; it is an entry here so the two
@@ -8518,6 +9592,16 @@ var AUTHORING_RULES = [
8518
9592
  // `displayField` (#5775) — so gating today would fail the platform's own pages
8519
9593
  // to enforce declarations the platform does not keep. The error upgrade is a
8520
9594
  // separate step, once the warning-period inventory is empty.
9595
+ //
9596
+ // #5775 settled the record picker's half: `displayField` is retired in favour
9597
+ // of the `labelField` the renderer actually reads. Its claim that "the rest of
9598
+ // the keys the renderers honour are declared" did NOT hold — #6776 found five
9599
+ // more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
9600
+ // `page:accordion.variant`, and the tab strip's visual style, whose declared
9601
+ // spelling `page:tabs.type` collided with the component node's own dispatch
9602
+ // key and so was unauthorable in the flat and JSX carriers). All five are
9603
+ // declared as of #6776, the last as the renamed `tabStyle`. What remains
9604
+ // before the error upgrade is #5728 and two page rewrites.
8521
9605
  {
8522
9606
  name: "validateComponentProps",
8523
9607
  tier: "advisory",
@@ -8596,10 +9680,12 @@ var AUTHORING_RULES = [
8596
9680
  //
8597
9681
  // `gating` since #5762, which reviewed the file's rules as one family and
8598
9682
  // 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`
9683
+ // dead? Four rules answer yes and emit `error` — a `config.timeRelative`
8600
9684
  // 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
9685
+ // predicate cannot route at all, a `record-*` triggerType outside the
9686
+ // closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
9687
+ // `type: 'record_change'` flow whose triggerType the engine's binding resolver
9688
+ // routes nowhere, silently demoting it to a manual flow. None of those verdicts
8603
9689
  // can be changed by installing a package, so there is no reading under which
8604
9690
  // the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
8605
9691
  // (the object may come from another installed package — a hedge this rule
@@ -8726,12 +9812,39 @@ var AUTHORING_RULES = [
8726
9812
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8727
9813
  run: (stack) => validateSeedStateMachine(stack)
8728
9814
  },
8729
- // ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
8730
- // Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
8731
- // during parse, so the alias the author wrote is gone from `result.data`.
9815
+ // ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
9816
+ // gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
9817
+ // schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
9818
+ // the alias the author wrote is gone from `result.data`". Measured false at
9819
+ // #6073: the ADR-0087 D2 conversions do that fold INSIDE
9820
+ // `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
9821
+ // alias site the alias-KEY rule reported zero here too.
9822
+ //
9823
+ // #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
9824
+ // re-anchored. Re-anchoring would have had to move this entry's input to a
9825
+ // pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
9826
+ // a change to this package's external input contract, and the maintainer's
9827
+ // call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
9828
+ // costs no author a signal: the same D2 conversion already shouts through
9829
+ // `warnConversionNotice` in `defineStack`, naming the site, the conversion and
9830
+ // the protocol-16 retirement window — better wording than the rule ever had.
9831
+ //
9832
+ // Every rule left in the family judges the predicate's VALUE, and the value
9833
+ // moves into `visibleWhen` intact, so all three report normally on this tier.
9834
+ // The tier therefore stays `normalized` on its SURVIVING justification (a
9835
+ // finding still reaches the author when an unrelated schema error would stop
9836
+ // the parse — see `AuthoringRuleInputTier`), never on the retired
9837
+ // "pre-parse evidence" one.
9838
+ //
9839
+ // `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
9840
+ // ADR-0089 rules stay advisory findings within it — the tier is a property of
9841
+ // the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
9842
+ // what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
9843
+ // has worked since #3760. The promotion follows the #5762 precedent: a family
9844
+ // that gains an `error` finding moves its registry tier in the same edit.
8732
9845
  {
8733
9846
  name: "validateVisibilityPredicates",
8734
- tier: "advisory",
9847
+ tier: "gating",
8735
9848
  input: "normalized",
8736
9849
  commands: ALL,
8737
9850
  source: "packages/lint/src/validate-visibility-predicates.ts",
@@ -8739,6 +9852,30 @@ var AUTHORING_RULES = [
8739
9852
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8740
9853
  run: (stack) => validateVisibilityPredicates(stack)
8741
9854
  },
9855
+ // #7010 — the same predicate surface, one question further in. The three
9856
+ // ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
9857
+ // rooted, is the root right for the layer) and never open the target schema,
9858
+ // so `data.tpye == 'formula'` passes all three and still resolves to nothing.
9859
+ // This rule resolves the PATH against the schema the form edits — the closed
9860
+ // `getMetadataTypeSchema` key set — and is therefore immune to the CEL
9861
+ // type-name blind spot that made #6248's gate structurally unable to catch
9862
+ // #6254's 16 bare `type ==` predicates.
9863
+ //
9864
+ // Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
9865
+ // the `record.*` layer is deliberately out of scope because an ObjectQL
9866
+ // object's addressable path set is NOT closed (lookup traversal, system
9867
+ // columns, formula outputs), and an `error` gate over an open set generates
9868
+ // false build errors. See the rule's module note.
9869
+ {
9870
+ name: "validatePredicatePathRefs",
9871
+ tier: "gating",
9872
+ input: "normalized",
9873
+ commands: ALL,
9874
+ source: "packages/lint/src/validate-predicate-path-refs.ts",
9875
+ surfaces: CLI_ONLY,
9876
+ surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
9877
+ run: (stack) => validatePredicatePathRefs(stack)
9878
+ },
8742
9879
  // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
8743
9880
  // `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
8744
9881
  // runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
@@ -9134,10 +10271,13 @@ export {
9134
10271
  DASHBOARD_ACTION_ROUTE_UNRESOLVED,
9135
10272
  DASHBOARD_ACTION_TARGET_UNDEFINED,
9136
10273
  DASHBOARD_FILTER_FIELD_UNKNOWN,
10274
+ DEFAULT_AGENT_OUTSIDE_ROSTER,
9137
10275
  EXPRESSION_INVALID,
9138
10276
  FIELD_GROUP_EMPTY,
9139
10277
  FIELD_GROUP_SHADOWED,
9140
10278
  FIELD_GROUP_UNDECLARED,
10279
+ FILTER_EMPTY_COMBINATOR,
10280
+ FILTER_EMPTY_NODE,
9141
10281
  FILTER_TOKEN_UNKNOWN,
9142
10282
  FLOW_APPROVAL_REVISE_DEAD_END,
9143
10283
  FLOW_APPROVAL_REVISE_DISABLED,
@@ -9164,6 +10304,7 @@ export {
9164
10304
  FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
9165
10305
  FLOW_TRIGGER_UNKNOWN_EVENT,
9166
10306
  FLOW_TRIGGER_UNKNOWN_OBJECT,
10307
+ FLOW_TRIGGER_UNROUTABLE,
9167
10308
  FLOW_UPDATE_READONLY_FIELD,
9168
10309
  FLOW_UPDATE_READONLY_WHEN_FIELD,
9169
10310
  FLOW_WRITE_NODE_TYPES,
@@ -9184,19 +10325,25 @@ export {
9184
10325
  NULL_GUARD_HINT,
9185
10326
  OBJECT_REFERENCE_UNKNOWN,
9186
10327
  OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
10328
+ OPEN_VOCABULARY_PROBES,
9187
10329
  ORG_AXIS_CROSS_ORG_BU_GRANT,
9188
10330
  ORG_AXIS_PERMISSION_INHERITANCE,
9189
10331
  PAGE_FIELD_UNKNOWN,
9190
10332
  PAGE_SOURCE_CLASSNAME,
10333
+ PREDICATE_PATH_UNRESOLVED,
10334
+ PREDICATE_PATH_UNROOTED,
10335
+ PRE_SEAL_PHASES,
9191
10336
  REACT_BLOCK_NEEDS_RECORD_CONTEXT,
9192
10337
  REACT_CHART_AGGREGATE_INVALID,
9193
10338
  REACT_CHART_AXIS_UNKNOWN,
9194
10339
  REACT_CHART_DRILLDOWN_INVALID,
9195
10340
  REACT_CHART_FIELD_UNKNOWN,
9196
10341
  REFERENCE_INTEGRITY_RULES,
10342
+ RLS_PREDICATE_OVER_BUDGET,
9197
10343
  RLS_PREDICATE_UNENFORCEABLE,
9198
10344
  RLS_PREDICATE_UNPARSEABLE,
9199
10345
  RUNTIME_AJV_OPTIONS,
10346
+ SEAL_MARKERS,
9200
10347
  SEARCHABLE_FIELD_UNKNOWN,
9201
10348
  SEARCHABLE_FIELD_UNSEARCHABLE,
9202
10349
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
@@ -9216,6 +10363,9 @@ export {
9216
10363
  SEMANTIC_ROLE_FIELD_UNKNOWN,
9217
10364
  SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
9218
10365
  SHARING_RULE_UNLOWERABLE_CONDITION,
10366
+ STARTUP_OPEN_VOCABULARY_VERDICT,
10367
+ STARTUP_VERDICT_ASSERTIVE_WORDING,
10368
+ STARTUP_VERDICT_HINT,
9219
10369
  STYLE_CLASSNAME_TAILWIND,
9220
10370
  STYLE_NODE_MISSING_ID,
9221
10371
  STYLE_RESPONSIVE_NO_BASE,
@@ -9237,7 +10387,9 @@ export {
9237
10387
  VIEW_KEY_COLLISION,
9238
10388
  VIEW_REF_FORM_TARGET_KIND,
9239
10389
  VIEW_REF_FORM_TARGET_MISSING,
9240
- VISIBILITY_ALIAS_DEPRECATED,
10390
+ VISIBILITY_BARE_IDENTIFIER,
10391
+ VISIBILITY_PREDICATE_OVER_BUDGET,
10392
+ VISIBILITY_PREDICATE_SYNTAX,
9241
10393
  VISIBILITY_ROOT_MISLAYERED,
9242
10394
  WIDGET_DATASET_UNKNOWN,
9243
10395
  WIDGET_DIMENSION_UNKNOWN,
@@ -9249,6 +10401,7 @@ export {
9249
10401
  diffAccessMatrix,
9250
10402
  extractHookBodyWriteSet,
9251
10403
  extractHookBodyWrites,
10404
+ findStartupRegistryVerdicts,
9252
10405
  findUnguardedNullableOperands,
9253
10406
  isSourceAuthoredPage,
9254
10407
  lintAutonumberFormats,
@@ -9278,6 +10431,7 @@ export {
9278
10431
  validateChartBindings,
9279
10432
  validateComponentProps,
9280
10433
  validateDashboardActionRefs,
10434
+ validateEmptyCombinators,
9281
10435
  validateFilterTokens,
9282
10436
  validateFlowNodeWrites,
9283
10437
  validateFlowTemplatePaths,
@@ -9293,6 +10447,7 @@ export {
9293
10447
  validateOrgAxisRedLines,
9294
10448
  validatePageFieldBindings,
9295
10449
  validatePageSourceStyling,
10450
+ validatePredicatePathRefs,
9296
10451
  validateReactPageProps,
9297
10452
  validateReactPages,
9298
10453
  validateReadonlyFlowWrites,