@objectstack/lint 17.0.0 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  ACTION_BODY_WRITE_PATTERNS: () => ACTION_BODY_WRITE_PATTERNS,
25
25
  ACTION_BODY_WRITE_PATTERN_IDS: () => ACTION_BODY_WRITE_PATTERN_IDS,
26
26
  ACTION_BODY_WRITE_UNKNOWN_FIELD: () => ACTION_BODY_WRITE_UNKNOWN_FIELD,
27
+ ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR: () => ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
27
28
  ACTION_NAME_UNDEFINED: () => ACTION_NAME_UNDEFINED,
28
29
  ACTION_NO_PLACEMENT: () => ACTION_NO_PLACEMENT,
29
30
  ACTION_RECORD_WRITE_DISCARDED: () => ACTION_RECORD_WRITE_DISCARDED,
@@ -69,6 +70,7 @@ __export(index_exports, {
69
70
  FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
70
71
  FILTER_EMPTY_COMBINATOR: () => FILTER_EMPTY_COMBINATOR,
71
72
  FILTER_EMPTY_NODE: () => FILTER_EMPTY_NODE,
73
+ FILTER_PRESET_COMPARAND: () => FILTER_PRESET_COMPARAND,
72
74
  FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
73
75
  FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
74
76
  FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
@@ -86,6 +88,7 @@ __export(index_exports, {
86
88
  FLOW_MULTIPLE_DEFAULT_EDGES: () => FLOW_MULTIPLE_DEFAULT_EDGES,
87
89
  FLOW_MULTI_WRITE_UNFILTERED: () => FLOW_MULTI_WRITE_UNFILTERED,
88
90
  FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
91
+ FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR: () => FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
89
92
  FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
90
93
  FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
91
94
  FLOW_TEMPLATE_FIELD_UNPROVISIONED: () => FLOW_TEMPLATE_FIELD_UNPROVISIONED,
@@ -107,6 +110,7 @@ __export(index_exports, {
107
110
  HOOK_BODY_WRITE_PATTERNS: () => HOOK_BODY_WRITE_PATTERNS,
108
111
  HOOK_BODY_WRITE_PATTERN_IDS: () => HOOK_BODY_WRITE_PATTERN_IDS,
109
112
  HOOK_BODY_WRITE_UNKNOWN_FIELD: () => HOOK_BODY_WRITE_UNKNOWN_FIELD,
113
+ HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR: () => HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
110
114
  LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
111
115
  LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
112
116
  LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
@@ -142,6 +146,7 @@ __export(index_exports, {
142
146
  RUNTIME_AJV_OPTIONS: () => RUNTIME_AJV_OPTIONS,
143
147
  SEAL_MARKERS: () => SEAL_MARKERS,
144
148
  SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
149
+ SEARCHABLE_FIELD_UNPROVISIONED: () => SEARCHABLE_FIELD_UNPROVISIONED,
145
150
  SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
146
151
  SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
147
152
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
@@ -160,8 +165,12 @@ __export(index_exports, {
160
165
  SEED_VALUE_OUTSIDE_STATE_MACHINE: () => SEED_VALUE_OUTSIDE_STATE_MACHINE,
161
166
  SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
162
167
  SEMANTIC_ROLE_FIELD_UNPROVISIONED: () => SEMANTIC_ROLE_FIELD_UNPROVISIONED,
168
+ SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT: () => SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
169
+ SHARING_RULE_OBJECT_NOT_SHAREABLE: () => SHARING_RULE_OBJECT_NOT_SHAREABLE,
163
170
  SHARING_RULE_RUNTIME_VARIABLE_CONDITION: () => SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
164
171
  SHARING_RULE_UNLOWERABLE_CONDITION: () => SHARING_RULE_UNLOWERABLE_CONDITION,
172
+ SORT_FIELD_UNKNOWN: () => SORT_FIELD_UNKNOWN,
173
+ SORT_FIELD_UNSORTABLE: () => SORT_FIELD_UNSORTABLE,
165
174
  STARTUP_OPEN_VOCABULARY_VERDICT: () => STARTUP_OPEN_VOCABULARY_VERDICT,
166
175
  STARTUP_VERDICT_ASSERTIVE_WORDING: () => STARTUP_VERDICT_ASSERTIVE_WORDING,
167
176
  STARTUP_VERDICT_HINT: () => STARTUP_VERDICT_HINT,
@@ -198,6 +207,7 @@ __export(index_exports, {
198
207
  authoringRulesFor: () => authoringRulesFor,
199
208
  buildAccessMatrix: () => buildAccessMatrix,
200
209
  buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
210
+ checkSortDeclaration: () => checkSortDeclaration,
201
211
  diffAccessMatrix: () => diffAccessMatrix,
202
212
  extractHookBodyWriteSet: () => extractHookBodyWriteSet,
203
213
  extractHookBodyWrites: () => extractHookBodyWrites,
@@ -212,6 +222,7 @@ __export(index_exports, {
212
222
  lintUniqueDeclarations: () => lintUniqueDeclarations,
213
223
  lintUnscopedDeclaredIndexes: () => lintUnscopedDeclaredIndexes,
214
224
  lintViewRefs: () => lintViewRefs,
225
+ narrowObjectsToPackageClosure: () => narrowObjectsToPackageClosure,
215
226
  nearestRegisteredFormat: () => nearestRegisteredFormat,
216
227
  nullGuardMessage: () => nullGuardMessage,
217
228
  runAuthoringRules: () => runAuthoringRules,
@@ -250,6 +261,7 @@ __export(index_exports, {
250
261
  validatePageFieldBindings: () => validatePageFieldBindings,
251
262
  validatePageSourceStyling: () => validatePageSourceStyling,
252
263
  validatePredicatePathRefs: () => validatePredicatePathRefs,
264
+ validatePresetComparands: () => validatePresetComparands,
253
265
  validateReactPageProps: () => validateReactPageProps,
254
266
  validateReactPages: () => validateReactPages,
255
267
  validateReadonlyFlowWrites: () => validateReadonlyFlowWrites,
@@ -266,6 +278,7 @@ __export(index_exports, {
266
278
  validateSeedStateMachine: () => validateSeedStateMachine,
267
279
  validateSemanticRoles: () => validateSemanticRoles,
268
280
  validateSharingRuleEnforceability: () => validateSharingRuleEnforceability,
281
+ validateSortableFields: () => validateSortableFields,
269
282
  validateStackExpressions: () => validateStackExpressions,
270
283
  validateTranslatableSections: () => validateTranslatableSections,
271
284
  validateTranslationReferences: () => validateTranslationReferences,
@@ -860,10 +873,10 @@ var OUTCOME_CLAUSE = {
860
873
  "fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
861
874
  "fail-open": "so the predicate is SKIPPED fail-open \u2014 the field is never actually required, the write proceeds unchecked, and the only trace is a `requiredWhen \u2026 failed to evaluate \u2014 skipped` log line (#4649/#4811)"
862
875
  };
863
- function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
876
+ function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
864
877
  const owner = objectName ? `'${objectName}'` : "this object";
865
- const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
866
- return `${subject} applies \`${finding.operator}\` to \`${finding.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
878
+ const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
879
+ return `${subject} applies \`${finding2.operator}\` to \`${finding2.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding2.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
867
880
  }
868
881
 
869
882
  // src/validate-expressions.ts
@@ -1046,10 +1059,10 @@ function validateStackExpressions(stack) {
1046
1059
  if (!nullableFields || nullableFields.size === 0) return;
1047
1060
  const source = celSourceOf(raw);
1048
1061
  if (!source) return;
1049
- for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
1062
+ for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
1050
1063
  issues.push({
1051
1064
  where,
1052
- message: nullGuardMessage(subject, objectName, finding, outcome),
1065
+ message: nullGuardMessage(subject, objectName, finding2, outcome),
1053
1066
  source,
1054
1067
  severity: "error"
1055
1068
  });
@@ -1073,7 +1086,7 @@ function validateStackExpressions(stack) {
1073
1086
  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";
1074
1087
  const FIELD_RULE_SLOT_CONSEQUENCE = {
1075
1088
  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)",
1076
- 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",
1089
+ 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`). The server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
1077
1090
  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",
1078
1091
  // Listed rather than left to the `??` below, so the map covers every slot
1079
1092
  // the field walk passes and the default stays unreachable. `FieldSchema`
@@ -1132,14 +1145,15 @@ function validateStackExpressions(stack) {
1132
1145
  if (retired.length > 0) {
1133
1146
  issues.push({
1134
1147
  where: `${at} \xB7 node '${node.id}' (script) callable`,
1135
- 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
1136
- // behaviour, never the retired key's fate — "rewrite it" reads two ways
1137
- // over a branch that DELETES the key (template/recipients/variables/script),
1138
- // "rewrite existing sources" only one. Plain-quoted (not a template literal)
1148
+ 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), reworded under #9529: the house
1149
+ // sentence names the TOOL's behaviour, never the retired key's fate —
1150
+ // "rewrite it" read two ways over a branch that DELETES the key
1151
+ // (template/recipients/variables/script), and the tool never rewrote a
1152
+ // source file at all. Plain-quoted (not a template literal)
1139
1153
  // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
1140
1154
  // widened scan (#7030) on the same textual shape as the spec corpus — no
1141
1155
  // interpolation lives in this clause, so nothing is lost switching quote style.
1142
- "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
1156
+ "Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
1143
1157
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
1144
1158
  });
1145
1159
  } else if (!fn) {
@@ -1210,10 +1224,10 @@ function validateStackExpressions(stack) {
1210
1224
  f.expression,
1211
1225
  objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
1212
1226
  );
1213
- const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
1214
- for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
1215
- for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
1216
- warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
1227
+ const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
1228
+ for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
1229
+ for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
1230
+ warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
1217
1231
  }
1218
1232
  }
1219
1233
  }
@@ -2835,6 +2849,7 @@ var import_data5 = require("@objectstack/spec/data");
2835
2849
  var import_data4 = require("@objectstack/spec/data");
2836
2850
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
2837
2851
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
2852
+ var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
2838
2853
  function asArray10(v) {
2839
2854
  if (Array.isArray(v)) return v;
2840
2855
  if (v && typeof v === "object") {
@@ -2925,7 +2940,7 @@ function indexObjectSearchTargets(stack) {
2925
2940
  }
2926
2941
  return fieldsByObject;
2927
2942
  }
2928
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
2943
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
2929
2944
  const findings = [];
2930
2945
  if (!Array.isArray(declared) || declared.length === 0) return findings;
2931
2946
  if (!objectName) return findings;
@@ -2933,6 +2948,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2933
2948
  const target = fieldsByObject.get(objectName);
2934
2949
  if (!target) return findings;
2935
2950
  const known = target.names;
2951
+ const anchors = unprovisionedAnchors?.get(objectName);
2936
2952
  const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
2937
2953
  for (let i = 0; i < declared.length; i++) {
2938
2954
  const entry = declared[i];
@@ -2950,6 +2966,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2950
2966
  });
2951
2967
  continue;
2952
2968
  }
2969
+ if (anchors?.has(name)) {
2970
+ findings.push({
2971
+ severity: "warning",
2972
+ rule: SEARCHABLE_FIELD_UNPROVISIONED,
2973
+ where,
2974
+ path: `${path}[${i}]`,
2975
+ message: `${subject} entry "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)}` + (role === "narrowing" ? ` \u2014 clients echo this declaration verbatim as the '$searchFields' override, so every toolbar search on this list scans a column that is empty on every record: it reads as search coverage and matches nothing.` : ` \u2014 'search' scans it on every record and it can never match, so the object's searchable set is narrower than it declares. Should it be the ONLY entry that resolves, the set scans nothing at all.`),
2976
+ hint: unprovisionedAnchorHint(objectName, name)
2977
+ });
2978
+ }
2953
2979
  if ((0, import_data4.isVirtualSearchField)(target.fields[name])) {
2954
2980
  const vtype = target.fields[name]?.type;
2955
2981
  findings.push({
@@ -3003,9 +3029,19 @@ function validateSearchableFields(stack) {
3003
3029
  if (!isRec4(stack)) return findings;
3004
3030
  const objects = asArray10(stack.objects);
3005
3031
  const fieldsByObject = indexObjectSearchTargets(stack);
3032
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
3006
3033
  const check = (declared, objectName, where, path, subject, role) => {
3007
3034
  findings.push(
3008
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
3035
+ ...checkSearchableFieldList(
3036
+ declared,
3037
+ objectName,
3038
+ fieldsByObject,
3039
+ where,
3040
+ path,
3041
+ subject,
3042
+ role,
3043
+ unprovisionedAnchors
3044
+ )
3009
3045
  );
3010
3046
  };
3011
3047
  for (let oi = 0; oi < objects.length; oi++) {
@@ -3192,7 +3228,12 @@ var COMPONENT_FIELD_SPECS = {
3192
3228
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
3193
3229
  "record:path": { props: ["statusField"] },
3194
3230
  "element:number": { props: ["field"] },
3195
- "element:filter": { props: ["fields"] },
3231
+ // `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
3232
+ // whole element at element grain (ADR-0049 — no renderer ever shipped for it).
3233
+ // Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
3234
+ // spec-conformant page carries `fields` on it, and the #5068 props gate
3235
+ // reports an authored one by name with the element-retirement prescription —
3236
+ // the same #5775/#6629 residue class as the record-picker entries below.
3196
3237
  "element:form": { props: ["fields"] },
3197
3238
  // `labelField` is the one field-bearing prop this element declares. Its former
3198
3239
  // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
@@ -3896,7 +3937,12 @@ function validateReactPageProps(stack) {
3896
3937
  searchTargets,
3897
3938
  where,
3898
3939
  `${path} \u203A searchableFields`,
3899
- "searchableFields"
3940
+ "searchableFields",
3941
+ // A `<ListView>` prop is a view-level narrowing — the checker's
3942
+ // default, spelled out here because the #8404 provenance index
3943
+ // follows it positionally.
3944
+ "narrowing",
3945
+ unprovisionedAnchors
3900
3946
  )
3901
3947
  );
3902
3948
  }
@@ -5468,6 +5514,7 @@ function validateSecurityPosture(stack, opts) {
5468
5514
  }
5469
5515
  }
5470
5516
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
5517
+ const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
5471
5518
  const nowMs = opts?.nowMs ?? Date.now();
5472
5519
  for (const [i, seed] of asArray19(stack.data).entries()) {
5473
5520
  const seedObject = typeof seed.object === "string" ? seed.object : "";
@@ -5491,7 +5538,7 @@ function validateSecurityPosture(stack, opts) {
5491
5538
  }
5492
5539
  }
5493
5540
  const delegatedFrom = rec.delegated_from;
5494
- if (delegatedFrom != null && delegatedFrom !== "") {
5541
+ if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
5495
5542
  const reason = rec.reason;
5496
5543
  if (typeof reason !== "string" || reason.trim().length === 0) {
5497
5544
  findings.push({
@@ -5657,6 +5704,8 @@ function validateOrgAxisRedLines(stack) {
5657
5704
  var import_formula6 = require("@objectstack/formula");
5658
5705
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
5659
5706
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
5707
+ var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
5708
+ var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
5660
5709
  function asArray21(v) {
5661
5710
  if (Array.isArray(v)) return v;
5662
5711
  if (v && typeof v === "object") {
@@ -5681,10 +5730,67 @@ function sourceOf(condition) {
5681
5730
  return str2(input?.source);
5682
5731
  }
5683
5732
  var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column `record.<field>` paths (ADR-0058 D2).";
5733
+ function effectiveSharingModelOf(obj) {
5734
+ const m = obj.sharingModel;
5735
+ if (m === "private") return "private";
5736
+ if (m === "public_read") return "read";
5737
+ if (m === "public_read_write" || m === "controlled_by_parent") return "public";
5738
+ if (m == null) {
5739
+ const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
5740
+ return isSystem ? "public" : "private";
5741
+ }
5742
+ return "private";
5743
+ }
5744
+ function masterOf(obj) {
5745
+ for (const f of asArray21(obj.fields)) {
5746
+ if (f.type === "master_detail") {
5747
+ const ref = f.reference;
5748
+ if (typeof ref === "string" && ref) return ref;
5749
+ }
5750
+ }
5751
+ return void 0;
5752
+ }
5753
+ function anchorFindings(rule, index, objectsByName) {
5754
+ const object = str2(rule.object);
5755
+ if (!object) return [];
5756
+ const target = objectsByName.get(object);
5757
+ if (!target) return [];
5758
+ const name = str2(rule.name) || String(index);
5759
+ const where = `sharing rule "${name}" on object "${object}"`;
5760
+ const path = `sharingRules[${index}].object`;
5761
+ const owd = target.sharingModel;
5762
+ if (owd === "controlled_by_parent") {
5763
+ const master = masterOf(target);
5764
+ return [{
5765
+ severity: "error",
5766
+ rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
5767
+ where,
5768
+ path,
5769
+ message: `Sharing rule "${name}" is anchored on object "${object}", which declares sharingModel 'controlled_by_parent'. A detail record has no record-level access of its own \u2014 its visibility is DERIVED from its master (ADR-0055), so it holds no shares to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is controlled by its parent (master-detail); share the master record instead"), so the rule's boot backfill fails, no \`sys_record_share\` row is ever written, and the recipients this rule names get whatever the MASTER grants them \u2014 which may be nothing. The grant is declared and does not exist.`,
5770
+ hint: `Move the rule onto the MASTER object` + (master ? ` \u2014 "${object}" derives from "${master}" through its master_detail field, so share "${master}" and the detail rows follow` : `, and share that instead; the detail rows follow`) + `. If "${object}" is meant to carry a record-level baseline of its own, that is a different decision: change its sharingModel to 'private' (owner + shares) or 'public_read', and this rule becomes enforceable where it stands.`
5771
+ }];
5772
+ }
5773
+ if (effectiveSharingModelOf(target) !== "public") return [];
5774
+ const declared = owd === "public_read_write" ? `declares sharingModel 'public_read_write'` : `declares no sharingModel and is a system object (\`isSystem: true\` or a \`sys_\` name), which ADR-0090 D1 resolves to public`;
5775
+ return [{
5776
+ severity: "error",
5777
+ rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
5778
+ where,
5779
+ path,
5780
+ message: `Sharing rule "${name}" is anchored on object "${object}", which ${declared}. Its effective sharing model is therefore \`public\`, and sharing only ever WIDENS an OWD baseline \u2014 on the widest baseline there is nothing left to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is not under record-sharing enforcement"), so the rule's boot backfill fails and no \`sys_record_share\` row is ever written. Measured: \`buildReadFilter\` returns \`null\` for this object, i.e. NO record-level filter at all \u2014 every principal already reads every row, so this rule advertises a restriction that does not exist.`,
5781
+ hint: `Decide which half is wrong. If the ACCESS is right \u2014 everyone should read and write these records \u2014 the rule is dead metadata: delete it (ADR-0049 enforce-or-remove). If the RULE is right \u2014 only the named audience should reach these records \u2014 then "${object}"'s OWD is the defect: set sharingModel: 'private' (owner + shares) or 'public_read', and this rule starts enforcing. Do NOT re-home the rule onto another public object; that moves the inertness instead of removing it.`
5782
+ }];
5783
+ }
5684
5784
  function validateSharingRuleEnforceability(stack) {
5685
5785
  const findings = [];
5686
5786
  const cfg = stack ?? {};
5787
+ const objectsByName = /* @__PURE__ */ new Map();
5788
+ for (const obj of asArray21(cfg.objects)) {
5789
+ const name = str2(obj.name);
5790
+ if (name) objectsByName.set(name, obj);
5791
+ }
5687
5792
  asArray21(cfg.sharingRules).forEach((rule, index) => {
5793
+ anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
5688
5794
  const input = toCompilerInput(rule.condition);
5689
5795
  if (input === null) return;
5690
5796
  const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
@@ -5817,7 +5923,6 @@ function asArray23(v) {
5817
5923
  function strName7(v) {
5818
5924
  return typeof v === "string" && v.length > 0 ? v : void 0;
5819
5925
  }
5820
- var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
5821
5926
  var URL_COLLECTION_TO_STACK_KEY = {
5822
5927
  object: "objects",
5823
5928
  objects: "objects",
@@ -5862,13 +5967,10 @@ function collectKnownTargets(stack) {
5862
5967
  return { actions, objects, reports, dashboards, pages, views };
5863
5968
  }
5864
5969
  function resolveActionTarget(actionType, target, known) {
5865
- if (known.actions.has(target)) return true;
5866
5970
  if (actionType === "modal") {
5867
- if (known.objects.has(target)) return true;
5868
- const m = MODAL_VERB_RE.exec(target);
5869
- if (m && known.objects.has(m[1])) return true;
5971
+ return known.pages.has(target);
5870
5972
  }
5871
- return false;
5973
+ return known.actions.has(target);
5872
5974
  }
5873
5975
  function resolveUrlRoute(target, known) {
5874
5976
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
@@ -5898,14 +6000,13 @@ function validateDashboardActionRefs(stack) {
5898
6000
  const actionType = strName7(action.actionType) ?? "url";
5899
6001
  if (actionType === "script" || actionType === "modal") {
5900
6002
  if (resolveActionTarget(actionType, target, known)) return;
5901
- const kindWord = actionType === "script" ? "script" : "modal";
5902
6003
  findings.push({
5903
6004
  severity: "error",
5904
6005
  rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
5905
6006
  where,
5906
6007
  path,
5907
- message: `${kindWord} action target "${target}" resolves to no defined action` + (actionType === "modal" ? " or object" : "") + `. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
5908
- hint: actionType === "modal" ? `Define an action named "${target}" (stack.actions or the object's actions), use the "<verb>_<object>" convention against a real object (e.g. "create_<object>"), point actionUrl at an existing object, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
6008
+ message: actionType === "modal" ? `modal action target "${target}" names no declared page \u2014 a modal target names a PAGE, only (objectstack#6739). The button renders but the runtime refuses the dispatch when clicked \u2014 a dangling reference (ADR-0049: a declared reference must resolve).` : `script action target "${target}" resolves to no defined action. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
6009
+ hint: actionType === "modal" ? `Point actionUrl at a declared page (stack.pages), or use actionType: 'form' with an "<object>.<view>" form-view target to open an object's form, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
5909
6010
  });
5910
6011
  return;
5911
6012
  }
@@ -5947,7 +6048,7 @@ function validateDashboardActionRefs(stack) {
5947
6048
  var import_data7 = require("@objectstack/spec/data");
5948
6049
 
5949
6050
  // src/filter-walk.ts
5950
- var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
6051
+ var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
5951
6052
  function asArray24(v) {
5952
6053
  if (Array.isArray(v)) return v;
5953
6054
  if (v && typeof v === "object") {
@@ -6051,8 +6152,146 @@ function validateFilterTokens(stack) {
6051
6152
  return out;
6052
6153
  }
6053
6154
 
6054
- // src/validate-empty-combinators.ts
6155
+ // src/validate-preset-comparands.ts
6055
6156
  var import_data8 = require("@objectstack/spec/data");
6157
+ var import_ui3 = require("@objectstack/spec/ui");
6158
+ var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
6159
+ var PRESET_COMPARAND_SURFACES = [
6160
+ { key: "dashboards", kind: "dashboard" },
6161
+ { key: "objects", kind: "object" },
6162
+ { key: "views", kind: "view" },
6163
+ { key: "reports", kind: "report" },
6164
+ { key: "datasets", kind: "dataset" },
6165
+ { key: "pages", kind: "page" },
6166
+ { key: "apps", kind: "app" },
6167
+ { key: "flows", kind: "flow" }
6168
+ ];
6169
+ var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
6170
+ var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
6171
+ var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
6172
+ "greater_than",
6173
+ "greater_than_or_equal",
6174
+ "less_than",
6175
+ "less_than_or_equal",
6176
+ "before",
6177
+ "after"
6178
+ ]);
6179
+ var MAX_DEPTH = 32;
6180
+ function isPlainObject(v) {
6181
+ return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
6182
+ }
6183
+ function finding(where, path, preset, operator) {
6184
+ return {
6185
+ severity: "error",
6186
+ rule: FILTER_PRESET_COMPARAND,
6187
+ where,
6188
+ path,
6189
+ message: (0, import_data8.bareDateRangePresetComparandMessage)(preset, operator),
6190
+ hint: "Presets belong to the dashboard date-filter bar (dateRange.defaultRange, a date global filter's defaultValue). In a filter comparand, write the {date-macro} window the message names, or an ISO date."
6191
+ };
6192
+ }
6193
+ function judgeConditionNode(node, path, where, out, depth) {
6194
+ if (depth > MAX_DEPTH) return;
6195
+ for (const [key, value] of Object.entries(node)) {
6196
+ const here = `${path}.${key}`;
6197
+ if (key === "$and" || key === "$or") {
6198
+ if (Array.isArray(value)) {
6199
+ value.forEach((arm, i) => {
6200
+ if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
6201
+ });
6202
+ }
6203
+ continue;
6204
+ }
6205
+ if (key === "$not") {
6206
+ if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
6207
+ continue;
6208
+ }
6209
+ if (key.startsWith("$")) continue;
6210
+ if (!isPlainObject(value)) continue;
6211
+ const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
6212
+ if (!hasOps) {
6213
+ judgeConditionNode(value, here, where, out, depth + 1);
6214
+ continue;
6215
+ }
6216
+ for (const [op, comparand] of Object.entries(value)) {
6217
+ if (ORDERING_DOLLAR_OPS.has(op) && (0, import_data8.isDateRangePresetName)(comparand)) {
6218
+ out.push(finding(where, `${here}.${op}`, comparand, op));
6219
+ continue;
6220
+ }
6221
+ if (op === "$between" && Array.isArray(comparand)) {
6222
+ comparand.forEach((endpoint, i) => {
6223
+ if ((0, import_data8.isDateRangePresetName)(endpoint)) {
6224
+ out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
6225
+ }
6226
+ });
6227
+ }
6228
+ }
6229
+ }
6230
+ }
6231
+ function judgeFilterRule(rule, path, where, out) {
6232
+ const operator = (0, import_ui3.normalizeFilterOperator)(rule.operator);
6233
+ if (typeof operator !== "string") return;
6234
+ const value = rule.value;
6235
+ if (ORDERING_RULE_OPS.has(operator) && (0, import_data8.isDateRangePresetName)(value)) {
6236
+ out.push(finding(where, `${path}.value`, value, operator));
6237
+ return;
6238
+ }
6239
+ if (operator === "between" && Array.isArray(value)) {
6240
+ value.forEach((endpoint, i) => {
6241
+ if ((0, import_data8.isDateRangePresetName)(endpoint)) {
6242
+ out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
6243
+ }
6244
+ });
6245
+ }
6246
+ }
6247
+ function judgeTriple(triple, path, where, out) {
6248
+ const op = triple[1];
6249
+ if (typeof op !== "string") return;
6250
+ const canonical = (0, import_data8.canonicalAstOperator)(op);
6251
+ const value = triple[2];
6252
+ if (ORDERING_INFIX_OPS.has(canonical) && (0, import_data8.isDateRangePresetName)(value)) {
6253
+ out.push(finding(where, `${path}[2]`, value, op));
6254
+ return;
6255
+ }
6256
+ if (canonical === "between" && Array.isArray(value)) {
6257
+ value.forEach((endpoint, i) => {
6258
+ if ((0, import_data8.isDateRangePresetName)(endpoint)) {
6259
+ out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
6260
+ }
6261
+ });
6262
+ }
6263
+ }
6264
+ function judgeFilterValue(node, path, where, out, depth) {
6265
+ if (depth > MAX_DEPTH) return;
6266
+ if (Array.isArray(node)) {
6267
+ if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
6268
+ judgeTriple(node, path, where, out);
6269
+ return;
6270
+ }
6271
+ node.forEach((member, i) => {
6272
+ if (typeof member === "string") return;
6273
+ judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
6274
+ });
6275
+ return;
6276
+ }
6277
+ if (!isPlainObject(node)) return;
6278
+ if (typeof node.field === "string" && typeof node.operator === "string") {
6279
+ judgeFilterRule(node, path, where, out);
6280
+ return;
6281
+ }
6282
+ judgeConditionNode(node, path, where, out, depth);
6283
+ }
6284
+ function validatePresetComparands(stack) {
6285
+ if (!stack || typeof stack !== "object") return [];
6286
+ const out = [];
6287
+ walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
6288
+ judgeFilterValue(value, path, where, out, 0);
6289
+ });
6290
+ return out;
6291
+ }
6292
+
6293
+ // src/validate-empty-combinators.ts
6294
+ var import_data9 = require("@objectstack/spec/data");
6056
6295
  var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
6057
6296
  var FILTER_EMPTY_NODE = "filter-empty-node";
6058
6297
  var EMPTY_COMBINATOR_SURFACES = [
@@ -6071,12 +6310,12 @@ function isFilterNode(value) {
6071
6310
  return proto === Object.prototype || proto === null;
6072
6311
  }
6073
6312
  var VERDICT_OF = {
6074
- $and: (0, import_data8.reduceFilterVerdict)({ $and: [] }),
6075
- $or: (0, import_data8.reduceFilterVerdict)({ $or: [] }),
6076
- $not: (0, import_data8.reduceFilterVerdict)({ $not: {} }),
6077
- node: (0, import_data8.reduceFilterVerdict)({}),
6313
+ $and: (0, import_data9.reduceFilterVerdict)({ $and: [] }),
6314
+ $or: (0, import_data9.reduceFilterVerdict)({ $or: [] }),
6315
+ $not: (0, import_data9.reduceFilterVerdict)({ $not: {} }),
6316
+ node: (0, import_data9.reduceFilterVerdict)({}),
6078
6317
  /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
6079
- orWithEmptyBranch: (0, import_data8.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
6318
+ orWithEmptyBranch: (0, import_data9.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
6080
6319
  };
6081
6320
  function rows(verdict) {
6082
6321
  if (verdict === "true") return "matches EVERY row";
@@ -6431,7 +6670,7 @@ function validateNavTargetRefs(stack) {
6431
6670
  }
6432
6671
 
6433
6672
  // src/validate-nav-object-servability.ts
6434
- var import_data9 = require("@objectstack/spec/data");
6673
+ var import_data10 = require("@objectstack/spec/data");
6435
6674
  var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
6436
6675
  var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6437
6676
  function asArray27(v) {
@@ -6467,7 +6706,7 @@ function validateNavObjectServability(stack) {
6467
6706
  if (nav.type === "object") {
6468
6707
  const target = strName10(nav.objectName);
6469
6708
  const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
6470
- if (target && declared && !(0, import_data9.canServeApiOperation)(declared.enable, "list")) {
6709
+ if (target && declared && !(0, import_data10.canServeApiOperation)(declared.enable, "list")) {
6471
6710
  const enable = isRec15(declared.enable) ? declared.enable : {};
6472
6711
  const apiDisabled = enable.apiEnabled === false;
6473
6712
  const condition = apiDisabled ? "`enable.apiEnabled: false`" : "`enable.apiMethods` does not grant `list`" + (Array.isArray(enable.apiMethods) ? ` (declared: ${enable.apiMethods.length === 0 ? "[] \u2014 deny-all" : enable.apiMethods.map((m) => `\`${String(m)}\``).join(", ")})` : "");
@@ -6498,6 +6737,176 @@ function validateNavObjectServability(stack) {
6498
6737
  return findings;
6499
6738
  }
6500
6739
 
6740
+ // src/validate-sortable-fields.ts
6741
+ var import_data11 = require("@objectstack/spec/data");
6742
+ var SORT_FIELD_UNKNOWN = "sort-field-unknown";
6743
+ var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
6744
+ function isRec16(v) {
6745
+ return !!v && typeof v === "object" && !Array.isArray(v);
6746
+ }
6747
+ function strName11(v) {
6748
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6749
+ }
6750
+ function readSortKeys(declared) {
6751
+ const fromShorthand = (raw, at) => {
6752
+ const trimmed = raw.trim();
6753
+ if (!trimmed) return void 0;
6754
+ const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
6755
+ return bare ? { field: bare, at } : void 0;
6756
+ };
6757
+ if (typeof declared === "string") {
6758
+ return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
6759
+ }
6760
+ if (Array.isArray(declared)) {
6761
+ const keys = [];
6762
+ for (let i = 0; i < declared.length; i++) {
6763
+ const el = declared[i];
6764
+ if (typeof el === "string") {
6765
+ const k = fromShorthand(el, `[${i}]`);
6766
+ if (k) keys.push(k);
6767
+ continue;
6768
+ }
6769
+ if (!isRec16(el)) continue;
6770
+ const field = strName11(el.field);
6771
+ if (field) keys.push({ field: field.trim(), at: `[${i}]` });
6772
+ }
6773
+ return keys;
6774
+ }
6775
+ return [];
6776
+ }
6777
+ function suggest4(target, known) {
6778
+ let best;
6779
+ let bestScore = Infinity;
6780
+ for (const candidate of known) {
6781
+ const d = distance3(target, candidate);
6782
+ if (d < bestScore) {
6783
+ bestScore = d;
6784
+ best = candidate;
6785
+ }
6786
+ }
6787
+ const limit = Math.max(2, Math.floor(target.length / 3));
6788
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
6789
+ }
6790
+ function distance3(a, b) {
6791
+ const m = a.length;
6792
+ const n = b.length;
6793
+ if (m === 0) return n;
6794
+ if (n === 0) return m;
6795
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
6796
+ for (let i = 1; i <= m; i++) {
6797
+ const curr = [i, ...new Array(n).fill(0)];
6798
+ for (let j = 1; j <= n; j++) {
6799
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6800
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
6801
+ }
6802
+ prev = curr;
6803
+ }
6804
+ return prev[n];
6805
+ }
6806
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
6807
+ const findings = [];
6808
+ if (declared === void 0 || declared === null) return findings;
6809
+ if (!objectName) return findings;
6810
+ if (!fieldsByObject.has(objectName)) return findings;
6811
+ const target = fieldsByObject.get(objectName);
6812
+ if (!target) return findings;
6813
+ const known = target.names;
6814
+ for (const key of readSortKeys(declared)) {
6815
+ const name = key.field;
6816
+ const head = name.split(".")[0];
6817
+ if (SYSTEM_FIELDS.has(head)) continue;
6818
+ if (!known.has(head)) {
6819
+ const dotted = name.includes(".");
6820
+ findings.push({
6821
+ severity: "error",
6822
+ rule: SORT_FIELD_UNKNOWN,
6823
+ where,
6824
+ path: `${path}${key.at}`,
6825
+ message: `${subject} orders by "${name}", which is not a field on object "${objectName}". The runtime refuses the sort rather than dropping it: every load of this view answers 400 INVALID_SORT (#6994), because a sort is the view's FIRST fetch and not an optional interaction.` + (dotted ? "" : suggest4(head, known)),
6826
+ hint: (dotted ? `'sort' reaches only whole columns of "${objectName}" itself, never a related record's column \u2014 denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + (known.size > 0 ? `Object fields: ${[...known].sort().join(", ")}.` : "")
6827
+ });
6828
+ continue;
6829
+ }
6830
+ const meta = target.fields[name];
6831
+ if ((0, import_data11.isVirtualSearchField)(meta)) {
6832
+ const vtype = meta?.type;
6833
+ findings.push({
6834
+ severity: "error",
6835
+ rule: SORT_FIELD_UNSORTABLE,
6836
+ where,
6837
+ path: `${path}${key.at}`,
6838
+ message: `${subject} orders by "${name}" on object "${objectName}", a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materialises a column to ORDER BY. Measured, an unrefused sort on one returns 'asc' and 'desc' in byte-identical order \u2014 the rows carry the values they were asked to be ordered by, unordered, under a success.`,
6839
+ hint: `Denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that, or drop "${name}" from this sort. At runtime both doors now refuse it with 400 INVALID_SORT \u2014 the REST ingress (#6994) and the engine itself (#7095) \u2014 so the declaration breaks the view's first fetch, and every fetch after it.`
6840
+ });
6841
+ }
6842
+ }
6843
+ return findings;
6844
+ }
6845
+ function validateSortableFields(stack) {
6846
+ const findings = [];
6847
+ if (!isRec16(stack)) return findings;
6848
+ const objects = Array.isArray(stack.objects) ? stack.objects : isRec16(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
6849
+ const fieldsByObject = indexObjectSearchTargets(stack);
6850
+ const check = (declared, objectName, where, path, subject) => {
6851
+ findings.push(
6852
+ ...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
6853
+ );
6854
+ };
6855
+ for (let oi = 0; oi < objects.length; oi++) {
6856
+ const obj = objects[oi];
6857
+ if (!isRec16(obj)) continue;
6858
+ const objName = strName11(obj.name);
6859
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
6860
+ if (isRec16(obj.listViews)) {
6861
+ for (const [key, lv] of Object.entries(obj.listViews)) {
6862
+ if (!isRec16(lv)) continue;
6863
+ check(
6864
+ lv.sort,
6865
+ // A built-in list view belongs to its object; an inline `data.object`
6866
+ // may still retarget it (ADR-0047 allows the explicit binding).
6867
+ listViewObject2(lv) ?? objName,
6868
+ `${label2} \u203A listViews.${key}`,
6869
+ `objects[${oi}].listViews.${key}.sort`,
6870
+ "list-view sort"
6871
+ );
6872
+ }
6873
+ }
6874
+ }
6875
+ const views = Array.isArray(stack.views) ? stack.views : [];
6876
+ for (let vi = 0; vi < views.length; vi++) {
6877
+ const view = views[vi];
6878
+ if (!isRec16(view)) continue;
6879
+ const viewLabel2 = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
6880
+ const viewObject = strName11(view.objectName) ?? strName11(view.object);
6881
+ if (isRec16(view.list)) {
6882
+ check(
6883
+ view.list.sort,
6884
+ listViewObject2(view.list) ?? viewObject,
6885
+ `view "${viewLabel2}" \u203A list`,
6886
+ `views[${vi}].list.sort`,
6887
+ "list-view sort"
6888
+ );
6889
+ }
6890
+ if (isRec16(view.listViews)) {
6891
+ for (const [key, lv] of Object.entries(view.listViews)) {
6892
+ if (!isRec16(lv)) continue;
6893
+ check(
6894
+ lv.sort,
6895
+ listViewObject2(lv) ?? viewObject,
6896
+ `view "${viewLabel2}" \u203A listViews.${key}`,
6897
+ `views[${vi}].listViews.${key}.sort`,
6898
+ "list-view sort"
6899
+ );
6900
+ }
6901
+ }
6902
+ }
6903
+ return findings;
6904
+ }
6905
+ function listViewObject2(listView) {
6906
+ const data = listView.data;
6907
+ return isRec16(data) ? strName11(data.object) : void 0;
6908
+ }
6909
+
6501
6910
  // src/validate-action-name-refs.ts
6502
6911
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
6503
6912
  function asArray28(v) {
@@ -6507,13 +6916,13 @@ function asArray28(v) {
6507
6916
  }
6508
6917
  return [];
6509
6918
  }
6510
- function strName11(v) {
6919
+ function strName12(v) {
6511
6920
  return typeof v === "string" && v.length > 0 ? v : void 0;
6512
6921
  }
6513
6922
  function strList(v) {
6514
6923
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
6515
6924
  }
6516
- function distance3(a, b) {
6925
+ function distance4(a, b) {
6517
6926
  const m = a.length;
6518
6927
  const n = b.length;
6519
6928
  if (m === 0) return n;
@@ -6529,11 +6938,11 @@ function distance3(a, b) {
6529
6938
  }
6530
6939
  return prev[n];
6531
6940
  }
6532
- function suggest4(target, known) {
6941
+ function suggest5(target, known) {
6533
6942
  let best;
6534
6943
  let bestScore = Infinity;
6535
6944
  for (const candidate of known) {
6536
- const d = distance3(target, candidate);
6945
+ const d = distance4(target, candidate);
6537
6946
  if (d < bestScore) {
6538
6947
  bestScore = d;
6539
6948
  best = candidate;
@@ -6545,13 +6954,13 @@ function suggest4(target, known) {
6545
6954
  function collectActionNames(stack) {
6546
6955
  const names = /* @__PURE__ */ new Set();
6547
6956
  for (const action of asArray28(stack.actions)) {
6548
- const n = strName11(action?.name);
6957
+ const n = strName12(action?.name);
6549
6958
  if (n) names.add(n);
6550
6959
  }
6551
6960
  for (const obj of asArray28(stack.objects)) {
6552
6961
  if (!obj || typeof obj !== "object") continue;
6553
6962
  for (const action of asArray28(obj.actions)) {
6554
- const n = strName11(action?.name);
6963
+ const n = strName12(action?.name);
6555
6964
  if (n) names.add(n);
6556
6965
  }
6557
6966
  }
@@ -6568,7 +6977,7 @@ function validateActionNameRefs(stack) {
6568
6977
  rule: ACTION_NAME_UNDEFINED,
6569
6978
  where,
6570
6979
  path,
6571
- message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest4(name, known),
6980
+ message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest5(name, known),
6572
6981
  hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) ${placement}, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
6573
6982
  });
6574
6983
  };
@@ -6594,7 +7003,7 @@ function validateActionNameRefs(stack) {
6594
7003
  if (!def || typeof def !== "object") continue;
6595
7004
  if (def.execution !== "aggregate") continue;
6596
7005
  if (def.actionDef !== void 0) continue;
6597
- const name = strName11(def.name);
7006
+ const name = strName12(def.name);
6598
7007
  if (!name) continue;
6599
7008
  check(
6600
7009
  name,
@@ -6609,7 +7018,7 @@ function validateActionNameRefs(stack) {
6609
7018
  for (let vi = 0; vi < views.length; vi++) {
6610
7019
  const view = views[vi];
6611
7020
  if (!view || typeof view !== "object") continue;
6612
- const viewName = strName11(view.name) ?? strName11(view.object) ?? `#${vi}`;
7021
+ const viewName = strName12(view.name) ?? strName12(view.object) ?? `#${vi}`;
6613
7022
  const owner = `view "${viewName}"`;
6614
7023
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
6615
7024
  const listViews = view.listViews;
@@ -6625,7 +7034,7 @@ function validateActionNameRefs(stack) {
6625
7034
  if (!obj || typeof obj !== "object") continue;
6626
7035
  const objListViews = obj.listViews;
6627
7036
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
6628
- const owner = `object "${strName11(obj.name) ?? `#${oi}`}"`;
7037
+ const owner = `object "${strName12(obj.name) ?? `#${oi}`}"`;
6629
7038
  for (const [key, lv] of Object.entries(objListViews)) {
6630
7039
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
6631
7040
  }
@@ -6634,7 +7043,7 @@ function validateActionNameRefs(stack) {
6634
7043
  for (let pi = 0; pi < pages.length; pi++) {
6635
7044
  const page = pages[pi];
6636
7045
  if (!page || typeof page !== "object") continue;
6637
- const pageName = strName11(page.name) ?? `#${pi}`;
7046
+ const pageName = strName12(page.name) ?? `#${pi}`;
6638
7047
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6639
7048
  const props = component.properties;
6640
7049
  if (!props || typeof props !== "object") continue;
@@ -6642,7 +7051,7 @@ function validateActionNameRefs(stack) {
6642
7051
  for (let ai = 0; ai < names.length; ai++) {
6643
7052
  check(
6644
7053
  names[ai],
6645
- `page "${pageName}" \xB7 component "${strName11(component.type) ?? "?"}"`,
7054
+ `page "${pageName}" \xB7 component "${strName12(component.type) ?? "?"}"`,
6646
7055
  `${path}.properties.actionNames[${ai}]`,
6647
7056
  "Quick-actions bar"
6648
7057
  );
@@ -6653,7 +7062,7 @@ function validateActionNameRefs(stack) {
6653
7062
  for (let ai = 0; ai < apps.length; ai++) {
6654
7063
  const app = apps[ai];
6655
7064
  if (!app || typeof app !== "object") continue;
6656
- const appName = strName11(app.name) ?? `#${ai}`;
7065
+ const appName = strName12(app.name) ?? `#${ai}`;
6657
7066
  const walkNav = (items, basePath) => {
6658
7067
  const navItems = asArray28(items);
6659
7068
  for (let ni = 0; ni < navItems.length; ni++) {
@@ -6661,20 +7070,20 @@ function validateActionNameRefs(stack) {
6661
7070
  if (!nav || typeof nav !== "object") continue;
6662
7071
  const navPath = `${basePath}[${ni}]`;
6663
7072
  const actionDef = nav.actionDef;
6664
- const actionName = strName11(actionDef?.actionName);
7073
+ const actionName = strName12(actionDef?.actionName);
6665
7074
  if (nav.type === "action" && actionName) {
6666
7075
  check(
6667
7076
  actionName,
6668
- `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
7077
+ `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
6669
7078
  `${navPath}.actionDef.actionName`,
6670
7079
  "Navigation action item"
6671
7080
  );
6672
7081
  }
6673
- const runAction = strName11(nav.runAction);
7082
+ const runAction = strName12(nav.runAction);
6674
7083
  if (nav.type === "object" && runAction) {
6675
7084
  check(
6676
7085
  runAction,
6677
- `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
7086
+ `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
6678
7087
  `${navPath}.runAction`,
6679
7088
  "Navigation deep-link auto-run"
6680
7089
  );
@@ -6700,7 +7109,7 @@ function asArray29(v) {
6700
7109
  }
6701
7110
  return [];
6702
7111
  }
6703
- function strName12(v) {
7112
+ function strName13(v) {
6704
7113
  return typeof v === "string" && v.length > 0 ? v : void 0;
6705
7114
  }
6706
7115
  function strList2(v) {
@@ -6715,7 +7124,7 @@ function collectNamePlacedActions(stack) {
6715
7124
  for (const n of strList2(list3[key])) placed.add(n);
6716
7125
  }
6717
7126
  for (const def of asArray29(list3.bulkActionDefs)) {
6718
- const n = strName12(def?.name);
7127
+ const n = strName13(def?.name);
6719
7128
  if (n) placed.add(n);
6720
7129
  }
6721
7130
  };
@@ -6741,7 +7150,7 @@ function validateActionLocations(stack) {
6741
7150
  const check = (action, path) => {
6742
7151
  if (!action || typeof action !== "object") return;
6743
7152
  if ("locations" in action) return;
6744
- const name = strName12(action.name);
7153
+ const name = strName13(action.name);
6745
7154
  if (!name) return;
6746
7155
  if (namePlaced.has(name)) return;
6747
7156
  findings.push({
@@ -6766,14 +7175,14 @@ function validateActionLocations(stack) {
6766
7175
  }
6767
7176
 
6768
7177
  // src/validate-component-props.ts
6769
- var import_ui3 = require("@objectstack/spec/ui");
7178
+ var import_ui4 = require("@objectstack/spec/ui");
6770
7179
  var import_spec3 = require("@objectstack/spec");
6771
7180
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
6772
7181
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
6773
- function isRec16(v) {
7182
+ function isRec17(v) {
6774
7183
  return !!v && typeof v === "object" && !Array.isArray(v);
6775
7184
  }
6776
- function strName13(v) {
7185
+ function strName14(v) {
6777
7186
  return typeof v === "string" && v.length > 0 ? v : void 0;
6778
7187
  }
6779
7188
  function asArray30(v) {
@@ -6783,12 +7192,12 @@ function asArray30(v) {
6783
7192
  }
6784
7193
  return [];
6785
7194
  }
6786
- var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
7195
+ var PROPS_SCHEMAS = import_ui4.ComponentPropsMap;
6787
7196
  var DATASOURCE_SUPPLIED_PROP = "object";
6788
7197
  function suppliedByDataSource(issue, component) {
6789
7198
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
6790
- const dataSource = isRec16(component.dataSource) ? component.dataSource : void 0;
6791
- return strName13(dataSource?.object) !== void 0;
7199
+ const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
7200
+ return strName14(dataSource?.object) !== void 0;
6792
7201
  }
6793
7202
  function unrecognizedKeysFromUnionArm(issue) {
6794
7203
  if (issue.code !== "invalid_union") return void 0;
@@ -6805,18 +7214,18 @@ function unrecognizedKeysFromUnionArm(issue) {
6805
7214
  }
6806
7215
  function validateComponentProps(stack) {
6807
7216
  const findings = [];
6808
- if (!isRec16(stack)) return findings;
7217
+ if (!isRec17(stack)) return findings;
6809
7218
  const pages = asArray30(stack.pages);
6810
7219
  for (let pi = 0; pi < pages.length; pi++) {
6811
7220
  const page = pages[pi];
6812
- if (!isRec16(page)) continue;
6813
- const pageName = strName13(page.name) ?? `#${pi}`;
7221
+ if (!isRec17(page)) continue;
7222
+ const pageName = strName14(page.name) ?? `#${pi}`;
6814
7223
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6815
- const type = strName13(component.type);
7224
+ const type = strName14(component.type);
6816
7225
  if (!type) continue;
6817
7226
  const schema = PROPS_SCHEMAS[type];
6818
7227
  if (!schema) continue;
6819
- const props = isRec16(component.properties) ? component.properties : void 0;
7228
+ const props = isRec17(component.properties) ? component.properties : void 0;
6820
7229
  if (!props) continue;
6821
7230
  const where = `page "${pageName}" \xB7 ${type}`;
6822
7231
  const base = `${path}.properties`;
@@ -6888,16 +7297,16 @@ function asArray31(v) {
6888
7297
  }
6889
7298
  return [];
6890
7299
  }
6891
- function strName14(v) {
7300
+ function strName15(v) {
6892
7301
  return typeof v === "string" && v.length > 0 ? v : void 0;
6893
7302
  }
6894
7303
  function strList3(v) {
6895
7304
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
6896
7305
  }
6897
- function isRec17(v) {
7306
+ function isRec18(v) {
6898
7307
  return !!v && typeof v === "object" && !Array.isArray(v);
6899
7308
  }
6900
- function distance4(a, b) {
7309
+ function distance5(a, b) {
6901
7310
  const m = a.length;
6902
7311
  const n = b.length;
6903
7312
  if (m === 0) return n;
@@ -6913,11 +7322,11 @@ function distance4(a, b) {
6913
7322
  }
6914
7323
  return prev[n];
6915
7324
  }
6916
- function suggest5(target, known) {
7325
+ function suggest6(target, known) {
6917
7326
  let best;
6918
7327
  let bestScore = Infinity;
6919
7328
  for (const c of known) {
6920
- const d = distance4(target, c);
7329
+ const d = distance5(target, c);
6921
7330
  if (d < bestScore) {
6922
7331
  bestScore = d;
6923
7332
  best = c;
@@ -6933,16 +7342,16 @@ function list2(names) {
6933
7342
  function indexDatasets(stack) {
6934
7343
  const out = /* @__PURE__ */ new Map();
6935
7344
  for (const ds of asArray31(stack.datasets)) {
6936
- const name = strName14(ds.name);
7345
+ const name = strName15(ds.name);
6937
7346
  if (!name) continue;
6938
7347
  const dimensions = /* @__PURE__ */ new Set();
6939
7348
  for (const d of asArray31(ds.dimensions)) {
6940
- const n = strName14(d.name);
7349
+ const n = strName15(d.name);
6941
7350
  if (n) dimensions.add(n);
6942
7351
  }
6943
7352
  const measures = /* @__PURE__ */ new Set();
6944
7353
  for (const m of asArray31(ds.measures)) {
6945
- const n = strName14(m.name);
7354
+ const n = strName15(m.name);
6946
7355
  if (n) measures.add(n);
6947
7356
  }
6948
7357
  out.set(name, { dimensions, measures });
@@ -6965,7 +7374,7 @@ function validateChartBindings(stack) {
6965
7374
  where: binding.where,
6966
7375
  path: `${binding.path}.dataset`,
6967
7376
  message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
6968
- hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
7377
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
6969
7378
  });
6970
7379
  return;
6971
7380
  }
@@ -6977,7 +7386,7 @@ function validateChartBindings(stack) {
6977
7386
  where: binding.where,
6978
7387
  path,
6979
7388
  message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
6980
- hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
7389
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
6981
7390
  });
6982
7391
  };
6983
7392
  const measureRef = (name, path, selected2) => {
@@ -6988,7 +7397,7 @@ function validateChartBindings(stack) {
6988
7397
  where: binding.where,
6989
7398
  path,
6990
7399
  message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
6991
- hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
7400
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
6992
7401
  });
6993
7402
  return;
6994
7403
  }
@@ -7023,26 +7432,26 @@ function validateChartBindings(stack) {
7023
7432
  const reports = asArray31(stack.reports);
7024
7433
  for (let ri = 0; ri < reports.length; ri++) {
7025
7434
  const report = reports[ri];
7026
- if (!isRec17(report)) continue;
7027
- const reportName = strName14(report.name) ?? `#${ri}`;
7435
+ if (!isRec18(report)) continue;
7436
+ const reportName = strName15(report.name) ?? `#${ri}`;
7028
7437
  const checkReportChart = (chart, dataset, values, where, path) => {
7029
- if (!isRec17(chart)) return;
7438
+ if (!isRec18(chart)) return;
7030
7439
  check({
7031
7440
  dataset,
7032
7441
  // `values` is the report's measure SELECTION, not a chart ref; feeding
7033
7442
  // it in lets the yAxis "declared but not selected" check work without
7034
7443
  // reporting the selection itself twice.
7035
7444
  values: { names: values, path: `${path}.values` },
7036
- xAxis: strName14(chart.xAxis) ? { name: strName14(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
7037
- yAxis: strName14(chart.yAxis) ? { name: strName14(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
7038
- series: asArray31(chart.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
7445
+ xAxis: strName15(chart.xAxis) ? { name: strName15(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
7446
+ yAxis: strName15(chart.yAxis) ? { name: strName15(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
7447
+ series: asArray31(chart.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
7039
7448
  where,
7040
7449
  path: `${path}.chart`
7041
7450
  });
7042
7451
  };
7043
7452
  checkReportChart(
7044
7453
  report.chart,
7045
- strName14(report.dataset),
7454
+ strName15(report.dataset),
7046
7455
  strList3(report.values),
7047
7456
  `report "${reportName}" \xB7 chart`,
7048
7457
  `reports[${ri}]`
@@ -7050,22 +7459,22 @@ function validateChartBindings(stack) {
7050
7459
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
7051
7460
  for (let bi = 0; bi < blocks.length; bi++) {
7052
7461
  const block = blocks[bi];
7053
- if (!isRec17(block)) continue;
7462
+ if (!isRec18(block)) continue;
7054
7463
  checkReportChart(
7055
7464
  block.chart,
7056
- strName14(block.dataset),
7465
+ strName15(block.dataset),
7057
7466
  strList3(block.values),
7058
- `report "${reportName}" \xB7 block "${strName14(block.name) ?? `#${bi}`}" chart`,
7467
+ `report "${reportName}" \xB7 block "${strName15(block.name) ?? `#${bi}`}" chart`,
7059
7468
  `reports[${ri}].blocks[${bi}]`
7060
7469
  );
7061
7470
  }
7062
7471
  }
7063
7472
  const checkListChart = (container, where, path) => {
7064
- if (!isRec17(container)) return;
7473
+ if (!isRec18(container)) return;
7065
7474
  const chart = container.chart;
7066
- if (!isRec17(chart)) return;
7475
+ if (!isRec18(chart)) return;
7067
7476
  check({
7068
- dataset: strName14(chart.dataset),
7477
+ dataset: strName15(chart.dataset),
7069
7478
  dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
7070
7479
  values: { names: strList3(chart.values), path: `${path}.chart.values` },
7071
7480
  where,
@@ -7075,10 +7484,10 @@ function validateChartBindings(stack) {
7075
7484
  const views = asArray31(stack.views);
7076
7485
  for (let vi = 0; vi < views.length; vi++) {
7077
7486
  const view = views[vi];
7078
- if (!isRec17(view)) continue;
7079
- const viewName = strName14(view.name) ?? strName14(view.objectName) ?? `#${vi}`;
7487
+ if (!isRec18(view)) continue;
7488
+ const viewName = strName15(view.name) ?? strName15(view.objectName) ?? `#${vi}`;
7080
7489
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
7081
- if (isRec17(view.listViews)) {
7490
+ if (isRec18(view.listViews)) {
7082
7491
  for (const [key, lv] of Object.entries(view.listViews)) {
7083
7492
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
7084
7493
  }
@@ -7087,8 +7496,8 @@ function validateChartBindings(stack) {
7087
7496
  const objects = asArray31(stack.objects);
7088
7497
  for (let oi = 0; oi < objects.length; oi++) {
7089
7498
  const obj = objects[oi];
7090
- if (!isRec17(obj) || !isRec17(obj.listViews)) continue;
7091
- const objName = strName14(obj.name) ?? `#${oi}`;
7499
+ if (!isRec18(obj) || !isRec18(obj.listViews)) continue;
7500
+ const objName = strName15(obj.name) ?? `#${oi}`;
7092
7501
  for (const [key, lv] of Object.entries(obj.listViews)) {
7093
7502
  checkListChart(
7094
7503
  lv,
@@ -7100,19 +7509,19 @@ function validateChartBindings(stack) {
7100
7509
  const pages = asArray31(stack.pages);
7101
7510
  for (let pi = 0; pi < pages.length; pi++) {
7102
7511
  const page = pages[pi];
7103
- if (!isRec17(page)) continue;
7104
- const pageName = strName14(page.name) ?? `#${pi}`;
7512
+ if (!isRec18(page)) continue;
7513
+ const pageName = strName15(page.name) ?? `#${pi}`;
7105
7514
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
7106
- const props = isRec17(component.properties) ? component.properties : void 0;
7107
- if (!props || !strName14(props.dataset)) continue;
7108
- const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName14(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
7109
- const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
7515
+ const props = isRec18(component.properties) ? component.properties : void 0;
7516
+ if (!props || !strName15(props.dataset)) continue;
7517
+ const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName15(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
7518
+ const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
7110
7519
  check({
7111
- dataset: strName14(props.dataset),
7520
+ dataset: strName15(props.dataset),
7112
7521
  dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
7113
7522
  values: { names: strList3(props.values), path: `${path}.properties.values` },
7114
7523
  series: [...axisRefs, ...seriesRefs],
7115
- where: `page "${pageName}" \xB7 ${strName14(component.type) ?? "chart"}`,
7524
+ where: `page "${pageName}" \xB7 ${strName15(component.type) ?? "chart"}`,
7116
7525
  path: `${path}.properties`
7117
7526
  });
7118
7527
  }
@@ -7126,11 +7535,11 @@ var import_meta4 = {};
7126
7535
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
7127
7536
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
7128
7537
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
7129
- var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7538
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7130
7539
  function asArray32(v) {
7131
- if (Array.isArray(v)) return v.filter(isRec18);
7132
- if (isRec18(v)) {
7133
- return Object.entries(v).filter(([, def]) => isRec18(def)).map(([name, def]) => ({ name, ...def }));
7540
+ if (Array.isArray(v)) return v.filter(isRec19);
7541
+ if (isRec19(v)) {
7542
+ return Object.entries(v).filter(([, def]) => isRec19(def)).map(([name, def]) => ({ name, ...def }));
7134
7543
  }
7135
7544
  return [];
7136
7545
  }
@@ -7147,7 +7556,7 @@ function loadAjv() {
7147
7556
  `@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.`
7148
7557
  );
7149
7558
  }
7150
- const ctor = isRec18(mod) && "default" in mod ? mod.default : mod;
7559
+ const ctor = isRec19(mod) && "default" in mod ? mod.default : mod;
7151
7560
  cachedAjv = ctor;
7152
7561
  return ctor;
7153
7562
  }
@@ -7162,7 +7571,7 @@ function loadAddFormats() {
7162
7571
  `@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.`
7163
7572
  );
7164
7573
  }
7165
- const plugin = isRec18(mod) && "default" in mod ? mod.default : mod;
7574
+ const plugin = isRec19(mod) && "default" in mod ? mod.default : mod;
7166
7575
  cachedAddFormats = plugin;
7167
7576
  return plugin;
7168
7577
  }
@@ -7192,13 +7601,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
7192
7601
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
7193
7602
  for (const branch of ["then", "otherwise"]) {
7194
7603
  const nested = rule[branch];
7195
- if (isRec18(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
7604
+ if (isRec19(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
7196
7605
  }
7197
7606
  return out;
7198
7607
  }
7199
7608
  function walkObjectValidationRules(stack) {
7200
7609
  const walked = [];
7201
- if (!isRec18(stack)) return walked;
7610
+ if (!isRec19(stack)) return walked;
7202
7611
  for (const obj of asArray32(stack.objects)) {
7203
7612
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7204
7613
  const validations = obj.validations;
@@ -7233,7 +7642,7 @@ function validateRuleCompilability(stack) {
7233
7642
  });
7234
7643
  }
7235
7644
  }
7236
- if (rule.type === "json_schema" && isRec18(rule.schema)) {
7645
+ if (rule.type === "json_schema" && isRec19(rule.schema)) {
7237
7646
  try {
7238
7647
  createRuntimeAjv().compile(rule.schema);
7239
7648
  } catch (err) {
@@ -7253,7 +7662,7 @@ function validateRuleCompilability(stack) {
7253
7662
 
7254
7663
  // src/validate-rule-schema-formats.ts
7255
7664
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7256
- var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7665
+ var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7257
7666
  var SUBSCHEMA_KEYS = [
7258
7667
  "additionalItems",
7259
7668
  "additionalProperties",
@@ -7277,7 +7686,7 @@ var SUBSCHEMA_MAP_KEYS = [
7277
7686
  var MAX_SCHEMA_WALK_DEPTH = 32;
7278
7687
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7279
7688
  function collectFormatUses(schema, pointer, out, depth) {
7280
- if (!isRec19(schema)) return;
7689
+ if (!isRec20(schema)) return;
7281
7690
  if (typeof schema.format === "string") {
7282
7691
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7283
7692
  }
@@ -7296,7 +7705,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7296
7705
  }
7297
7706
  for (const key of SUBSCHEMA_MAP_KEYS) {
7298
7707
  const value = schema[key];
7299
- if (!isRec19(value)) continue;
7708
+ if (!isRec20(value)) continue;
7300
7709
  for (const [name, entry] of Object.entries(value)) {
7301
7710
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7302
7711
  }
@@ -7304,13 +7713,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7304
7713
  const items = schema.items;
7305
7714
  if (Array.isArray(items)) {
7306
7715
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7307
- } else if (isRec19(items)) {
7716
+ } else if (isRec20(items)) {
7308
7717
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7309
7718
  }
7310
7719
  const dependencies = schema.dependencies;
7311
- if (isRec19(dependencies)) {
7720
+ if (isRec20(dependencies)) {
7312
7721
  for (const [name, entry] of Object.entries(dependencies)) {
7313
- if (!isRec19(entry)) continue;
7722
+ if (!isRec20(entry)) continue;
7314
7723
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7315
7724
  }
7316
7725
  }
@@ -7337,9 +7746,9 @@ function nearestRegisteredFormat(name, registered) {
7337
7746
  let best = null;
7338
7747
  let bestDistance = Number.POSITIVE_INFINITY;
7339
7748
  for (const candidate of [...registered].sort()) {
7340
- const distance7 = editDistance2(authored, candidate);
7341
- if (distance7 < bestDistance) {
7342
- bestDistance = distance7;
7749
+ const distance8 = editDistance2(authored, candidate);
7750
+ if (distance8 < bestDistance) {
7751
+ bestDistance = distance8;
7343
7752
  best = candidate;
7344
7753
  }
7345
7754
  }
@@ -7349,7 +7758,7 @@ function validateRuleSchemaFormats(stack) {
7349
7758
  const findings = [];
7350
7759
  const pending = [];
7351
7760
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7352
- if (rule.type !== "json_schema" || !isRec19(rule.schema)) continue;
7761
+ if (rule.type !== "json_schema" || !isRec20(rule.schema)) continue;
7353
7762
  const uses = [];
7354
7763
  collectFormatUses(rule.schema, "", uses, 0);
7355
7764
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7474,7 +7883,7 @@ function asArray34(v) {
7474
7883
  }
7475
7884
  return [];
7476
7885
  }
7477
- function strName15(v) {
7886
+ function strName16(v) {
7478
7887
  return typeof v === "string" && v.length > 0 ? v : void 0;
7479
7888
  }
7480
7889
  function collectNavExposures(stack) {
@@ -7483,18 +7892,18 @@ function collectNavExposures(stack) {
7483
7892
  for (let ai = 0; ai < apps.length; ai++) {
7484
7893
  const app = apps[ai];
7485
7894
  if (!app || typeof app !== "object") continue;
7486
- const appName = strName15(app.name) ?? `#${ai}`;
7895
+ const appName = strName16(app.name) ?? `#${ai}`;
7487
7896
  const walk = (items, basePath) => {
7488
7897
  const navItems = asArray34(items);
7489
7898
  for (let ni = 0; ni < navItems.length; ni++) {
7490
7899
  const nav = navItems[ni];
7491
7900
  if (!nav || typeof nav !== "object") continue;
7492
7901
  const navPath = `${basePath}[${ni}]`;
7493
- const objectName = strName15(nav.objectName);
7902
+ const objectName = strName16(nav.objectName);
7494
7903
  if (nav.type === "object" && objectName) {
7495
7904
  out.push({
7496
7905
  objectName,
7497
- where: `app "${appName}" \xB7 nav "${strName15(nav.id) ?? `#${ni}`}"`,
7906
+ where: `app "${appName}" \xB7 nav "${strName16(nav.id) ?? `#${ni}`}"`,
7498
7907
  path: `${navPath}.objectName`
7499
7908
  });
7500
7909
  }
@@ -7518,7 +7927,7 @@ function validateNavAccess(stack) {
7518
7927
  if (exposures.length === 0) return findings;
7519
7928
  const ownObjects = /* @__PURE__ */ new Set();
7520
7929
  for (const obj of asArray34(stack.objects)) {
7521
- const n = strName15(obj.name);
7930
+ const n = strName16(obj.name);
7522
7931
  if (n) ownObjects.add(n);
7523
7932
  }
7524
7933
  const readable = /* @__PURE__ */ new Set();
@@ -7551,18 +7960,18 @@ var import_spec4 = require("@objectstack/spec");
7551
7960
  var import_system4 = require("@objectstack/spec/system");
7552
7961
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
7553
7962
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
7554
- function isRec20(v) {
7963
+ function isRec21(v) {
7555
7964
  return !!v && typeof v === "object" && !Array.isArray(v);
7556
7965
  }
7557
7966
  function asArray35(v) {
7558
7967
  if (Array.isArray(v)) return v;
7559
- if (isRec20(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec20(def) ? def : {} }));
7968
+ if (isRec21(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec21(def) ? def : {} }));
7560
7969
  return [];
7561
7970
  }
7562
- function strName16(v) {
7971
+ function strName17(v) {
7563
7972
  return typeof v === "string" && v.length > 0 ? v : void 0;
7564
7973
  }
7565
- function distance5(a, b) {
7974
+ function distance6(a, b) {
7566
7975
  const m = a.length;
7567
7976
  const n = b.length;
7568
7977
  if (m === 0) return n;
@@ -7578,7 +7987,7 @@ function distance5(a, b) {
7578
7987
  }
7579
7988
  return prev[n];
7580
7989
  }
7581
- function suggest6(target, known) {
7990
+ function suggest7(target, known) {
7582
7991
  const names = [...known];
7583
7992
  const segmentMatch = names.find(
7584
7993
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -7587,7 +7996,7 @@ function suggest6(target, known) {
7587
7996
  let best;
7588
7997
  let bestScore = Infinity;
7589
7998
  for (const candidate of names) {
7590
- const d = distance5(target, candidate);
7999
+ const d = distance6(target, candidate);
7591
8000
  if (d < bestScore) {
7592
8001
  bestScore = d;
7593
8002
  best = candidate;
@@ -7620,33 +8029,33 @@ function collectViewRecord(view, factsFor) {
7620
8029
  const addSections = (container, binding) => {
7621
8030
  if (!binding) return;
7622
8031
  for (const section of asArray35(container.sections)) {
7623
- const sectionName = strName16(section.name);
8032
+ const sectionName = strName17(section.name);
7624
8033
  if (sectionName) factsFor(binding).sections.add(sectionName);
7625
8034
  }
7626
8035
  };
7627
- const listBinding = isRec20(view.list) ? bindingOf(view.list) : void 0;
7628
- if (isRec20(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
7629
- addView(recordObject ?? listBinding, strName16(view.name));
8036
+ const listBinding = isRec21(view.list) ? bindingOf(view.list) : void 0;
8037
+ if (isRec21(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
8038
+ addView(recordObject ?? listBinding, strName17(view.name));
7630
8039
  const named = namedViewKeys(view);
7631
8040
  for (const family of ["listViews", "formViews"]) {
7632
8041
  const container = view[family];
7633
- if (!isRec20(container)) continue;
8042
+ if (!isRec21(container)) continue;
7634
8043
  const registryKeys = family === "listViews" ? named.list : named.form;
7635
8044
  let at = 0;
7636
8045
  for (const sub of Object.values(container)) {
7637
8046
  if (!sub || typeof sub !== "object") continue;
7638
8047
  const registryKey = registryKeys[at++];
7639
- if (!isRec20(sub)) continue;
8048
+ if (!isRec21(sub)) continue;
7640
8049
  const binding = bindingOf(sub) ?? listBinding;
7641
8050
  addView(binding, registryKey);
7642
8051
  addSections(sub, binding);
7643
8052
  }
7644
8053
  }
7645
- if (isRec20(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
8054
+ if (isRec21(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
7646
8055
  addSections(view, recordObject ?? listBinding);
7647
8056
  }
7648
8057
  function defaultListViewKey(object, container) {
7649
- if (!object || !isRec20(container.list)) return void 0;
8058
+ if (!object || !isRec21(container.list)) return void 0;
7650
8059
  const item = (0, import_spec4.expandViewContainer)(object, container).find(
7651
8060
  (i) => i.viewKind === "list" && i.isDefault
7652
8061
  );
@@ -7658,7 +8067,7 @@ function namedViewKeys(container) {
7658
8067
  const object = "probe";
7659
8068
  const prefix = `${object}.`;
7660
8069
  const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
7661
- const countEntries = (v) => isRec20(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
8070
+ const countEntries = (v) => isRec21(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
7662
8071
  const listCount = countEntries(container.listViews);
7663
8072
  const formCount = countEntries(container.formViews);
7664
8073
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -7676,14 +8085,14 @@ function readOptions(field) {
7676
8085
  values.add(opt);
7677
8086
  continue;
7678
8087
  }
7679
- if (!isRec20(opt)) continue;
7680
- const value = strName16(opt.value);
8088
+ if (!isRec21(opt)) continue;
8089
+ const value = strName17(opt.value);
7681
8090
  if (!value) continue;
7682
8091
  values.add(value);
7683
- const label2 = strName16(opt.label);
8092
+ const label2 = strName17(opt.label);
7684
8093
  if (label2) byLabel.set(label2.toLowerCase(), value);
7685
8094
  }
7686
- } else if (isRec20(raw)) {
8095
+ } else if (isRec21(raw)) {
7687
8096
  for (const [value, label2] of Object.entries(raw)) {
7688
8097
  values.add(value);
7689
8098
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -7704,23 +8113,23 @@ function buildUniverse(stack) {
7704
8113
  return facts;
7705
8114
  };
7706
8115
  for (const obj of asArray35(stack.objects)) {
7707
- const objectName = strName16(obj.name);
8116
+ const objectName = strName17(obj.name);
7708
8117
  if (!objectName) continue;
7709
8118
  const facts = factsFor(objectName);
7710
8119
  for (const field of asArray35(obj.fields)) {
7711
- const fieldName = strName16(field.name);
8120
+ const fieldName = strName17(field.name);
7712
8121
  if (fieldName) facts.fields.set(fieldName, field);
7713
8122
  }
7714
8123
  for (const action of asArray35(obj.actions)) {
7715
- const actionName = strName16(action.name);
8124
+ const actionName = strName17(action.name);
7716
8125
  if (actionName) facts.actions.set(actionName, action);
7717
8126
  }
7718
8127
  for (const view of asArray35(obj.views)) {
7719
- collectViewRecord({ ...view, object: strName16(view.object) ?? objectName }, factsFor);
8128
+ collectViewRecord({ ...view, object: strName17(view.object) ?? objectName }, factsFor);
7720
8129
  }
7721
8130
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
7722
8131
  for (const group of asArray35(obj.fieldGroups)) {
7723
- const key = strName16(group.key) ?? strName16(group.name);
8132
+ const key = strName17(group.key) ?? strName17(group.name);
7724
8133
  if (key) facts.sections.add(key);
7725
8134
  }
7726
8135
  }
@@ -7731,10 +8140,10 @@ function buildUniverse(stack) {
7731
8140
  for (let pi = 0; pi < pages.length; pi++) {
7732
8141
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
7733
8142
  if (!walked.objectName) continue;
7734
- const props = isRec20(walked.component.properties) ? walked.component.properties : void 0;
8143
+ const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
7735
8144
  if (!props) continue;
7736
8145
  for (const section of asArray35(props.sections)) {
7737
- const sectionName = strName16(section.name);
8146
+ const sectionName = strName17(section.name);
7738
8147
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
7739
8148
  }
7740
8149
  }
@@ -7742,9 +8151,9 @@ function buildUniverse(stack) {
7742
8151
  const globalActions = /* @__PURE__ */ new Map();
7743
8152
  const actionOwners = /* @__PURE__ */ new Map();
7744
8153
  for (const action of asArray35(stack.actions)) {
7745
- const actionName = strName16(action.name);
8154
+ const actionName = strName17(action.name);
7746
8155
  if (!actionName) continue;
7747
- const owner = strName16(action.objectName) ?? strName16(action.object);
8156
+ const owner = strName17(action.objectName) ?? strName17(action.object);
7748
8157
  if (owner) {
7749
8158
  factsFor(owner).actions.set(actionName, action);
7750
8159
  actionOwners.set(actionName, owner);
@@ -7759,19 +8168,19 @@ function buildUniverse(stack) {
7759
8168
  }
7760
8169
  const apps = /* @__PURE__ */ new Map();
7761
8170
  for (const app of asArray35(stack.apps)) {
7762
- const appName = strName16(app.name);
8171
+ const appName = strName17(app.name);
7763
8172
  if (!appName) continue;
7764
8173
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
7765
8174
  const walkNav = (items) => {
7766
8175
  for (const item of asArray35(items)) {
7767
- const id = strName16(item.id);
8176
+ const id = strName17(item.id);
7768
8177
  if (id) navIds.add(id);
7769
8178
  if (item.children) walkNav(item.children);
7770
8179
  }
7771
8180
  };
7772
8181
  walkNav(app.navigation);
7773
8182
  for (const area of asArray35(app.areas)) {
7774
- const areaId = strName16(area.id);
8183
+ const areaId = strName17(area.id);
7775
8184
  if (areaId) navIds.add(areaId);
7776
8185
  walkNav(area.navigation);
7777
8186
  }
@@ -7779,20 +8188,20 @@ function buildUniverse(stack) {
7779
8188
  }
7780
8189
  const dashboards = /* @__PURE__ */ new Map();
7781
8190
  for (const dash of asArray35(stack.dashboards)) {
7782
- const dashName = strName16(dash.name);
8191
+ const dashName = strName17(dash.name);
7783
8192
  if (!dashName) continue;
7784
8193
  const widgets = /* @__PURE__ */ new Set();
7785
8194
  for (const widget of asArray35(dash.widgets)) {
7786
- const id = strName16(widget.id) ?? strName16(widget.name);
8195
+ const id = strName17(widget.id) ?? strName17(widget.name);
7787
8196
  if (id) widgets.add(id);
7788
8197
  }
7789
8198
  const actions = /* @__PURE__ */ new Set();
7790
8199
  const headerActions = [
7791
- ...asArray35(isRec20(dash.header) ? dash.header.actions : void 0),
8200
+ ...asArray35(isRec21(dash.header) ? dash.header.actions : void 0),
7792
8201
  ...asArray35(dash.actions)
7793
8202
  ];
7794
8203
  for (const action of headerActions) {
7795
- const key = strName16(action.actionUrl) ?? strName16(action.url) ?? strName16(action.name);
8204
+ const key = strName17(action.actionUrl) ?? strName17(action.url) ?? strName17(action.name);
7796
8205
  if (key) actions.add(key);
7797
8206
  }
7798
8207
  dashboards.set(dashName, { widgets, actions });
@@ -7804,7 +8213,7 @@ function localePath(bundleIndex, locale) {
7804
8213
  }
7805
8214
  function validateTranslationReferences(stack) {
7806
8215
  const findings = [];
7807
- if (!isRec20(stack)) return findings;
8216
+ if (!isRec21(stack)) return findings;
7808
8217
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
7809
8218
  if (bundles.length === 0) return findings;
7810
8219
  const universe = buildUniverse(stack);
@@ -7813,13 +8222,13 @@ function validateTranslationReferences(stack) {
7813
8222
  };
7814
8223
  for (let bi = 0; bi < bundles.length; bi++) {
7815
8224
  const bundle = bundles[bi];
7816
- if (!isRec20(bundle)) continue;
8225
+ if (!isRec21(bundle)) continue;
7817
8226
  for (const [locale, rawData] of Object.entries(bundle)) {
7818
- if (!isRec20(rawData)) continue;
8227
+ if (!isRec21(rawData)) continue;
7819
8228
  const base = localePath(bi, locale);
7820
8229
  const inLocale = `locale "${locale}"`;
7821
8230
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
7822
- if (!isRec20(rawNode)) continue;
8231
+ if (!isRec21(rawNode)) continue;
7823
8232
  const objPath = `${base}.objects.${objectName}`;
7824
8233
  const facts = universe.objects.get(objectName);
7825
8234
  if (!facts) {
@@ -7827,7 +8236,7 @@ function validateTranslationReferences(stack) {
7827
8236
  orphan(
7828
8237
  `${inLocale} \xB7 object "${objectName}"`,
7829
8238
  objPath,
7830
- (0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest6(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest6(objectName, universe.objects.keys()),
8239
+ (0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest7(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest7(objectName, universe.objects.keys()),
7831
8240
  `Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
7832
8241
  );
7833
8242
  continue;
@@ -7840,12 +8249,12 @@ function validateTranslationReferences(stack) {
7840
8249
  orphan(
7841
8250
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
7842
8251
  fieldPath,
7843
- `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest6(fieldName, facts.fields.keys()),
8252
+ `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest7(fieldName, facts.fields.keys()),
7844
8253
  `Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
7845
8254
  );
7846
8255
  continue;
7847
8256
  }
7848
- if (!isRec20(rawField)) continue;
8257
+ if (!isRec21(rawField)) continue;
7849
8258
  checkOptionKeys(findings, {
7850
8259
  optionMap: rawField.options,
7851
8260
  field,
@@ -7860,7 +8269,7 @@ function validateTranslationReferences(stack) {
7860
8269
  orphan(
7861
8270
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
7862
8271
  `${objPath}._views.${viewName}`,
7863
- `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest6(viewName, facts.views),
8272
+ `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest7(viewName, facts.views),
7864
8273
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
7865
8274
  );
7866
8275
  }
@@ -7869,7 +8278,7 @@ function validateTranslationReferences(stack) {
7869
8278
  orphan(
7870
8279
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
7871
8280
  `${objPath}._sections.${sectionName}`,
7872
- `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest6(sectionName, facts.sections),
8281
+ `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest7(sectionName, facts.sections),
7873
8282
  `Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
7874
8283
  );
7875
8284
  }
@@ -7880,7 +8289,7 @@ function validateTranslationReferences(stack) {
7880
8289
  orphan(
7881
8290
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
7882
8291
  actionPath,
7883
- `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest6(actionName, facts.actions.keys()),
8292
+ `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest7(actionName, facts.actions.keys()),
7884
8293
  `Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
7885
8294
  );
7886
8295
  continue;
@@ -7902,7 +8311,7 @@ function validateTranslationReferences(stack) {
7902
8311
  orphan(
7903
8312
  `${inLocale} \xB7 global action "${actionName}"`,
7904
8313
  actionPath,
7905
- owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest6(actionName, universe.globalActions.keys()),
8314
+ owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest7(actionName, universe.globalActions.keys()),
7906
8315
  owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
7907
8316
  );
7908
8317
  continue;
@@ -7922,18 +8331,18 @@ function validateTranslationReferences(stack) {
7922
8331
  orphan(
7923
8332
  `${inLocale} \xB7 app "${appName}"`,
7924
8333
  appPath,
7925
- `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest6(appName, universe.apps.keys()),
8334
+ `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest7(appName, universe.apps.keys()),
7926
8335
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
7927
8336
  );
7928
8337
  continue;
7929
8338
  }
7930
- if (!isRec20(rawApp)) continue;
8339
+ if (!isRec21(rawApp)) continue;
7931
8340
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
7932
8341
  if (navIds.has(navId)) continue;
7933
8342
  orphan(
7934
8343
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
7935
8344
  `${appPath}.navigation.${navId}`,
7936
- `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
8345
+ `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
7937
8346
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
7938
8347
  );
7939
8348
  }
@@ -7945,18 +8354,18 @@ function validateTranslationReferences(stack) {
7945
8354
  orphan(
7946
8355
  `${inLocale} \xB7 dashboard "${dashName}"`,
7947
8356
  dashPath,
7948
- `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest6(dashName, universe.dashboards.keys()),
8357
+ `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest7(dashName, universe.dashboards.keys()),
7949
8358
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
7950
8359
  );
7951
8360
  continue;
7952
8361
  }
7953
- if (!isRec20(rawDash)) continue;
8362
+ if (!isRec21(rawDash)) continue;
7954
8363
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
7955
8364
  if (dash.widgets.has(widgetId)) continue;
7956
8365
  orphan(
7957
8366
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
7958
8367
  `${dashPath}.widgets.${widgetId}`,
7959
- `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
8368
+ `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
7960
8369
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
7961
8370
  );
7962
8371
  }
@@ -7965,7 +8374,7 @@ function validateTranslationReferences(stack) {
7965
8374
  orphan(
7966
8375
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
7967
8376
  `${dashPath}.actions.${actionKey}`,
7968
- `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
8377
+ `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
7969
8378
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
7970
8379
  );
7971
8380
  }
@@ -7975,7 +8384,7 @@ function validateTranslationReferences(stack) {
7975
8384
  return findings;
7976
8385
  }
7977
8386
  function asRecord(v) {
7978
- return isRec20(v) ? v : {};
8387
+ return isRec21(v) ? v : {};
7979
8388
  }
7980
8389
  function checkOptionKeys(findings, ctx) {
7981
8390
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -7987,7 +8396,7 @@ function checkOptionKeys(findings, ctx) {
7987
8396
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
7988
8397
  where: ctx.where,
7989
8398
  path: ctx.path,
7990
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName16(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
8399
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName17(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
7991
8400
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
7992
8401
  });
7993
8402
  return;
@@ -8000,17 +8409,17 @@ function checkOptionKeys(findings, ctx) {
8000
8409
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
8001
8410
  where: ctx.where,
8002
8411
  path: `${ctx.path}.${key}`,
8003
- message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest6(key, declared.values),
8412
+ message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest7(key, declared.values),
8004
8413
  hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
8005
8414
  });
8006
8415
  }
8007
8416
  }
8008
8417
  function checkActionParams(findings, ctx) {
8009
- const rawParams = Object.keys(asRecord(isRec20(ctx.rawAction) ? ctx.rawAction.params : void 0));
8418
+ const rawParams = Object.keys(asRecord(isRec21(ctx.rawAction) ? ctx.rawAction.params : void 0));
8010
8419
  if (rawParams.length === 0) return;
8011
8420
  const declared = /* @__PURE__ */ new Set();
8012
8421
  for (const param of asArray35(ctx.action.params)) {
8013
- const name = strName16(param.name) ?? strName16(param.field);
8422
+ const name = strName17(param.name) ?? strName17(param.field);
8014
8423
  if (name) declared.add(name);
8015
8424
  }
8016
8425
  for (const paramName of rawParams) {
@@ -8020,7 +8429,7 @@ function checkActionParams(findings, ctx) {
8020
8429
  rule: TRANSLATION_TARGET_UNKNOWN,
8021
8430
  where: `${ctx.where} \xB7 param "${paramName}"`,
8022
8431
  path: `${ctx.path}.params.${paramName}`,
8023
- message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest6(paramName, declared),
8432
+ message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest7(paramName, declared),
8024
8433
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
8025
8434
  });
8026
8435
  }
@@ -8028,14 +8437,14 @@ function checkActionParams(findings, ctx) {
8028
8437
 
8029
8438
  // src/validate-translatable-sections.ts
8030
8439
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
8031
- function isRec21(v) {
8440
+ function isRec22(v) {
8032
8441
  return !!v && typeof v === "object" && !Array.isArray(v);
8033
8442
  }
8034
- function strName17(v) {
8443
+ function strName18(v) {
8035
8444
  return typeof v === "string" && v.length > 0 ? v : void 0;
8036
8445
  }
8037
8446
  function viewLabel(view) {
8038
- const name = strName17(view.name);
8447
+ const name = strName18(view.name);
8039
8448
  return name ? `view "${name}"` : "";
8040
8449
  }
8041
8450
  function joinWhere(...parts) {
@@ -8043,7 +8452,7 @@ function joinWhere(...parts) {
8043
8452
  }
8044
8453
  function collectViewSites(view, basePath, label2, sites) {
8045
8454
  const recordObject = viewObjectName(view);
8046
- const listBinding = isRec21(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
8455
+ const listBinding = isRec22(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
8047
8456
  for (const site of viewContainerSites(view, basePath)) {
8048
8457
  sites.push({
8049
8458
  path: `${site.path}.sections`,
@@ -8057,11 +8466,11 @@ function translatedObjectNames(stack) {
8057
8466
  const out = /* @__PURE__ */ new Set();
8058
8467
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
8059
8468
  for (const bundle of bundles) {
8060
- if (!isRec21(bundle)) continue;
8469
+ if (!isRec22(bundle)) continue;
8061
8470
  for (const data of Object.values(bundle)) {
8062
- if (!isRec21(data) || !isRec21(data.objects)) continue;
8471
+ if (!isRec22(data) || !isRec22(data.objects)) continue;
8063
8472
  for (const [objectName, node] of Object.entries(data.objects)) {
8064
- if (isRec21(node)) out.add(objectName);
8473
+ if (isRec22(node)) out.add(objectName);
8065
8474
  }
8066
8475
  }
8067
8476
  }
@@ -8073,22 +8482,22 @@ function suggestedName(label2) {
8073
8482
  }
8074
8483
  function validateTranslatableSections(stack) {
8075
8484
  const findings = [];
8076
- if (!isRec21(stack)) return findings;
8485
+ if (!isRec22(stack)) return findings;
8077
8486
  const translated = translatedObjectNames(stack);
8078
8487
  if (translated.size === 0) return findings;
8079
8488
  const sites = [];
8080
8489
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
8081
- const objectName = strName17(obj.name);
8490
+ const objectName = strName18(obj.name);
8082
8491
  if (!objectName) continue;
8083
8492
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
8084
8493
  collectViewSites(
8085
- { ...view, object: strName17(view.object) ?? objectName },
8494
+ { ...view, object: strName18(view.object) ?? objectName },
8086
8495
  path,
8087
8496
  viewLabel(view),
8088
8497
  sites
8089
8498
  );
8090
8499
  }
8091
- if (isRec21(obj.listViews)) {
8500
+ if (isRec22(obj.listViews)) {
8092
8501
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
8093
8502
  }
8094
8503
  }
@@ -8096,13 +8505,13 @@ function validateTranslatableSections(stack) {
8096
8505
  collectViewSites(view, path, viewLabel(view), sites);
8097
8506
  }
8098
8507
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
8099
- const pageName = strName17(page.name);
8508
+ const pageName = strName18(page.name);
8100
8509
  const pageLabel = pageName ? `page "${pageName}"` : "";
8101
8510
  for (const walked of walkPageComponents(page, pagePath)) {
8102
8511
  if (!walked.objectName) continue;
8103
- const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
8512
+ const props = isRec22(walked.component.properties) ? walked.component.properties : void 0;
8104
8513
  if (!props) continue;
8105
- const type = strName17(walked.component.type) ?? "component";
8514
+ const type = strName18(walked.component.type) ?? "component";
8106
8515
  sites.push({
8107
8516
  path: `${walked.path}.properties.sections`,
8108
8517
  surface: joinWhere(pageLabel, type),
@@ -8117,9 +8526,9 @@ function validateTranslatableSections(stack) {
8117
8526
  if (!Array.isArray(site.sections)) continue;
8118
8527
  for (let i = 0; i < site.sections.length; i++) {
8119
8528
  const section = site.sections[i];
8120
- if (!isRec21(section)) continue;
8121
- if (strName17(section.name)) continue;
8122
- const heading = strName17(section.label);
8529
+ if (!isRec22(section)) continue;
8530
+ if (strName18(section.name)) continue;
8531
+ const heading = strName18(section.label);
8123
8532
  if (!heading) continue;
8124
8533
  const slug = suggestedName(heading);
8125
8534
  findings.push({
@@ -8144,7 +8553,7 @@ function asArray36(v) {
8144
8553
  }
8145
8554
  return [];
8146
8555
  }
8147
- function strName18(v) {
8556
+ function strName19(v) {
8148
8557
  return typeof v === "string" && v.length > 0 ? v : void 0;
8149
8558
  }
8150
8559
  function surfaceOf(v) {
@@ -8155,17 +8564,17 @@ function validateAiSurfaceAffinity(stack) {
8155
8564
  if (!stack || typeof stack !== "object") return findings;
8156
8565
  const skillsByName = /* @__PURE__ */ new Map();
8157
8566
  for (const skill of asArray36(stack.skills)) {
8158
- const n = strName18(skill.name);
8567
+ const n = strName19(skill.name);
8159
8568
  if (n) skillsByName.set(n, skill);
8160
8569
  }
8161
8570
  const agents = asArray36(stack.agents);
8162
8571
  for (let ai = 0; ai < agents.length; ai++) {
8163
8572
  const agent = agents[ai];
8164
- const agentName = strName18(agent.name) ?? `#${ai}`;
8573
+ const agentName = strName19(agent.name) ?? `#${ai}`;
8165
8574
  const agentSurface = surfaceOf(agent.surface);
8166
8575
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
8167
8576
  for (let si = 0; si < skillRefs.length; si++) {
8168
- const ref = strName18(skillRefs[si]);
8577
+ const ref = strName19(skillRefs[si]);
8169
8578
  if (!ref) continue;
8170
8579
  const skill = skillsByName.get(ref);
8171
8580
  if (!skill) continue;
@@ -8194,10 +8603,10 @@ function asArray37(v) {
8194
8603
  }
8195
8604
  return [];
8196
8605
  }
8197
- function strName19(v) {
8606
+ function strName20(v) {
8198
8607
  return typeof v === "string" && v.length > 0 ? v : void 0;
8199
8608
  }
8200
- function distance6(a, b) {
8609
+ function distance7(a, b) {
8201
8610
  const m = a.length;
8202
8611
  const n = b.length;
8203
8612
  if (m === 0) return n;
@@ -8213,14 +8622,14 @@ function distance6(a, b) {
8213
8622
  }
8214
8623
  return prev[n];
8215
8624
  }
8216
- function suggest7(target, known) {
8625
+ function suggest8(target, known) {
8217
8626
  for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
8218
8627
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
8219
8628
  }
8220
8629
  let best;
8221
8630
  let bestScore = Infinity;
8222
8631
  for (const candidate of known) {
8223
- const d = distance6(target, candidate);
8632
+ const d = distance7(target, candidate);
8224
8633
  if (d < bestScore) {
8225
8634
  bestScore = d;
8226
8635
  best = candidate;
@@ -8235,8 +8644,8 @@ function materialisesAsTool(action) {
8235
8644
  if (!ai || typeof ai !== "object") return false;
8236
8645
  const aiRec = ai;
8237
8646
  if (aiRec.exposed !== true) return false;
8238
- if (!strName19(aiRec.description)) return false;
8239
- const type = strName19(action.type);
8647
+ if (!strName20(aiRec.description)) return false;
8648
+ const type = strName20(action.type);
8240
8649
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
8241
8650
  if (type === "script") return Boolean(action.target || action.body);
8242
8651
  return Boolean(action.target);
@@ -8244,12 +8653,12 @@ function materialisesAsTool(action) {
8244
8653
  function collectToolUniverse(stack) {
8245
8654
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
8246
8655
  for (const tool of asArray37(stack.tools)) {
8247
- const n = strName19(tool.name);
8656
+ const n = strName20(tool.name);
8248
8657
  if (n) universe.add(n);
8249
8658
  }
8250
8659
  const addActionFamily = (actions) => {
8251
8660
  for (const action of asArray37(actions)) {
8252
- const n = strName19(action.name);
8661
+ const n = strName20(action.name);
8253
8662
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
8254
8663
  }
8255
8664
  };
@@ -8263,7 +8672,7 @@ function collectUnexposedActionNames(stack) {
8263
8672
  const names = /* @__PURE__ */ new Set();
8264
8673
  const scan = (actions) => {
8265
8674
  for (const action of asArray37(actions)) {
8266
- const n = strName19(action.name);
8675
+ const n = strName20(action.name);
8267
8676
  if (n && !materialisesAsTool(action)) names.add(n);
8268
8677
  }
8269
8678
  };
@@ -8289,10 +8698,10 @@ function validateAiToolReferences(stack) {
8289
8698
  const skills = asArray37(stack.skills);
8290
8699
  for (let si = 0; si < skills.length; si++) {
8291
8700
  const skill = skills[si];
8292
- const skillName = strName19(skill.name) ?? `#${si}`;
8701
+ const skillName = strName20(skill.name) ?? `#${si}`;
8293
8702
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
8294
8703
  for (let ti = 0; ti < refs.length; ti++) {
8295
- const ref = strName19(refs[ti]);
8704
+ const ref = strName20(refs[ti]);
8296
8705
  if (!ref || resolves(ref)) continue;
8297
8706
  const isPattern = ref.endsWith("*");
8298
8707
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -8301,7 +8710,7 @@ function validateAiToolReferences(stack) {
8301
8710
  rule: AI_SKILL_TOOL_UNRESOLVED,
8302
8711
  where: `skill "${skillName}" \xB7 tools`,
8303
8712
  path: `skills[${si}].tools[${ti}]`,
8304
- message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest7(ref, universe),
8713
+ message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest8(ref, universe),
8305
8714
  hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${import_system5.PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
8306
8715
  });
8307
8716
  }
@@ -8319,7 +8728,7 @@ function asArray38(v) {
8319
8728
  }
8320
8729
  return [];
8321
8730
  }
8322
- function strName20(v) {
8731
+ function strName21(v) {
8323
8732
  return typeof v === "string" && v.length > 0 ? v : void 0;
8324
8733
  }
8325
8734
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -8329,7 +8738,7 @@ function validateAiAgentAuthoring(stack) {
8329
8738
  const agents = asArray38(stack.agents);
8330
8739
  for (let ai = 0; ai < agents.length; ai++) {
8331
8740
  const agent = agents[ai];
8332
- const name = strName20(agent.name) ?? `#${ai}`;
8741
+ const name = strName21(agent.name) ?? `#${ai}`;
8333
8742
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
8334
8743
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
8335
8744
  findings.push({
@@ -8345,9 +8754,9 @@ function validateAiAgentAuthoring(stack) {
8345
8754
  const apps = asArray38(stack.apps);
8346
8755
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
8347
8756
  const app = apps[appIdx];
8348
- const defaultAgent = strName20(app.defaultAgent);
8757
+ const defaultAgent = strName21(app.defaultAgent);
8349
8758
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
8350
- const appName = strName20(app.name) ?? `#${appIdx}`;
8759
+ const appName = strName21(app.name) ?? `#${appIdx}`;
8351
8760
  findings.push({
8352
8761
  severity: "warning",
8353
8762
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -8378,6 +8787,7 @@ function loadTypeScript3() {
8378
8787
  return cachedTs3;
8379
8788
  }
8380
8789
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
8790
+ var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
8381
8791
  var HOOK_BODY_WRITE_PATTERNS = [
8382
8792
  {
8383
8793
  id: "input-property-assign",
@@ -8450,13 +8860,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
8450
8860
  "owner",
8451
8861
  "record_type"
8452
8862
  ]);
8453
- var isRec22 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8863
+ function unprovisionedAnchorWriteConsequence() {
8864
+ return `so the value can never land: the anchor exists only in the registered schema, which is what carries it PAST the write-path validator that refuses an undeclared name outright (INVALID_FIELD). The remote database is what rejects it \u2014 on a SQL remote with an untyped driver error ('no such column') that aborts the whole statement, so the correctly named fields in the same payload never land either; on a schemaless remote the key is persisted into a column no read surface returns (#4271).`;
8865
+ }
8866
+ var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8454
8867
  function asArray39(v) {
8455
- if (Array.isArray(v)) return v.filter((x) => isRec22(x));
8456
- if (isRec22(v)) {
8868
+ if (Array.isArray(v)) return v.filter((x) => isRec23(x));
8869
+ if (isRec23(v)) {
8457
8870
  return Object.entries(v).map(([name, def]) => ({
8458
8871
  name,
8459
- ...isRec22(def) ? def : {}
8872
+ ...isRec23(def) ? def : {}
8460
8873
  }));
8461
8874
  }
8462
8875
  return [];
@@ -8600,14 +9013,16 @@ function validateHookBodyWrites(stack) {
8600
9013
  const hooks = asArray39(stack.hooks);
8601
9014
  if (hooks.length === 0) return findings;
8602
9015
  let objectFields = null;
9016
+ let anchors = null;
8603
9017
  hooks.forEach((hook, hookIndex) => {
8604
9018
  const body = hook.body;
8605
- if (!isRec22(body) || body.language !== "js") return;
9019
+ if (!isRec23(body) || body.language !== "js") return;
8606
9020
  const source = body.source;
8607
9021
  if (typeof source !== "string" || source.trim() === "") return;
8608
9022
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
8609
9023
  if (writes.length === 0) return;
8610
9024
  objectFields ?? (objectFields = indexObjectFields2(stack));
9025
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8611
9026
  const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
8612
9027
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
8613
9028
  (o) => typeof o === "string" && o.trim() !== ""
@@ -8622,7 +9037,20 @@ function validateHookBodyWrites(stack) {
8622
9037
  if (reported.has(dedupeKey)) continue;
8623
9038
  if (w.object === void 0) {
8624
9039
  if (!inputJudgeable) continue;
8625
- if (IMPLICIT_FIELDS2.has(w.field)) continue;
9040
+ if (IMPLICIT_FIELDS2.has(w.field)) {
9041
+ if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
9042
+ reported.add(dedupeKey);
9043
+ const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
9044
+ findings.push({
9045
+ severity: "warning",
9046
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
9047
+ where,
9048
+ path,
9049
+ message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
9050
+ hint: unprovisionedAnchorHint(anchorObj, w.field)
9051
+ });
9052
+ continue;
9053
+ }
8626
9054
  if (targetSets.some((s) => s.has(w.field))) continue;
8627
9055
  reported.add(dedupeKey);
8628
9056
  const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
@@ -8638,7 +9066,20 @@ function validateHookBodyWrites(stack) {
8638
9066
  } else {
8639
9067
  const known = judgeableFieldsOf(objectFields, w.object);
8640
9068
  if (!known) continue;
8641
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
9069
+ if (known.has(w.field)) continue;
9070
+ if (IMPLICIT_FIELDS2.has(w.field)) {
9071
+ if (!anchors.get(w.object)?.has(w.field)) continue;
9072
+ reported.add(dedupeKey);
9073
+ findings.push({
9074
+ severity: "warning",
9075
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
9076
+ where,
9077
+ path,
9078
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
9079
+ hint: unprovisionedAnchorHint(w.object, w.field)
9080
+ });
9081
+ continue;
9082
+ }
8642
9083
  reported.add(dedupeKey);
8643
9084
  findings.push({
8644
9085
  severity: "warning",
@@ -8667,6 +9108,7 @@ function fixHint(field, declared) {
8667
9108
  var import_shared2 = require("@objectstack/spec/shared");
8668
9109
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
8669
9110
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
9111
+ var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
8670
9112
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
8671
9113
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
8672
9114
  var ACTION_BODY_WRITE_EXCLUSIONS = [
@@ -8683,13 +9125,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
8683
9125
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
8684
9126
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
8685
9127
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
8686
- var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9128
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8687
9129
  function asArray40(v) {
8688
- if (Array.isArray(v)) return v.filter((x) => isRec23(x));
8689
- if (isRec23(v)) {
9130
+ if (Array.isArray(v)) return v.filter((x) => isRec24(x));
9131
+ if (isRec24(v)) {
8690
9132
  return Object.entries(v).map(([name, def]) => ({
8691
9133
  name,
8692
- ...isRec23(def) ? def : {}
9134
+ ...isRec24(def) ? def : {}
8693
9135
  }));
8694
9136
  }
8695
9137
  return [];
@@ -8707,7 +9149,7 @@ function collectActionBodies(stack) {
8707
9149
  const type = typeof action.type === "string" ? action.type : "script";
8708
9150
  if (type !== "script") return;
8709
9151
  const body = action.body;
8710
- if (!isRec23(body) || body.language !== "js") return;
9152
+ if (!isRec24(body) || body.language !== "js") return;
8711
9153
  const source = body.source;
8712
9154
  if (typeof source !== "string" || source.trim() === "") return;
8713
9155
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -8726,10 +9168,11 @@ function collectActionBodies(stack) {
8726
9168
  }
8727
9169
  function validateActionBodyWrites(stack) {
8728
9170
  const findings = [];
8729
- if (!isRec23(stack)) return findings;
9171
+ if (!isRec24(stack)) return findings;
8730
9172
  const sites = collectActionBodies(stack);
8731
9173
  if (sites.length === 0) return findings;
8732
9174
  let objectFields = null;
9175
+ let anchors = null;
8733
9176
  for (const site of sites) {
8734
9177
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
8735
9178
  const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
@@ -8754,6 +9197,7 @@ function validateActionBodyWrites(stack) {
8754
9197
  }
8755
9198
  if (writes.length === 0) continue;
8756
9199
  objectFields ?? (objectFields = indexObjectFields2(stack));
9200
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8757
9201
  const reported = /* @__PURE__ */ new Set();
8758
9202
  for (const w of writes) {
8759
9203
  if (w.object === void 0) continue;
@@ -8761,7 +9205,20 @@ function validateActionBodyWrites(stack) {
8761
9205
  if (reported.has(dedupeKey)) continue;
8762
9206
  const known = judgeableFieldsOf(objectFields, w.object);
8763
9207
  if (!known) continue;
8764
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
9208
+ if (known.has(w.field)) continue;
9209
+ if (IMPLICIT_FIELDS2.has(w.field)) {
9210
+ if (!anchors.get(w.object)?.has(w.field)) continue;
9211
+ reported.add(dedupeKey);
9212
+ findings.push({
9213
+ severity: "warning",
9214
+ rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
9215
+ where,
9216
+ path: site.path,
9217
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
9218
+ hint: unprovisionedAnchorHint(w.object, w.field)
9219
+ });
9220
+ continue;
9221
+ }
8765
9222
  reported.add(dedupeKey);
8766
9223
  findings.push({
8767
9224
  severity: "warning",
@@ -8783,15 +9240,16 @@ function fixHint2(field, declared) {
8783
9240
  // src/validate-flow-node-writes.ts
8784
9241
  var import_shared3 = require("@objectstack/spec/shared");
8785
9242
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
9243
+ var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
8786
9244
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
8787
9245
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
8788
- var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9246
+ var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8789
9247
  function asArray41(v) {
8790
- if (Array.isArray(v)) return v.filter((x) => isRec24(x));
8791
- if (isRec24(v)) {
9248
+ if (Array.isArray(v)) return v.filter((x) => isRec25(x));
9249
+ if (isRec25(v)) {
8792
9250
  return Object.entries(v).map(([name, def]) => ({
8793
9251
  name,
8794
- ...isRec24(def) ? def : {}
9252
+ ...isRec25(def) ? def : {}
8795
9253
  }));
8796
9254
  }
8797
9255
  return [];
@@ -8804,30 +9262,44 @@ function readLiteralObjectName2(config) {
8804
9262
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
8805
9263
  function validateFlowNodeWrites(stack) {
8806
9264
  const findings = [];
8807
- if (!isRec24(stack)) return findings;
9265
+ if (!isRec25(stack)) return findings;
8808
9266
  const flows = asArray41(stack.flows);
8809
9267
  if (flows.length === 0) return findings;
8810
9268
  let objectFields = null;
9269
+ let anchors = null;
8811
9270
  flows.forEach((flow, flowIndex) => {
8812
9271
  const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
8813
9272
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
8814
9273
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
8815
9274
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
8816
- const config = isRec24(node.config) ? node.config : void 0;
9275
+ const config = isRec25(node.config) ? node.config : void 0;
8817
9276
  if (!config) return;
8818
9277
  const fields = config.fields;
8819
- if (!isRec24(fields)) return;
9278
+ if (!isRec25(fields)) return;
8820
9279
  const written = Object.keys(fields);
8821
9280
  if (written.length === 0) return;
8822
9281
  const objectName = readLiteralObjectName2(config);
8823
9282
  if (!objectName) return;
8824
9283
  objectFields ?? (objectFields = indexObjectFields2(stack));
9284
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8825
9285
  const known = judgeableFieldsOf(objectFields, objectName);
8826
9286
  if (!known) return;
8827
9287
  const nodeName = flowNodeLabel(node, walkIndex);
8828
9288
  const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
8829
9289
  for (const fieldName of written) {
8830
- if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
9290
+ if (known.has(fieldName)) continue;
9291
+ if (IMPLICIT_FIELDS2.has(fieldName)) {
9292
+ if (!anchors.get(objectName)?.has(fieldName)) continue;
9293
+ findings.push({
9294
+ severity: "warning",
9295
+ rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
9296
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
9297
+ path: `${nodePath}.config.fields.${fieldName}`,
9298
+ message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
9299
+ hint: unprovisionedAnchorHint(objectName, fieldName)
9300
+ });
9301
+ continue;
9302
+ }
8831
9303
  if (fieldName.includes(".")) continue;
8832
9304
  findings.push({
8833
9305
  severity: "error",
@@ -8851,6 +9323,13 @@ function fixHint3(field, declared) {
8851
9323
  var REFERENCE_INTEGRITY_RULES = [
8852
9324
  { name: "validateObjectReferences", run: validateObjectReferences },
8853
9325
  { name: "validateSearchableFields", run: validateSearchableFields },
9326
+ // [#9257] The same reading, one axis over: a list view's `sort` is a field
9327
+ // name written in metadata, resolved against the object's declared fields. It
9328
+ // gates (`error`) because the runtime does not tolerate a bad one at all —
9329
+ // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
9330
+ // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
9331
+ // the refusal is the whole view, on every load, traced to nothing.
9332
+ { name: "validateSortableFields", run: validateSortableFields },
8854
9333
  { name: "validateActionNameRefs", run: validateActionNameRefs },
8855
9334
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
8856
9335
  { name: "validateChartBindings", run: validateChartBindings },
@@ -8966,7 +9445,7 @@ function validateReferenceIntegrity(stack) {
8966
9445
 
8967
9446
  // src/lint-flow-patterns.ts
8968
9447
  var import_automation5 = require("@objectstack/spec/automation");
8969
- var import_data10 = require("@objectstack/spec/data");
9448
+ var import_data12 = require("@objectstack/spec/data");
8970
9449
  function asArray42(v) {
8971
9450
  if (Array.isArray(v)) return v;
8972
9451
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -9241,7 +9720,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
9241
9720
  function filterCarriesNoCondition(filter) {
9242
9721
  if (filter === void 0 || filter === null) return true;
9243
9722
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
9244
- return (0, import_data10.reduceFilterVerdict)(filter) === "true";
9723
+ return (0, import_data12.reduceFilterVerdict)(filter) === "true";
9245
9724
  }
9246
9725
  function describeUnboundedFilter(filter) {
9247
9726
  if (filter === void 0 || filter === null) return "no `filter` key";
@@ -9608,7 +10087,7 @@ function lintLivenessProperties(stack) {
9608
10087
  }
9609
10088
 
9610
10089
  // src/lint-autonumber-formats.ts
9611
- var import_data11 = require("@objectstack/spec/data");
10090
+ var import_data13 = require("@objectstack/spec/data");
9612
10091
  var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
9613
10092
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
9614
10093
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
@@ -9634,8 +10113,8 @@ function lintAutonumberFormats(stack) {
9634
10113
  const name = typeof f.name === "string" ? f.name : "(unnamed field)";
9635
10114
  const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
9636
10115
  if (!fmt) continue;
9637
- const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
9638
- const refs = (0, import_data11.referencedFields)(tokens);
10116
+ const tokens = (0, import_data13.parseAutonumberFormat)(fmt);
10117
+ const refs = (0, import_data13.referencedFields)(tokens);
9639
10118
  const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
9640
10119
  for (const t of tokens) {
9641
10120
  if (t.kind !== "literal") continue;
@@ -9790,6 +10269,17 @@ function lintViewRefs(stack) {
9790
10269
  }
9791
10270
 
9792
10271
  // src/data-model-rules.ts
10272
+ function objectWhere(obj) {
10273
+ return `object "${obj?.name}"`;
10274
+ }
10275
+ function indexWhere(obj, idx, j, cols) {
10276
+ const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
10277
+ const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
10278
+ return `${objectWhere(obj)} \xB7 index ${label2}`;
10279
+ }
10280
+ function fieldWhere(obj, fieldName) {
10281
+ return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
10282
+ }
9793
10283
  var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
9794
10284
  var NUMERIC_TYPES = /* @__PURE__ */ new Set([
9795
10285
  "number",
@@ -9862,11 +10352,13 @@ function lintUnscopedDeclaredIndexes(objects) {
9862
10352
  for (let j = 0; j < declaredIndexes.length; j++) {
9863
10353
  const idx = declaredIndexes[j];
9864
10354
  if (idx?.unique !== true) continue;
9865
- const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
10355
+ const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
10356
+ const cols = colList.join(", ");
9866
10357
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
9867
10358
  issues.push({
9868
10359
  severity: "warning",
9869
10360
  rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
10361
+ where: indexWhere(obj, idx, j, colList),
9870
10362
  message: `"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` \u2014 a unique index whose scope is unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; an author who meant "unique per organization" gets no per-organization constraint and no error. Protocol 18 rejects this spelling (#5082).`,
9871
10363
  path: `objects[${i}].indexes[${j}]`,
9872
10364
  fix: `State the scope: \`unique: 'global'\` (installation-wide \u2014 exactly today's behavior) or \`unique: 'organization'\` (one holder per organization \u2014 the driver prepends the NULL-safe organization key part at registration).`
@@ -9915,6 +10407,11 @@ function lintUniqueDeclarations(objects) {
9915
10407
  issues.push({
9916
10408
  severity: "warning",
9917
10409
  rule: UNIQUE_DOUBLE_DECLARATION,
10410
+ // More specific than `path` on purpose: this rule's finding is about ONE
10411
+ // column, but its `path` has always been the whole object (`objects[i]`)
10412
+ // because the defect straddles `fields.<name>.unique` and an entry of
10413
+ // `indexes`. `where` can name the column without picking one of the two.
10414
+ where: fieldWhere(obj, name),
9918
10415
  message,
9919
10416
  path: `objects[${i}]`,
9920
10417
  fix
@@ -9943,6 +10440,7 @@ function lintLegacyOrganizationComposites(objects) {
9943
10440
  issues.push({
9944
10441
  severity: "warning",
9945
10442
  rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
10443
+ where: indexWhere(obj, idx, j, cols),
9946
10444
  message: `"${obj.name}" declares index${indexLabel} [${cols.join(", ")}] with ${spelling} and lists the organization column '${tenantColumn}' itself \u2014 the hand-written per-organization composite that predates the scope vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) \u2014 which on a single-organization deployment is every row.`,
9947
10445
  path: `objects[${i}].indexes[${j}]`,
9948
10446
  fix: `State the scope instead: \`unique: 'organization'\` on this index (keep \`fields\` exactly as they are \u2014 the driver makes the listed '${tenantColumn}' NULL-safe in place rather than prepending a second organization key part). ${rest.length > 0 ? `The constraint then really is "one ${rest.join(" + ")} per organization". ` : ""}Opting in is a physical tightening: it surfaces as a \`recreate_index\` drift op gated by the duplicate pre-flight probe (ADR-0120 D4), so pre-existing duplicate NULL-organization rows block it with a report rather than failing a boot. Leaving it as-is stays valid indefinitely and forces no drift.`
@@ -10089,7 +10587,7 @@ var CLI_ONLY = ["cli"];
10089
10587
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
10090
10588
  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.";
10091
10589
  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.";
10092
- 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.";
10590
+ var RUNTIME_OBJECT_ADVISORY_VOLUME = "Advisory-tier object rule: it cannot refuse a write, and it is held off the runtime door for advisory VOLUME (~8 findings per object write measured on unswept metadata, rendered in Studio since #4717), not refusal risk. Crossing it is a UX decision with its own card (#4716).";
10093
10591
  var EXPRESSION_INVALID = "expression-invalid";
10094
10592
  var AUTHORING_RULES = [
10095
10593
  // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
@@ -10150,8 +10648,14 @@ var AUTHORING_RULES = [
10150
10648
  input: "normalized",
10151
10649
  commands: ALL,
10152
10650
  source: "packages/lint/src/validate-functional-completeness.ts",
10153
- surfaces: CLI_ONLY,
10154
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10651
+ // Runtime publish gate (#4716): the OBJECT write door — the five gating
10652
+ // object rules cross together under the 2026-08-18 adjudication. The
10653
+ // false-positive budget the crossing owed was exempted on a measured
10654
+ // 0 refusals / 75 real object declarations (authored config-file metadata,
10655
+ // so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
10656
+ // advisory-tier object rules deliberately do NOT ride.
10657
+ surfaces: CLI_AND_RUNTIME,
10658
+ runtimeTypes: ["object"],
10155
10659
  run: (stack) => validateFunctionalCompleteness(stack)
10156
10660
  },
10157
10661
  // [#7521, via cloud#1225] A managed object advertising a generic write verb
@@ -10175,8 +10679,11 @@ var AUTHORING_RULES = [
10175
10679
  input: "normalized",
10176
10680
  commands: ALL,
10177
10681
  source: "packages/lint/src/validate-managed-api-methods.ts",
10178
- surfaces: CLI_ONLY,
10179
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10682
+ // Runtime publish gate (#4716): a managed object advertising a verb its
10683
+ // own affordances refuse is exactly the contradiction a Studio/MCP author
10684
+ // can save today — the CLI sweep (#7934) never sees an overlay row.
10685
+ surfaces: CLI_AND_RUNTIME,
10686
+ runtimeTypes: ["object"],
10180
10687
  run: (stack) => validateManagedApiMethods(stack)
10181
10688
  },
10182
10689
  // A view container in `views: []` that registers zero views: nothing appears
@@ -10198,19 +10705,38 @@ var AUTHORING_RULES = [
10198
10705
  },
10199
10706
  // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
10200
10707
  // chartConfig axis/series must resolve against the declared datasets.
10708
+ //
10709
+ // Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
10710
+ // widget bound to a dataset that resolves to nothing sailed through save AND
10711
+ // publish — this rule provably caught the body and was simply never invoked,
10712
+ // because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
10713
+ // hold a forward reference; publishing refuses it with the key path named.
10714
+ // The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
10715
+ // — without it every legitimate board reads as dangling, the 3-phantom
10716
+ // measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
10717
+ // rule's error ids on the publish gate, not just `widget-dataset-unknown` —
10718
+ // ruled 2026-08-15: they are one coherent "this board cannot render"
10719
+ // reference-integrity class, and the ~6× wider accept-set narrowing was
10720
+ // accepted knowingly rather than splitting the dataset limb into its own
10721
+ // rule (traversal-duplication drift) or adding a per-finding-id surface
10722
+ // filter (registry machinery that weakens "delete a rule from the table and
10723
+ // enforcement stops in the same commit"). Warning-tier ids ride along on the
10724
+ // advisory channel and never block (#4463 P1).
10201
10725
  {
10202
10726
  name: "validateWidgetBindings",
10203
10727
  tier: "gating",
10204
10728
  input: "parsed",
10205
10729
  commands: ALL,
10206
10730
  source: "packages/lint/src/validate-widget-bindings.ts",
10207
- surfaces: CLI_ONLY,
10208
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10731
+ surfaces: CLI_AND_RUNTIME,
10732
+ runtimeTypes: ["dashboard"],
10209
10733
  run: (stack) => validateWidgetBindings(stack)
10210
10734
  },
10211
- // ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
10212
- // target that resolves to no defined action ships a button that renders and
10213
- // silently does nothing on click. Unresolved `url` routes stay advisory.
10735
+ // ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
10736
+ // button that renders and refuses (or does nothing) on click: a `script`
10737
+ // target must name a defined action, a `modal` target must name a declared
10738
+ // page (objectstack#6739-A — a modal string target names a PAGE, only).
10739
+ // Unresolved `url` routes stay advisory.
10214
10740
  {
10215
10741
  name: "validateDashboardActionRefs",
10216
10742
  tier: "gating",
@@ -10235,6 +10761,28 @@ var AUTHORING_RULES = [
10235
10761
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10236
10762
  run: (stack) => validateFilterTokens(stack)
10237
10763
  },
10764
+ // #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
10765
+ // name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
10766
+ // no layer: the engine refuses it on a declared temporal field at query time
10767
+ // (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
10768
+ // literal string. This is the authoring-time refusal the ruling shipped
10769
+ // alongside the engine door, judging the filter literal in isolation —
10770
+ // ordering positions only, all three authored filter shapes. Like
10771
+ // `validateEmptyCombinators` it needs NO resolution context, so
10772
+ // RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
10773
+ // for every filter-carrying type the gate already maps: the write path is
10774
+ // the one door an AI author uses, and dashboards/views are where the preset
10775
+ // vocabulary is near enough to reach for.
10776
+ {
10777
+ name: "validatePresetComparands",
10778
+ tier: "gating",
10779
+ input: "parsed",
10780
+ commands: ALL,
10781
+ source: "packages/lint/src/validate-preset-comparands.ts",
10782
+ surfaces: CLI_AND_RUNTIME,
10783
+ runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
10784
+ run: (stack) => validatePresetComparands(stack)
10785
+ },
10238
10786
  // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
10239
10787
  // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
10240
10788
  // this rule does not touch it: it refuses the literal SPELLINGS at authoring
@@ -10453,7 +11001,7 @@ var AUTHORING_RULES = [
10453
11001
  commands: ALL,
10454
11002
  source: "packages/lint/src/validate-record-title.ts",
10455
11003
  surfaces: CLI_ONLY,
10456
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11004
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10457
11005
  run: (stack) => validateRecordTitle(stack)
10458
11006
  },
10459
11007
  // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
@@ -10466,7 +11014,7 @@ var AUTHORING_RULES = [
10466
11014
  commands: ALL,
10467
11015
  source: "packages/lint/src/validate-semantic-roles.ts",
10468
11016
  surfaces: CLI_ONLY,
10469
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11017
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10470
11018
  run: (stack) => validateSemanticRoles(stack)
10471
11019
  },
10472
11020
  // #2578 / #4449 — a form section's field reference that resolves to nothing
@@ -10677,7 +11225,7 @@ var AUTHORING_RULES = [
10677
11225
  commands: ALL,
10678
11226
  source: "packages/lint/src/lint-liveness-properties.ts",
10679
11227
  surfaces: CLI_ONLY,
10680
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11228
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10681
11229
  run: (stack) => lintLivenessProperties(stack).map((f) => ({
10682
11230
  severity: "warning",
10683
11231
  rule: f.rule,
@@ -10696,8 +11244,12 @@ var AUTHORING_RULES = [
10696
11244
  input: "parsed",
10697
11245
  commands: ALL,
10698
11246
  source: "packages/lint/src/lint-autonumber-formats.ts",
10699
- surfaces: CLI_ONLY,
10700
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11247
+ // Runtime publish gate (#4716): an autonumber referencing a field the
11248
+ // object does not carry is broken from the first record; only the error
11249
+ // arm blocks — the optional-field arm is `warning` and rides the
11250
+ // advisory channel like every other non-error finding (#4463 P1).
11251
+ surfaces: CLI_AND_RUNTIME,
11252
+ runtimeTypes: ["object"],
10701
11253
  run: (stack) => lintAutonumberFormats(stack).map((f) => ({
10702
11254
  severity: f.severity,
10703
11255
  rule: f.rule,
@@ -10738,12 +11290,12 @@ var AUTHORING_RULES = [
10738
11290
  commands: ["validate", "build"],
10739
11291
  source: "packages/lint/src/data-model-rules.ts",
10740
11292
  surfaces: CLI_ONLY,
10741
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11293
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10742
11294
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly ahead of R10 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
10743
11295
  run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10744
11296
  severity: f.severity === "suggestion" ? "info" : f.severity,
10745
11297
  rule: f.rule,
10746
- where: f.path,
11298
+ where: f.where,
10747
11299
  path: f.path,
10748
11300
  message: f.message,
10749
11301
  hint: f.fix ?? ""
@@ -10759,12 +11311,12 @@ var AUTHORING_RULES = [
10759
11311
  commands: ["validate", "build"],
10760
11312
  source: "packages/lint/src/data-model-rules.ts",
10761
11313
  surfaces: CLI_ONLY,
10762
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11314
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10763
11315
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly as R10 of its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
10764
11316
  run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10765
11317
  severity: f.severity === "suggestion" ? "info" : f.severity,
10766
11318
  rule: f.rule,
10767
- where: f.path,
11319
+ where: f.where,
10768
11320
  path: f.path,
10769
11321
  message: f.message,
10770
11322
  hint: f.fix ?? ""
@@ -10781,12 +11333,12 @@ var AUTHORING_RULES = [
10781
11333
  commands: ["validate", "build"],
10782
11334
  source: "packages/lint/src/data-model-rules.ts",
10783
11335
  surfaces: CLI_ONLY,
10784
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11336
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10785
11337
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly alongside R10/R11 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
10786
11338
  run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10787
11339
  severity: f.severity === "suggestion" ? "info" : f.severity,
10788
11340
  rule: f.rule,
10789
- where: f.path,
11341
+ where: f.where,
10790
11342
  path: f.path,
10791
11343
  message: f.message,
10792
11344
  hint: f.fix ?? ""
@@ -10914,16 +11466,26 @@ var AUTHORING_RULES = [
10914
11466
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10915
11467
  run: (stack) => validateOrgAxisRedLines(stack)
10916
11468
  },
10917
- // #4698 — the "declared but never read" gate, for the one surface where the
10918
- // predicate is EXACT rather than inferred. A sharing rule's `condition` has a
10919
- // single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
10920
- // the key is `compileCelToFilter(condition, { variables: {} })`; a condition
10921
- // that does not lower means the rule is SKIPPED at boot, so the grant is
10922
- // declared and does not exist. The lint calls that same compiler, from the
10923
- // same package, with the same options the verdict cannot drift from the
10924
- // consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
10925
- // docblock states: the whole authorable surface is enforced, and this was the
10926
- // one field where that sentence was not yet true.
11469
+ // #4698 / #9698 — the "declared but never read" gate, for the two fields of a
11470
+ // sharing rule where the predicate is EXACT rather than inferred.
11471
+ //
11472
+ // - `condition` has a single runtime consumer
11473
+ // (`bootstrapDeclaredSharingRules`) whose only use of the key is
11474
+ // `compileCelToFilter(condition, { variables: {} })`; a condition that
11475
+ // does not lower means the rule is SKIPPED at boot.
11476
+ // - `object` decides whether the grant is refused outright: reconcile hands
11477
+ // each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
11478
+ // `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
11479
+ // `public` or it is a `controlled_by_parent` detail. Both are decidable
11480
+ // from authored metadata; the other arms of that verdict (`owner_id`, the
11481
+ // bypass set, federated anchors) are not, and are excluded by name.
11482
+ //
11483
+ // Either way the grant is declared and does not exist. The lint calls the
11484
+ // same compiler and mirrors the same verdict function, from the same inputs
11485
+ // — the verdict cannot drift from the consumers'. Gating for the ADR-0078
11486
+ // reason `SharingRuleSchema`'s own docblock states: the whole authorable
11487
+ // surface is enforced, and these were the fields where that sentence was not
11488
+ // yet true.
10927
11489
  {
10928
11490
  name: "validateSharingRuleEnforceability",
10929
11491
  tier: "gating",
@@ -10931,7 +11493,7 @@ var AUTHORING_RULES = [
10931
11493
  commands: ALL,
10932
11494
  source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
10933
11495
  surfaces: CLI_ONLY,
10934
- surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. The rule itself is snapshot-safe \u2014 it reads ONLY `stack.sharingRules[].condition` and needs no other collection \u2014 so widening it here is a `runtimeTypes: ['sharing_rule']` edit once the gate accepts that type, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
11496
+ surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. This entry used to add that the rule reads ONLY `stack.sharingRules[].condition` and needs no other collection, so crossing was a lone `runtimeTypes` edit. #9698 FALSIFIED that: the anchor arm resolves `sharingRules[].object` against `stack.objects` to read the anchor's OWD, so the rule is now cross-collection. `objects` IS carried by the per-write snapshot (`CONTEXT_STACK_KEYS`, #8309), so the remaining gap is unchanged in SHAPE \u2014 the gate must accept a `sharing_rule` type and the snapshot must carry `sharingRules`, which it does not \u2014 but it is now TWO collections, not one. Crossing with `sharingRules` uncarried would enforce this id for zero of its inputs while the entry claimed the door (#7220). Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
10935
11497
  run: (stack) => validateSharingRuleEnforceability(stack)
10936
11498
  },
10937
11499
  // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
@@ -10970,8 +11532,15 @@ var AUTHORING_RULES = [
10970
11532
  input: "parsed",
10971
11533
  commands: ALL,
10972
11534
  source: "packages/lint/src/validate-rule-compilability.ts",
10973
- surfaces: CLI_ONLY,
10974
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11535
+ // Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
11536
+ // judged snapshot actually carries a `json_schema` validation — so an
11537
+ // ordinary object write (no `json_schema` anywhere) still loads no
11538
+ // compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
11539
+ // The load it does take (~64 ms cold once, ~15 ms warm per publish
11540
+ // carrying such a rule) is the measured, adjudicated price of refusing a
11541
+ // validation rule that would otherwise ship compiled-by-nothing.
11542
+ surfaces: CLI_AND_RUNTIME,
11543
+ runtimeTypes: ["object"],
10975
11544
  run: (stack) => validateRuleCompilability(stack)
10976
11545
  },
10977
11546
  // #5178 — the residual half of #5029, which registering `ajv-formats` does
@@ -10992,8 +11561,12 @@ var AUTHORING_RULES = [
10992
11561
  input: "parsed",
10993
11562
  commands: ALL,
10994
11563
  source: "packages/lint/src/validate-rule-schema-formats.ts",
10995
- surfaces: CLI_ONLY,
10996
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11564
+ // Runtime publish gate (#4716): crosses with its compile sibling above —
11565
+ // the two judgements over one artifact stay on one side of the wall
11566
+ // (#7220's family discipline). Same lazy-ajv contract: the registered
11567
+ // format set is only enumerated once a schema actually names a format.
11568
+ surfaces: CLI_AND_RUNTIME,
11569
+ runtimeTypes: ["object"],
10997
11570
  run: (stack) => validateRuleSchemaFormats(stack)
10998
11571
  }
10999
11572
  ];
@@ -11053,7 +11626,20 @@ var TYPE_TO_STACK_KEY = {
11053
11626
  permission: "permissions",
11054
11627
  book: "books"
11055
11628
  };
11056
- var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
11629
+ function narrowObjectsToPackageClosure(objects, scope) {
11630
+ if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
11631
+ const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
11632
+ return objects.filter((entry) => {
11633
+ if (!entry || typeof entry !== "object") return true;
11634
+ const owner = entry[PACKAGE_PROVENANCE_KEY];
11635
+ if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
11636
+ if (reachable.has(owner)) return true;
11637
+ return isSystemObject(entry);
11638
+ });
11639
+ }
11640
+ var PACKAGE_PROVENANCE_KEY = "_packageId";
11641
+ var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
11642
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
11057
11643
  function runtimeAuthoringRulesFor(type) {
11058
11644
  return AUTHORING_RULES.filter(
11059
11645
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -11079,7 +11665,8 @@ function buildRuntimeWriteSnapshots(args) {
11079
11665
  const itemName = typeof item.name === "string" ? item.name : void 0;
11080
11666
  const baseline = {};
11081
11667
  for (const key of CONTEXT_STACK_KEYS) {
11082
- const collection = args.context?.[key] ?? [];
11668
+ const raw = args.context?.[key] ?? [];
11669
+ const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
11083
11670
  baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
11084
11671
  }
11085
11672
  const candidate = {
@@ -11113,7 +11700,8 @@ function runRuntimeAuthoringRules(args) {
11113
11700
  const snapshots = buildRuntimeWriteSnapshots({
11114
11701
  type: args.type,
11115
11702
  item: args.item,
11116
- ...args.context !== void 0 ? { context: args.context } : {}
11703
+ ...args.context !== void 0 ? { context: args.context } : {},
11704
+ ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
11117
11705
  });
11118
11706
  if (!snapshots) return empty;
11119
11707
  const ctx = { sduiManifest: args.sduiManifest };
@@ -11131,6 +11719,7 @@ function runRuntimeAuthoringRules(args) {
11131
11719
  ACTION_BODY_WRITE_PATTERNS,
11132
11720
  ACTION_BODY_WRITE_PATTERN_IDS,
11133
11721
  ACTION_BODY_WRITE_UNKNOWN_FIELD,
11722
+ ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
11134
11723
  ACTION_NAME_UNDEFINED,
11135
11724
  ACTION_NO_PLACEMENT,
11136
11725
  ACTION_RECORD_WRITE_DISCARDED,
@@ -11176,6 +11765,7 @@ function runRuntimeAuthoringRules(args) {
11176
11765
  FIELD_GROUP_UNDECLARED,
11177
11766
  FILTER_EMPTY_COMBINATOR,
11178
11767
  FILTER_EMPTY_NODE,
11768
+ FILTER_PRESET_COMPARAND,
11179
11769
  FILTER_TOKEN_UNKNOWN,
11180
11770
  FLOW_APPROVAL_REVISE_DEAD_END,
11181
11771
  FLOW_APPROVAL_REVISE_DISABLED,
@@ -11193,6 +11783,7 @@ function runRuntimeAuthoringRules(args) {
11193
11783
  FLOW_MULTIPLE_DEFAULT_EDGES,
11194
11784
  FLOW_MULTI_WRITE_UNFILTERED,
11195
11785
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
11786
+ FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
11196
11787
  FLOW_PHANTOM_AGGREGATION,
11197
11788
  FLOW_RUNAS_UNSCOPED,
11198
11789
  FLOW_TEMPLATE_FIELD_UNPROVISIONED,
@@ -11214,6 +11805,7 @@ function runRuntimeAuthoringRules(args) {
11214
11805
  HOOK_BODY_WRITE_PATTERNS,
11215
11806
  HOOK_BODY_WRITE_PATTERN_IDS,
11216
11807
  HOOK_BODY_WRITE_UNKNOWN_FIELD,
11808
+ HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
11217
11809
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
11218
11810
  LIVENESS_DEAD_PROPERTY,
11219
11811
  LIVENESS_EXPERIMENTAL_PROPERTY,
@@ -11249,6 +11841,7 @@ function runRuntimeAuthoringRules(args) {
11249
11841
  RUNTIME_AJV_OPTIONS,
11250
11842
  SEAL_MARKERS,
11251
11843
  SEARCHABLE_FIELD_UNKNOWN,
11844
+ SEARCHABLE_FIELD_UNPROVISIONED,
11252
11845
  SEARCHABLE_FIELD_UNSEARCHABLE,
11253
11846
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
11254
11847
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
@@ -11267,8 +11860,12 @@ function runRuntimeAuthoringRules(args) {
11267
11860
  SEED_VALUE_OUTSIDE_STATE_MACHINE,
11268
11861
  SEMANTIC_ROLE_FIELD_UNKNOWN,
11269
11862
  SEMANTIC_ROLE_FIELD_UNPROVISIONED,
11863
+ SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
11864
+ SHARING_RULE_OBJECT_NOT_SHAREABLE,
11270
11865
  SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
11271
11866
  SHARING_RULE_UNLOWERABLE_CONDITION,
11867
+ SORT_FIELD_UNKNOWN,
11868
+ SORT_FIELD_UNSORTABLE,
11272
11869
  STARTUP_OPEN_VOCABULARY_VERDICT,
11273
11870
  STARTUP_VERDICT_ASSERTIVE_WORDING,
11274
11871
  STARTUP_VERDICT_HINT,
@@ -11305,6 +11902,7 @@ function runRuntimeAuthoringRules(args) {
11305
11902
  authoringRulesFor,
11306
11903
  buildAccessMatrix,
11307
11904
  buildRuntimeWriteSnapshots,
11905
+ checkSortDeclaration,
11308
11906
  diffAccessMatrix,
11309
11907
  extractHookBodyWriteSet,
11310
11908
  extractHookBodyWrites,
@@ -11319,6 +11917,7 @@ function runRuntimeAuthoringRules(args) {
11319
11917
  lintUniqueDeclarations,
11320
11918
  lintUnscopedDeclaredIndexes,
11321
11919
  lintViewRefs,
11920
+ narrowObjectsToPackageClosure,
11322
11921
  nearestRegisteredFormat,
11323
11922
  nullGuardMessage,
11324
11923
  runAuthoringRules,
@@ -11357,6 +11956,7 @@ function runRuntimeAuthoringRules(args) {
11357
11956
  validatePageFieldBindings,
11358
11957
  validatePageSourceStyling,
11359
11958
  validatePredicatePathRefs,
11959
+ validatePresetComparands,
11360
11960
  validateReactPageProps,
11361
11961
  validateReactPages,
11362
11962
  validateReadonlyFlowWrites,
@@ -11373,6 +11973,7 @@ function runRuntimeAuthoringRules(args) {
11373
11973
  validateSeedStateMachine,
11374
11974
  validateSemanticRoles,
11375
11975
  validateSharingRuleEnforceability,
11976
+ validateSortableFields,
11376
11977
  validateStackExpressions,
11377
11978
  validateTranslatableSections,
11378
11979
  validateTranslationReferences,