@objectstack/lint 17.0.0-rc.5 → 17.0.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/runtime.js CHANGED
@@ -1,9 +1,13 @@
1
1
  // src/validate-expressions.ts
2
- import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
2
+ import { validateExpression, collectCelRootIdentifiers, parseCelToAst as parseCelToAst2, SCOPE_ROOTS } from "@objectstack/formula";
3
3
  import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
4
4
 
5
5
  // src/system-fields.ts
6
- import { FIELD_GROUP_SYSTEM_FIELDS, resolveInjectedSystemColumns } from "@objectstack/spec/data";
6
+ import {
7
+ FIELD_GROUP_SYSTEM_FIELDS,
8
+ resolveInjectedSystemColumns,
9
+ unprovisionedInjectedColumns
10
+ } from "@objectstack/spec/data";
7
11
  import { SystemFieldName } from "@objectstack/spec/system";
8
12
  var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
9
13
  ...FIELD_GROUP_SYSTEM_FIELDS,
@@ -12,6 +16,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
12
16
  function injectedColumnsFor(objectDef) {
13
17
  return resolveInjectedSystemColumns(objectDef).names;
14
18
  }
19
+ function unprovisionedInjectedColumnsFor(objectDef) {
20
+ return new Set(unprovisionedInjectedColumns(objectDef));
21
+ }
22
+ function objectDefsOf(stack) {
23
+ if (!stack || typeof stack !== "object") return [];
24
+ const objects = stack.objects;
25
+ if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
26
+ if (objects && typeof objects === "object") {
27
+ return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
28
+ }
29
+ return [];
30
+ }
31
+ function indexUnprovisionedAnchors(stack) {
32
+ const index = /* @__PURE__ */ new Map();
33
+ for (const obj of objectDefsOf(stack)) {
34
+ const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
35
+ if (!name) continue;
36
+ const anchors = unprovisionedInjectedColumnsFor(obj);
37
+ if (anchors.size > 0) index.set(name, anchors);
38
+ }
39
+ return index;
40
+ }
41
+ function unprovisionedAnchorCause(objectName, field) {
42
+ return `'${field}' is an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column`;
43
+ }
44
+ function unprovisionedAnchorHint(objectName, field) {
45
+ return `If the remote table really carries '${field}', declare it in ${objectName}'s own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`;
46
+ }
15
47
 
16
48
  // src/validate-null-guards.ts
17
49
  import { parseCelToAst } from "@objectstack/formula";
@@ -245,6 +277,37 @@ function buildFieldIndex(objects) {
245
277
  }
246
278
  return idx;
247
279
  }
280
+ var BOUND_RECORD_ROOTS = ["record", "previous"];
281
+ function isCelNode(v) {
282
+ return !!v && typeof v === "object" && typeof v.op === "string";
283
+ }
284
+ function collectBoundRecordReads(source) {
285
+ const out = /* @__PURE__ */ new Map();
286
+ const ast = parseCelToAst2(source);
287
+ if (!ast) return out;
288
+ const pending = [ast];
289
+ while (pending.length > 0) {
290
+ const celNode = pending.pop();
291
+ if (!isCelNode(celNode)) continue;
292
+ if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
293
+ const [celRecv, seg] = celNode.args;
294
+ if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
295
+ if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
296
+ }
297
+ }
298
+ const celArgs = celNode.args;
299
+ if (isCelNode(celArgs)) pending.push(celArgs);
300
+ else if (Array.isArray(celArgs)) {
301
+ for (const a of celArgs) {
302
+ if (isCelNode(a)) pending.push(a);
303
+ else if (Array.isArray(a)) {
304
+ for (const b of a) if (isCelNode(b)) pending.push(b);
305
+ }
306
+ }
307
+ }
308
+ }
309
+ return out;
310
+ }
248
311
  function buildFieldTypeIndex(objects) {
249
312
  const idx = /* @__PURE__ */ new Map();
250
313
  for (const obj of objects) {
@@ -344,6 +407,29 @@ function validateStackExpressions(stack) {
344
407
  const fieldIndex = buildFieldIndex(objects);
345
408
  const fieldTypeIndex = buildFieldTypeIndex(objects);
346
409
  const nullableIndex = buildNullableFieldIndex(objects);
410
+ const unprovisionedIndex = /* @__PURE__ */ new Map();
411
+ for (const obj of objects) {
412
+ const name = typeof obj.name === "string" ? obj.name : void 0;
413
+ if (!name) continue;
414
+ const anchors = unprovisionedInjectedColumnsFor(obj);
415
+ if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
416
+ }
417
+ const warnUnprovisionedAnchors = (where, raw, objectName) => {
418
+ if (!objectName) return;
419
+ const anchors = unprovisionedIndex.get(objectName);
420
+ if (!anchors) return;
421
+ const source = celSourceOf(raw);
422
+ if (!source) return;
423
+ for (const [field, operand] of collectBoundRecordReads(source)) {
424
+ if (!anchors.has(field)) continue;
425
+ issues.push({
426
+ where,
427
+ message: `\`${operand}\` reads '${field}', an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column. The predicate can never match a real value \u2014 on SQLite it silently degrades to constant-false (HTTP 200, zero rows, no error). If the remote table really carries this column, declare '${field}' in the object's own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`,
428
+ source,
429
+ severity: "warning"
430
+ });
431
+ }
432
+ };
347
433
  const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
348
434
  if (!objectName) return;
349
435
  const nullableFields = nullableIndex.get(objectName);
@@ -370,6 +456,39 @@ function validateStackExpressions(stack) {
370
456
  );
371
457
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
372
458
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
459
+ warnUnprovisionedAnchors(where, raw, objectName);
460
+ };
461
+ const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
462
+ const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
463
+ 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";
464
+ const FIELD_RULE_SLOT_CONSEQUENCE = {
465
+ 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)",
466
+ 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",
467
+ 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",
468
+ // Listed rather than left to the `??` below, so the map covers every slot
469
+ // the field walk passes and the default stays unreachable. `FieldSchema`
470
+ // declares this key only as a `retiredKey`, which rejects it by name, so
471
+ // there is no fourth runtime to measure — the honest clause is the generic
472
+ // one, not a fabricated fourth cell (#6716).
473
+ conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
474
+ };
475
+ const checkFieldRuleRoot = (where, slot, raw) => {
476
+ const source = celSourceOf(raw);
477
+ if (!source) return;
478
+ const roots = collectCelRootIdentifiers(source);
479
+ if (!roots.ok) return;
480
+ const kept = SCOPE_ROOTS.filter(
481
+ (r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
482
+ );
483
+ if (kept.length === 0) return;
484
+ const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
485
+ const prescription = FIELD_RULE_USER_ROOTS.includes(root) ? `To gate the CHOICES of a select by user, move the predicate to the option's own \`visibleWhen\` (\`options: [{ \u2026, visibleWhen: \u2026 }]\`) \u2014 per-option is the one \`*When\` surface that binds \`current_user\` and its ADR-0068 aliases. To hide the FIELD by role, declare field-level security on a permission set (\`fields: { '<object>.<field>': { readable: false } }\`), which the server enforces. To gate on record state, rewrite the predicate against \`record\`.` : root === "data" ? `\`data\` is the root of a METADATA form (a \`*.form\` module \u2014 the metadata row being edited); this is an OBJECT field, whose runtime form binds the row as \`record\` \u2014 one key name, two form kinds, two roots. Rewrite \`data.<key>\` as \`record.<field>\`.` : `\`${root}\` is declared platform-wide and bound at OTHER evaluation sites (flow, automation, screen and action predicates), never at the field level. Rewrite the predicate against \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item), or move the decision to a surface that binds \`${root}\`.`;
486
+ issues.push({
487
+ where,
488
+ message: `\`${slot}\` reads \`${root}\`, but a field-level conditional rule binds only \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item) \u2014 \`${root}\` is unbound here, so ${FIELD_RULE_SLOT_CONSEQUENCE[slot] ?? FIELD_RULE_SLOT_CONSEQUENCE_GENERIC}. ` + prescription,
489
+ source,
490
+ severity: "error"
491
+ });
373
492
  };
374
493
  const checkDeclaredPredicate = (where, raw) => {
375
494
  if (raw == null) return;
@@ -403,7 +522,14 @@ function validateStackExpressions(stack) {
403
522
  if (retired.length > 0) {
404
523
  issues.push({
405
524
  where: `${at} \xB7 node '${node.id}' (script) callable`,
406
- message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + `Run \`os migrate meta --from 16\` to rewrite it automatically.`,
525
+ 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
526
+ // behaviour, never the retired key's fate — "rewrite it" reads two ways
527
+ // over a branch that DELETES the key (template/recipients/variables/script),
528
+ // "rewrite existing sources" only one. Plain-quoted (not a template literal)
529
+ // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
530
+ // widened scan (#7030) on the same textual shape as the spec corpus — no
531
+ // interpolation lives in this clause, so nothing is lost switching quote style.
532
+ "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
407
533
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
408
534
  });
409
535
  } else if (!fn) {
@@ -437,13 +563,27 @@ function validateStackExpressions(stack) {
437
563
  for (const [fname, f] of fieldList) {
438
564
  for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
439
565
  check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
566
+ checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
567
+ }
568
+ for (const [oi, opt] of asArray(f.options).entries()) {
569
+ const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
570
+ check(
571
+ `object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
572
+ opt.visibleWhen,
573
+ objectName,
574
+ "record"
575
+ );
440
576
  }
441
- const roWhenSource = celSourceOf(f.readonlyWhen);
442
- if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
577
+ for (const [slot, raw, consequence2] of [
578
+ ["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
579
+ ["requiredWhen", f.requiredWhen, `the requirement would never be enforced \u2014 the predicate faults, the server logs and skips it, and the field stays optional in the database`]
580
+ ]) {
581
+ const source = celSourceOf(raw);
582
+ if (masters === 1 || !source || !readsParentRoot(source)) continue;
443
583
  issues.push({
444
- where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
445
- message: `\`readonlyWhen\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and the field would be locked on every write. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
446
- source: roWhenSource,
584
+ where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
585
+ message: `\`${slot}\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and ${consequence2}. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
586
+ source,
447
587
  severity: "error"
448
588
  });
449
589
  }
@@ -463,6 +603,7 @@ function validateStackExpressions(stack) {
463
603
  const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
464
604
  for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
465
605
  for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
606
+ warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
466
607
  }
467
608
  }
468
609
  }
@@ -672,6 +813,46 @@ function validateFunctionalCompleteness(stack) {
672
813
  return out;
673
814
  }
674
815
 
816
+ // src/validate-managed-api-methods.ts
817
+ import {
818
+ checkManagedApiMethodAffordances,
819
+ describeManagedApiMethodConflicts
820
+ } from "@objectstack/spec/data";
821
+ var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
822
+ var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
823
+ function entriesOf2(v) {
824
+ if (Array.isArray(v)) {
825
+ return v.flatMap(
826
+ (def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
827
+ );
828
+ }
829
+ if (isRec2(v)) {
830
+ return Object.entries(v).flatMap(
831
+ ([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
832
+ );
833
+ }
834
+ return [];
835
+ }
836
+ function validateManagedApiMethods(stack) {
837
+ const out = [];
838
+ if (!isRec2(stack)) return out;
839
+ for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
840
+ const conflicts = checkManagedApiMethodAffordances(obj.def);
841
+ if (conflicts.length === 0) continue;
842
+ const verbs = conflicts.map((c) => c.verb).join(", ");
843
+ const flags = [...new Set(conflicts.map((c) => c.needs))];
844
+ out.push({
845
+ severity: "error",
846
+ rule: MANAGED_API_METHOD_UNAFFORDABLE,
847
+ where: `object "${obj.name}"`,
848
+ path: `objects[${oi}].enable.apiMethods`,
849
+ message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + describeManagedApiMethodConflicts(conflicts) + ` The registry STRIPS [${verbs}] at registration, so this declaration and the API you actually get already disagree \u2014 today the only trace is a line in the boot log.`,
850
+ hint: `Either add \`userActions: { ${flags.map((f) => `${f}: true`).join(", ")} }\` to the object \u2014 only if the write is genuinely one a user context may perform, and only once the guard enforcing it exists (ADR-0092 D4: affordance never ships ahead of the guard) \u2014 or remove [${verbs}] from \`enable.apiMethods\`, which is what the runtime does for you today.`
851
+ });
852
+ }
853
+ return out;
854
+ }
855
+
675
856
  // src/validate-view-containers.ts
676
857
  var VIEW_CONTAINER_SHAPE = "view-container-shape";
677
858
  var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
@@ -689,10 +870,32 @@ function containerViewCount(rec) {
689
870
  function validateViewContainers(stack) {
690
871
  const out = [];
691
872
  if (!stack || typeof stack !== "object") return out;
873
+ const viewItems = stack.viewItems;
874
+ if (viewItems != null && asEntries(viewItems).length > 0) {
875
+ out.push({
876
+ severity: "error",
877
+ rule: VIEW_CONTAINER_SHAPE,
878
+ where: "viewItems",
879
+ path: "viewItems",
880
+ message: "`viewItems` is the machine-assembled channel for non-container view artifacts in runtime-assembled manifests (package export, environment artifacts) \u2014 it is not an authoring surface.",
881
+ hint: "Author views as defineView containers in `views:`; author a standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`), not in stack source."
882
+ });
883
+ }
692
884
  for (const { key, value } of asEntries(stack.views)) {
693
885
  if (!value || typeof value !== "object" || Array.isArray(value)) continue;
694
886
  const rec = value;
695
- if (rec.viewKind != null) continue;
887
+ if (rec.viewKind != null) {
888
+ const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
889
+ out.push({
890
+ severity: "error",
891
+ rule: VIEW_CONTAINER_SHAPE,
892
+ where: `views${key}${label3}`,
893
+ path: `views${key}`,
894
+ message: "A ViewItem record is not a view container: the stack `views:` collection carries containers only \u2014 `viewKind` belongs to a single VIEW, not to the container. The registration loop refuses this entry (#5320).",
895
+ hint: "Wrap it in a defineView container: defineView({ list: { type, data, columns, ... }, listViews: { ... } }) \u2014 or author the standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`). Machine-assembled manifests carry it under `viewItems:`."
896
+ });
897
+ continue;
898
+ }
696
899
  if (containerViewCount(rec) > 0) continue;
697
900
  const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
698
901
  const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
@@ -722,6 +925,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
722
925
  var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
723
926
  var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
724
927
  var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
928
+ var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
725
929
  var LEGACY_ANALYTICS_KEYS = [
726
930
  "categoryField",
727
931
  "valueField",
@@ -841,6 +1045,7 @@ function validateWidgetBindings(stack) {
841
1045
  }
842
1046
  objectFieldTypes.set(o.name, fm);
843
1047
  }
1048
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
844
1049
  const datasetList = asArray3(stack.datasets);
845
1050
  for (let i = 0; i < datasetList.length; i++) {
846
1051
  const ds = datasetList[i];
@@ -927,13 +1132,24 @@ function validateWidgetBindings(stack) {
927
1132
  if (dashFilterDefs.length > 0) {
928
1133
  const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
929
1134
  const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
930
- if (objectFields) {
1135
+ const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
1136
+ if (objectFields && datasetObject) {
931
1137
  for (const def of dashFilterDefs) {
932
1138
  const eff = effectiveFilterField(w, def);
933
1139
  if (!eff) continue;
934
1140
  const field = eff.field;
935
1141
  if (field.includes(".")) continue;
936
- if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
1142
+ if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
1143
+ if (anchors?.has(field)) {
1144
+ push2({
1145
+ severity: "warning",
1146
+ rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
1147
+ message: (eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but ` : `inherits dashboard filter \`${def.name}(${field})\`, but `) + `${unprovisionedAnchorCause(datasetObject, field)}. The filter is ANDed into this widget's analytics query (#2501), so it can never match a real value \u2014 on SQLite it silently degrades to constant-false and the widget renders empty (HTTP 200, zero rows, no error).`,
1148
+ hint: `${unprovisionedAnchorHint(datasetObject, field)} A widget can also opt out with filterBindings: { ${def.name}: false }. Suppress with suppressWarnings: ['${DASHBOARD_FILTER_FIELD_UNPROVISIONED}'] if the remote schema resolves it some other way.`
1149
+ });
1150
+ }
1151
+ continue;
1152
+ }
937
1153
  push2({
938
1154
  severity: "error",
939
1155
  rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
@@ -1171,7 +1387,8 @@ function validateDashboardActionRefs(stack) {
1171
1387
 
1172
1388
  // src/validate-filter-tokens.ts
1173
1389
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
1174
- var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1390
+
1391
+ // src/filter-walk.ts
1175
1392
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
1176
1393
  function asArray5(v) {
1177
1394
  if (Array.isArray(v)) return v;
@@ -1183,7 +1400,62 @@ function asArray5(v) {
1183
1400
  function label(v, fallback) {
1184
1401
  return typeof v === "string" && v.length > 0 ? v : fallback;
1185
1402
  }
1403
+ function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
1404
+ if (!node || typeof node !== "object") return;
1405
+ if (seen.has(node)) return;
1406
+ seen.add(node);
1407
+ if (Array.isArray(node)) {
1408
+ node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
1409
+ return;
1410
+ }
1411
+ for (const [k, v] of Object.entries(node)) {
1412
+ const childPath = `${path}.${k}`;
1413
+ if (FILTER_KEYS.has(k)) {
1414
+ visit({ value: v, path: childPath, where });
1415
+ continue;
1416
+ }
1417
+ scanForFilters(v, childPath, where, visit, seen);
1418
+ }
1419
+ }
1420
+ function walkAuthoredFilters(stack, surfaces, visit) {
1421
+ if (!stack || typeof stack !== "object") return;
1422
+ for (const { key, kind } of surfaces) {
1423
+ const items = asArray5(stack[key]);
1424
+ items.forEach((item, i) => {
1425
+ const name = label(item.name ?? item.id, `#${i}`);
1426
+ if (kind === "dashboard") {
1427
+ const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1428
+ widgets.forEach((w, wi) => {
1429
+ const wName = label(w.id ?? w.title, `#${wi}`);
1430
+ scanForFilters(
1431
+ w,
1432
+ `${key}[${i}].widgets[${wi}]`,
1433
+ `dashboard "${name}" \xB7 widget "${wName}"`,
1434
+ visit,
1435
+ /* @__PURE__ */ new Set()
1436
+ );
1437
+ });
1438
+ const { widgets: _skip, ...rest } = item;
1439
+ scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
1440
+ return;
1441
+ }
1442
+ scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
1443
+ });
1444
+ }
1445
+ }
1446
+
1447
+ // src/validate-filter-tokens.ts
1448
+ var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1186
1449
  var KNOWN_LIST = CONTEXT_TOKENS.join("}, {");
1450
+ var TOKEN_FILTER_SURFACES = [
1451
+ { key: "dashboards", kind: "dashboard" },
1452
+ { key: "objects", kind: "object" },
1453
+ { key: "views", kind: "view" },
1454
+ { key: "reports", kind: "report" },
1455
+ { key: "datasets", kind: "dataset" },
1456
+ { key: "pages", kind: "page" },
1457
+ { key: "apps", kind: "app" }
1458
+ ];
1187
1459
  function walkFilterValues(node, path, where, out, seen) {
1188
1460
  if (node === null || node === void 0) return;
1189
1461
  if (typeof node === "string") {
@@ -1212,58 +1484,132 @@ function walkFilterValues(node, path, where, out, seen) {
1212
1484
  walkFilterValues(v, `${path}.${k}`, where, out, seen);
1213
1485
  }
1214
1486
  }
1215
- function scanForFilters(node, path, where, out, seen) {
1216
- if (!node || typeof node !== "object") return;
1217
- if (seen.has(node)) return;
1218
- seen.add(node);
1219
- if (Array.isArray(node)) {
1220
- node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, out, seen));
1487
+ function validateFilterTokens(stack) {
1488
+ if (!stack || typeof stack !== "object") return [];
1489
+ const out = [];
1490
+ walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
1491
+ walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
1492
+ });
1493
+ return out;
1494
+ }
1495
+
1496
+ // src/validate-empty-combinators.ts
1497
+ import { reduceFilterVerdict } from "@objectstack/spec/data";
1498
+ var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
1499
+ var FILTER_EMPTY_NODE = "filter-empty-node";
1500
+ var EMPTY_COMBINATOR_SURFACES = [
1501
+ { key: "dashboards", kind: "dashboard" },
1502
+ { key: "objects", kind: "object" },
1503
+ { key: "views", kind: "view" },
1504
+ { key: "reports", kind: "report" },
1505
+ { key: "datasets", kind: "dataset" },
1506
+ { key: "pages", kind: "page" },
1507
+ { key: "apps", kind: "app" },
1508
+ { key: "flows", kind: "flow" }
1509
+ ];
1510
+ function isFilterNode(value) {
1511
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1512
+ const proto = Object.getPrototypeOf(value);
1513
+ return proto === Object.prototype || proto === null;
1514
+ }
1515
+ var VERDICT_OF = {
1516
+ $and: reduceFilterVerdict({ $and: [] }),
1517
+ $or: reduceFilterVerdict({ $or: [] }),
1518
+ $not: reduceFilterVerdict({ $not: {} }),
1519
+ node: reduceFilterVerdict({}),
1520
+ /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
1521
+ orWithEmptyBranch: reduceFilterVerdict({ $or: [{ status: "open" }, {}] })
1522
+ };
1523
+ function rows(verdict) {
1524
+ if (verdict === "true") return "matches EVERY row";
1525
+ if (verdict === "false") return "matches NO row";
1526
+ return "carries a real predicate";
1527
+ }
1528
+ var MATCH_NONE_SPELLING = "If you really do want a predicate that selects nothing, `{ <field>: { $in: [] } }` is the declared spelling for it (an empty `$in` list matches nothing, on every backend) \u2014 it says so where an empty combinator only implies it.";
1529
+ var OMIT_THE_KEY = 'To express "no filter", DELETE the key \u2014 an absent `filter` and a filter that reduces to TRUE run identically, and only the absent key says so to the next reader (and to the next AI author that copies this metadata).';
1530
+ function emitEmptyCombinator(key, path, ctx) {
1531
+ const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
1532
+ const message = key === "$and" ? `\`$and: []\` is a conjunction of ZERO conditions. Under the #5322 identity ruling it ${rows(VERDICT_OF.$and)} \u2014 the key is authored, and it constrains nothing, so this surface reads as filtered and is not.` : key === "$or" ? `\`$or: []\` is a disjunction of ZERO branches. Under the #5322 identity ruling it ${rows(VERDICT_OF.$or)}: this surface renders permanently empty, and on a read scope it hides every row (fail-closed by design \u2014 #5134).` : `\`$not: {}\` negates an EMPTY node. An empty node is TRUE and NOT TRUE is FALSE, so it ${rows(VERDICT_OF.$not)} \u2014 the opposite of the "no filter" an empty operand looks like.`;
1533
+ const hint = key === "$and" ? `${OMIT_THE_KEY} To express a constraint, put the conditions in the array. ${MATCH_NONE_SPELLING}` : key === "$or" ? `If you meant "no filter", this is its OPPOSITE: emptying the array does not relax the filter, it closes it. ${OMIT_THE_KEY} If you meant to offer alternatives, put the branches in the array. ${MATCH_NONE_SPELLING}` : `Put the condition you are negating inside \`$not\` (\`{ $not: { status: 'closed' } }\`). ${OMIT_THE_KEY} ${MATCH_NONE_SPELLING}`;
1534
+ ctx.out.push({
1535
+ severity: "error",
1536
+ rule: FILTER_EMPTY_COMBINATOR,
1537
+ where: ctx.where,
1538
+ path,
1539
+ message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
1540
+ hint: `${hint} A PROGRAMMATIC producer that loops to zero operands keeps the runtime identity unchanged \u2014 this rule judges only what is written in the metadata.`
1541
+ });
1542
+ }
1543
+ function emitEmptyNode(position, path, ctx) {
1544
+ if (position === "root") {
1545
+ ctx.out.push({
1546
+ severity: "error",
1547
+ rule: FILTER_EMPTY_NODE,
1548
+ where: ctx.where,
1549
+ path,
1550
+ message: `An EMPTY filter node (\`{}\`) is authored here. Under the #5322 identity ruling an empty node is TRUE \u2014 it ${rows(VERDICT_OF.node)}, exactly as if the key were absent \u2014 so a filter is declared and enforces nothing.`,
1551
+ hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
1552
+ });
1221
1553
  return;
1222
1554
  }
1223
- for (const [k, v] of Object.entries(node)) {
1224
- const childPath = `${path}.${k}`;
1225
- if (FILTER_KEYS.has(k)) {
1226
- walkFilterValues(v, childPath, where, out, /* @__PURE__ */ new Set());
1555
+ if (position === "or-branch") {
1556
+ ctx.out.push({
1557
+ severity: "error",
1558
+ rule: FILTER_EMPTY_NODE,
1559
+ where: ctx.where,
1560
+ path,
1561
+ message: `An EMPTY branch (\`{}\`) of a \`$or\`. An empty node is TRUE, and one TRUE disjunct ABSORBS the whole disjunction (\`{ $or: [{ status: 'open' }, {}] }\` ${rows(VERDICT_OF.orWithEmptyBranch)}), so every branch you wrote beside it is dead.`,
1562
+ hint: "Delete the empty branch \u2014 the `$or` then means what it looks like. If it was meant to carry a condition, write it. (A compiler that DROPPED the empty branch instead would silently NARROW the scope to the surviving branches, which is why the runtime absorbs rather than filters \u2014 #5297.)"
1563
+ });
1564
+ return;
1565
+ }
1566
+ ctx.out.push({
1567
+ severity: "error",
1568
+ rule: FILTER_EMPTY_NODE,
1569
+ where: ctx.where,
1570
+ path,
1571
+ message: "An EMPTY branch (`{}`) of a `$and`. An empty node is TRUE \u2014 the AND identity \u2014 so the branch contributes no condition and the conjunction means whatever its other branches mean.",
1572
+ hint: "Delete the empty branch, or write the condition it was meant to carry. A branch that constrains nothing is indistinguishable from one whose condition was lost in an edit."
1573
+ });
1574
+ }
1575
+ function scanNodeKeys(node, path, ctx) {
1576
+ for (const [key, value] of Object.entries(node)) {
1577
+ if (key === "$and" || key === "$or") {
1578
+ if (!Array.isArray(value)) continue;
1579
+ if (value.length === 0) {
1580
+ emitEmptyCombinator(key, `${path}.${key}`, ctx);
1581
+ continue;
1582
+ }
1583
+ value.forEach((element, index) => {
1584
+ scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
1585
+ });
1586
+ continue;
1587
+ }
1588
+ if (key === "$not") {
1589
+ if (!isFilterNode(value)) continue;
1590
+ if (Object.keys(value).length === 0) {
1591
+ emitEmptyCombinator("$not", `${path}.$not`, ctx);
1592
+ continue;
1593
+ }
1594
+ scanNodeKeys(value, `${path}.$not`, ctx);
1227
1595
  continue;
1228
1596
  }
1229
- scanForFilters(v, childPath, where, out, seen);
1230
1597
  }
1231
1598
  }
1232
- function validateFilterTokens(stack) {
1599
+ function scanBranch(value, path, position, ctx) {
1600
+ if (!isFilterNode(value)) return;
1601
+ if (Object.keys(value).length === 0) {
1602
+ emitEmptyNode(position, path, ctx);
1603
+ return;
1604
+ }
1605
+ scanNodeKeys(value, path, ctx);
1606
+ }
1607
+ function validateEmptyCombinators(stack) {
1233
1608
  if (!stack || typeof stack !== "object") return [];
1234
1609
  const out = [];
1235
- const surfaces = [
1236
- ["dashboards", "dashboard"],
1237
- ["objects", "object"],
1238
- ["views", "view"],
1239
- ["reports", "report"],
1240
- ["datasets", "dataset"],
1241
- ["pages", "page"],
1242
- ["apps", "app"]
1243
- ];
1244
- for (const [key, kind] of surfaces) {
1245
- const items = asArray5(stack[key]);
1246
- items.forEach((item, i) => {
1247
- const name = label(item.name ?? item.id, `#${i}`);
1248
- if (kind === "dashboard") {
1249
- const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1250
- widgets.forEach((w, wi) => {
1251
- const wName = label(w.id ?? w.title, `#${wi}`);
1252
- scanForFilters(
1253
- w,
1254
- `${key}[${i}].widgets[${wi}]`,
1255
- `dashboard "${name}" \xB7 widget "${wName}"`,
1256
- out,
1257
- /* @__PURE__ */ new Set()
1258
- );
1259
- });
1260
- const { widgets: _skip, ...rest } = item;
1261
- scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
1262
- return;
1263
- }
1264
- scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
1265
- });
1266
- }
1610
+ walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
1611
+ scanBranch(value, path, "root", { where, out });
1612
+ });
1267
1613
  return out;
1268
1614
  }
1269
1615
 
@@ -1463,6 +1809,7 @@ function validateObjectReferences(stack) {
1463
1809
  // src/validate-searchable-fields.ts
1464
1810
  import {
1465
1811
  resolveSearchFieldResolution,
1812
+ isVirtualSearchField,
1466
1813
  SEARCHABLE_TEXTUAL_TYPES,
1467
1814
  SEARCHABLE_ENUM_TYPES,
1468
1815
  SEARCH_AUTO_EXCLUDED_FIELDS
@@ -1476,7 +1823,7 @@ function asArray7(v) {
1476
1823
  }
1477
1824
  return [];
1478
1825
  }
1479
- function isRec2(v) {
1826
+ function isRec3(v) {
1480
1827
  return !!v && typeof v === "object" && !Array.isArray(v);
1481
1828
  }
1482
1829
  function strName3(v) {
@@ -1552,7 +1899,7 @@ function distance2(a, b) {
1552
1899
  }
1553
1900
  function indexObjectSearchTargets(stack) {
1554
1901
  const fieldsByObject = /* @__PURE__ */ new Map();
1555
- if (!isRec2(stack)) return fieldsByObject;
1902
+ if (!isRec3(stack)) return fieldsByObject;
1556
1903
  for (const obj of asArray7(stack.objects)) {
1557
1904
  const name = strName3(obj.name);
1558
1905
  if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
@@ -1580,7 +1927,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1580
1927
  where,
1581
1928
  path: `${path}[${i}]`,
1582
1929
  message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest3(name, known)),
1583
- hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a formula field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1930
+ hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a stored text field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1931
+ });
1932
+ continue;
1933
+ }
1934
+ if (isVirtualSearchField(target.fields[name])) {
1935
+ const vtype = target.fields[name]?.type;
1936
+ findings.push({
1937
+ severity: "error",
1938
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1939
+ where,
1940
+ path: `${path}[${i}]`,
1941
+ message: `${subject} entry "${name}" on object "${objectName}" is a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materializes a column for 'search' to scan and the entry can never match. It reads as search coverage and delivers none \u2014 the runtime used to admit it verbatim because the declaration named it (#6674).`,
1942
+ hint: `Mirror the computed value onto a stored text field on "${objectName}" and declare that instead, or drop "${name}". At runtime the ingress gate now refuses this entry with 400 INVALID_FIELD, the same answer a stale entry gets (#4254).`
1584
1943
  });
1585
1944
  continue;
1586
1945
  }
@@ -1615,14 +1974,14 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1615
1974
  where,
1616
1975
  path: `${path}[${i}]`,
1617
1976
  message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
1618
- hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text/formula field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1977
+ hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1619
1978
  });
1620
1979
  }
1621
1980
  return findings;
1622
1981
  }
1623
1982
  function validateSearchableFields(stack) {
1624
1983
  const findings = [];
1625
- if (!isRec2(stack)) return findings;
1984
+ if (!isRec3(stack)) return findings;
1626
1985
  const objects = asArray7(stack.objects);
1627
1986
  const fieldsByObject = indexObjectSearchTargets(stack);
1628
1987
  const check = (declared, objectName, where, path, subject, role) => {
@@ -1632,7 +1991,7 @@ function validateSearchableFields(stack) {
1632
1991
  };
1633
1992
  for (let oi = 0; oi < objects.length; oi++) {
1634
1993
  const obj = objects[oi];
1635
- if (!isRec2(obj)) continue;
1994
+ if (!isRec3(obj)) continue;
1636
1995
  const objName = strName3(obj.name);
1637
1996
  const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
1638
1997
  check(
@@ -1643,9 +2002,9 @@ function validateSearchableFields(stack) {
1643
2002
  "searchableFields",
1644
2003
  "canonical"
1645
2004
  );
1646
- if (isRec2(obj.listViews)) {
2005
+ if (isRec3(obj.listViews)) {
1647
2006
  for (const [key, lv] of Object.entries(obj.listViews)) {
1648
- if (!isRec2(lv)) continue;
2007
+ if (!isRec3(lv)) continue;
1649
2008
  check(
1650
2009
  lv.searchableFields,
1651
2010
  // A built-in list view belongs to its object; an inline `data.object`
@@ -1662,10 +2021,10 @@ function validateSearchableFields(stack) {
1662
2021
  const views = asArray7(stack.views);
1663
2022
  for (let vi = 0; vi < views.length; vi++) {
1664
2023
  const view = views[vi];
1665
- if (!isRec2(view)) continue;
2024
+ if (!isRec3(view)) continue;
1666
2025
  const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
1667
2026
  const viewObject = strName3(view.objectName) ?? strName3(view.object);
1668
- if (isRec2(view.list)) {
2027
+ if (isRec3(view.list)) {
1669
2028
  check(
1670
2029
  view.list.searchableFields,
1671
2030
  listViewObject(view.list) ?? viewObject,
@@ -1675,9 +2034,9 @@ function validateSearchableFields(stack) {
1675
2034
  "narrowing"
1676
2035
  );
1677
2036
  }
1678
- if (isRec2(view.listViews)) {
2037
+ if (isRec3(view.listViews)) {
1679
2038
  for (const [key, lv] of Object.entries(view.listViews)) {
1680
- if (!isRec2(lv)) continue;
2039
+ if (!isRec3(lv)) continue;
1681
2040
  check(
1682
2041
  lv.searchableFields,
1683
2042
  listViewObject(lv) ?? viewObject,
@@ -1693,11 +2052,11 @@ function validateSearchableFields(stack) {
1693
2052
  }
1694
2053
  function listViewObject(listView) {
1695
2054
  const data = listView.data;
1696
- return isRec2(data) ? strName3(data.object) : void 0;
2055
+ return isRec3(data) ? strName3(data.object) : void 0;
1697
2056
  }
1698
2057
 
1699
2058
  // src/page-walk.ts
1700
- function isRec3(v) {
2059
+ function isRec4(v) {
1701
2060
  return !!v && typeof v === "object" && !Array.isArray(v);
1702
2061
  }
1703
2062
  function strName4(v) {
@@ -1710,19 +2069,19 @@ function isSourceAuthoredPage(page) {
1710
2069
  }
1711
2070
  function walkPageComponents(page, pagePath) {
1712
2071
  const out = [];
1713
- if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
2072
+ if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
1714
2073
  const pageObject = strName4(page.object);
1715
2074
  const visit = (node, path, inheritedObject) => {
1716
- if (!isRec3(node)) return;
1717
- const props = isRec3(node.properties) ? node.properties : void 0;
1718
- const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
2075
+ if (!isRec4(node)) return;
2076
+ const props = isRec4(node.properties) ? node.properties : void 0;
2077
+ const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
1719
2078
  const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
1720
2079
  out.push({ component: node, path, objectName });
1721
2080
  if (!props) return;
1722
2081
  if (Array.isArray(props.items)) {
1723
2082
  for (let i = 0; i < props.items.length; i++) {
1724
2083
  const item = props.items[i];
1725
- if (!isRec3(item) || !Array.isArray(item.children)) continue;
2084
+ if (!isRec4(item) || !Array.isArray(item.children)) continue;
1726
2085
  for (let c = 0; c < item.children.length; c++) {
1727
2086
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
1728
2087
  }
@@ -1744,12 +2103,12 @@ function walkPageComponents(page, pagePath) {
1744
2103
  const regions = Array.isArray(page.regions) ? page.regions : [];
1745
2104
  for (let r = 0; r < regions.length; r++) {
1746
2105
  const region = regions[r];
1747
- if (!isRec3(region) || !Array.isArray(region.components)) continue;
2106
+ if (!isRec4(region) || !Array.isArray(region.components)) continue;
1748
2107
  for (let c = 0; c < region.components.length; c++) {
1749
2108
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
1750
2109
  }
1751
2110
  }
1752
- const slots = isRec3(page.slots) ? page.slots : void 0;
2111
+ const slots = isRec4(page.slots) ? page.slots : void 0;
1753
2112
  if (slots) {
1754
2113
  for (const [slot, value] of Object.entries(slots)) {
1755
2114
  const list3 = Array.isArray(value) ? value : [value];
@@ -1934,6 +2293,15 @@ function validateActionNameRefs(stack) {
1934
2293
  "Navigation action item"
1935
2294
  );
1936
2295
  }
2296
+ const runAction = strName5(nav.runAction);
2297
+ if (nav.type === "object" && runAction) {
2298
+ check(
2299
+ runAction,
2300
+ `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2301
+ `${navPath}.runAction`,
2302
+ "Navigation deep-link auto-run"
2303
+ );
2304
+ }
1937
2305
  if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
1938
2306
  }
1939
2307
  };
@@ -1948,6 +2316,7 @@ function validateActionNameRefs(stack) {
1948
2316
 
1949
2317
  // src/validate-page-field-bindings.ts
1950
2318
  var PAGE_FIELD_UNKNOWN = "page-field-unknown";
2319
+ var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
1951
2320
  function asArray9(v) {
1952
2321
  if (Array.isArray(v)) return v;
1953
2322
  if (v && typeof v === "object") {
@@ -1958,7 +2327,7 @@ function asArray9(v) {
1958
2327
  function strName6(v) {
1959
2328
  return typeof v === "string" && v.length > 0 ? v : void 0;
1960
2329
  }
1961
- function isRec4(v) {
2330
+ function isRec5(v) {
1962
2331
  return !!v && typeof v === "object" && !Array.isArray(v);
1963
2332
  }
1964
2333
  function fieldRefsFrom(value, basePath) {
@@ -1969,7 +2338,7 @@ function fieldRefsFrom(value, basePath) {
1969
2338
  out.push({ name: bare, path });
1970
2339
  return;
1971
2340
  }
1972
- if (!isRec4(v)) return;
2341
+ if (!isRec5(v)) return;
1973
2342
  const named = strName6(v.field) ?? strName6(v.name);
1974
2343
  if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
1975
2344
  };
@@ -1999,8 +2368,21 @@ var COMPONENT_FIELD_SPECS = {
1999
2368
  "element:number": { props: ["field"] },
2000
2369
  "element:filter": { props: ["fields"] },
2001
2370
  "element:form": { props: ["fields"] },
2002
- // The schema says `displayField`; real pages author `labelField`. Accept both.
2003
- "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
2371
+ // `labelField` is the one field-bearing prop this element declares. Its former
2372
+ // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
2373
+ // `searchFields` (deleted, ADR-0049) were retired in #5775 and are
2374
+ // `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
2375
+ // spec-conformant page carries either, and this rule's job (resolve a field
2376
+ // NAME against the object) is not the question a retired key raises (#6629).
2377
+ //
2378
+ // A non-conformant page that writes one anyway is not left unattended: the
2379
+ // #5068 props gate reports the key with its rename/delete prescription. That
2380
+ // gate is advisory and CLI-only and lives in a different registry
2381
+ // (`authoring-rules`) from this suite, so it neither precedes nor suppresses
2382
+ // this rule — what these two entries actually added was a SECOND finding,
2383
+ // saying a field named by a key that no longer exists does not exist either.
2384
+ // The prescription is the useful half; this half was noise on top of it.
2385
+ "element:record_picker": { props: ["labelField"] }
2004
2386
  };
2005
2387
  var RELATED_LIST_TYPE = "record:related_list";
2006
2388
  function componentFieldRefs(type, props, basePath, sep = ".") {
@@ -2014,15 +2396,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2014
2396
  const sections = Array.isArray(props[key]) ? props[key] : [];
2015
2397
  for (let si = 0; si < sections.length; si++) {
2016
2398
  const section = sections[si];
2017
- if (!isRec4(section)) continue;
2399
+ if (!isRec5(section)) continue;
2018
2400
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2019
2401
  }
2020
2402
  }
2021
2403
  return refs;
2022
2404
  }
2023
2405
  function relatedListFieldRefs(props, basePath, sep = ".") {
2024
- const add = isRec4(props.add) ? props.add : void 0;
2025
- const picker = add && isRec4(add.picker) ? add.picker : void 0;
2406
+ const add = isRec5(props.add) ? props.add : void 0;
2407
+ const picker = add && isRec5(add.picker) ? add.picker : void 0;
2026
2408
  const at = (key) => `${basePath}${sep}${key}`;
2027
2409
  return {
2028
2410
  relatedObject: strName6(props.objectName),
@@ -2043,7 +2425,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2043
2425
  }
2044
2426
  function indexObjectFields(stack) {
2045
2427
  const objectFields = /* @__PURE__ */ new Map();
2046
- if (!isRec4(stack)) return objectFields;
2428
+ if (!isRec5(stack)) return objectFields;
2047
2429
  for (const obj of asArray9(stack.objects)) {
2048
2430
  const name = strName6(obj.name);
2049
2431
  if (!name) continue;
@@ -2056,14 +2438,27 @@ function indexObjectFields(stack) {
2056
2438
  }
2057
2439
  return objectFields;
2058
2440
  }
2059
- function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
2441
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
2060
2442
  const findings = [];
2061
2443
  if (!objectName) return findings;
2062
2444
  const known = objectFields.get(objectName);
2063
2445
  if (!known) return findings;
2446
+ const anchors = unprovisionedAnchors?.get(objectName);
2064
2447
  for (const ref of refs) {
2065
2448
  if (ref.name.includes(".")) continue;
2066
- if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
2449
+ if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
2450
+ if (anchors?.has(ref.name)) {
2451
+ findings.push({
2452
+ severity: "warning",
2453
+ rule: PAGE_FIELD_UNPROVISIONED,
2454
+ where,
2455
+ path: ref.path,
2456
+ message: `field "${ref.name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, ref.name)}` + (consequence2 === "queried" ? ' \u2014 it is used in a QUERY, so the predicate can never match a real value: on SQLite it silently degrades to constant-false and the surface renders an empty result that looks exactly like "there is no data".' : " \u2014 the component renders it, blank, on every record."),
2457
+ hint: unprovisionedAnchorHint(objectName, ref.name)
2458
+ });
2459
+ }
2460
+ continue;
2461
+ }
2067
2462
  findings.push({
2068
2463
  severity: consequence2 === "queried" ? "error" : "warning",
2069
2464
  rule: PAGE_FIELD_UNKNOWN,
@@ -2079,17 +2474,20 @@ function validatePageFieldBindings(stack) {
2079
2474
  const findings = [];
2080
2475
  if (!stack || typeof stack !== "object") return findings;
2081
2476
  const objectFields = indexObjectFields(stack);
2477
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2082
2478
  const pages = asArray9(stack.pages);
2083
2479
  for (let pi = 0; pi < pages.length; pi++) {
2084
2480
  const page = pages[pi];
2085
2481
  if (!page || typeof page !== "object") continue;
2086
2482
  const pageName = strName6(page.name) ?? `#${pi}`;
2087
2483
  const checkRefs = (refs, objectName, where) => {
2088
- findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
2484
+ findings.push(
2485
+ ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
2486
+ );
2089
2487
  };
2090
2488
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2091
2489
  const type = strName6(component.type);
2092
- const props = isRec4(component.properties) ? component.properties : void 0;
2490
+ const props = isRec5(component.properties) ? component.properties : void 0;
2093
2491
  if (!type || !props) continue;
2094
2492
  const where = `page "${pageName}" \xB7 ${type}`;
2095
2493
  const base = `${path}.properties`;
@@ -2104,7 +2502,7 @@ function validatePageFieldBindings(stack) {
2104
2502
  if (!refs) continue;
2105
2503
  checkRefs(refs, objectName, where);
2106
2504
  }
2107
- const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
2505
+ const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2108
2506
  if (cfg) {
2109
2507
  const cfgObject = strName6(cfg.source) ?? strName6(page.object);
2110
2508
  const base = `pages[${pi}].interfaceConfig`;
@@ -2113,7 +2511,7 @@ function validatePageFieldBindings(stack) {
2113
2511
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2114
2512
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2115
2513
  ];
2116
- const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
2514
+ const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2117
2515
  if (userFilters) {
2118
2516
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2119
2517
  }
@@ -2141,7 +2539,7 @@ function strName7(v) {
2141
2539
  function strList2(v) {
2142
2540
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2143
2541
  }
2144
- function isRec5(v) {
2542
+ function isRec6(v) {
2145
2543
  return !!v && typeof v === "object" && !Array.isArray(v);
2146
2544
  }
2147
2545
  function distance4(a, b) {
@@ -2270,10 +2668,10 @@ function validateChartBindings(stack) {
2270
2668
  const reports = asArray10(stack.reports);
2271
2669
  for (let ri = 0; ri < reports.length; ri++) {
2272
2670
  const report = reports[ri];
2273
- if (!isRec5(report)) continue;
2671
+ if (!isRec6(report)) continue;
2274
2672
  const reportName = strName7(report.name) ?? `#${ri}`;
2275
2673
  const checkReportChart = (chart, dataset, values, where, path) => {
2276
- if (!isRec5(chart)) return;
2674
+ if (!isRec6(chart)) return;
2277
2675
  check({
2278
2676
  dataset,
2279
2677
  // `values` is the report's measure SELECTION, not a chart ref; feeding
@@ -2297,7 +2695,7 @@ function validateChartBindings(stack) {
2297
2695
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
2298
2696
  for (let bi = 0; bi < blocks.length; bi++) {
2299
2697
  const block = blocks[bi];
2300
- if (!isRec5(block)) continue;
2698
+ if (!isRec6(block)) continue;
2301
2699
  checkReportChart(
2302
2700
  block.chart,
2303
2701
  strName7(block.dataset),
@@ -2308,9 +2706,9 @@ function validateChartBindings(stack) {
2308
2706
  }
2309
2707
  }
2310
2708
  const checkListChart = (container, where, path) => {
2311
- if (!isRec5(container)) return;
2709
+ if (!isRec6(container)) return;
2312
2710
  const chart = container.chart;
2313
- if (!isRec5(chart)) return;
2711
+ if (!isRec6(chart)) return;
2314
2712
  check({
2315
2713
  dataset: strName7(chart.dataset),
2316
2714
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
@@ -2322,10 +2720,10 @@ function validateChartBindings(stack) {
2322
2720
  const views = asArray10(stack.views);
2323
2721
  for (let vi = 0; vi < views.length; vi++) {
2324
2722
  const view = views[vi];
2325
- if (!isRec5(view)) continue;
2723
+ if (!isRec6(view)) continue;
2326
2724
  const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
2327
2725
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
2328
- if (isRec5(view.listViews)) {
2726
+ if (isRec6(view.listViews)) {
2329
2727
  for (const [key, lv] of Object.entries(view.listViews)) {
2330
2728
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
2331
2729
  }
@@ -2334,7 +2732,7 @@ function validateChartBindings(stack) {
2334
2732
  const objects = asArray10(stack.objects);
2335
2733
  for (let oi = 0; oi < objects.length; oi++) {
2336
2734
  const obj = objects[oi];
2337
- if (!isRec5(obj) || !isRec5(obj.listViews)) continue;
2735
+ if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
2338
2736
  const objName = strName7(obj.name) ?? `#${oi}`;
2339
2737
  for (const [key, lv] of Object.entries(obj.listViews)) {
2340
2738
  checkListChart(
@@ -2347,10 +2745,10 @@ function validateChartBindings(stack) {
2347
2745
  const pages = asArray10(stack.pages);
2348
2746
  for (let pi = 0; pi < pages.length; pi++) {
2349
2747
  const page = pages[pi];
2350
- if (!isRec5(page)) continue;
2748
+ if (!isRec6(page)) continue;
2351
2749
  const pageName = strName7(page.name) ?? `#${pi}`;
2352
2750
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2353
- const props = isRec5(component.properties) ? component.properties : void 0;
2751
+ const props = isRec6(component.properties) ? component.properties : void 0;
2354
2752
  if (!props || !strName7(props.dataset)) continue;
2355
2753
  const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
2356
2754
  const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
@@ -2500,10 +2898,10 @@ function validateNavAccess(stack) {
2500
2898
 
2501
2899
  // src/validate-nav-target-refs.ts
2502
2900
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
2503
- var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2901
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2504
2902
  function asArray13(v) {
2505
- if (Array.isArray(v)) return v.filter(isRec6);
2506
- if (isRec6(v)) return Object.entries(v).map(([name, def]) => isRec6(def) ? { name, ...def } : { name });
2903
+ if (Array.isArray(v)) return v.filter(isRec7);
2904
+ if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
2507
2905
  return [];
2508
2906
  }
2509
2907
  function strName9(v) {
@@ -2525,7 +2923,7 @@ function namesOf(collection) {
2525
2923
  }
2526
2924
  function validateNavTargetRefs(stack) {
2527
2925
  const findings = [];
2528
- if (!isRec6(stack)) return findings;
2926
+ if (!isRec7(stack)) return findings;
2529
2927
  const apps = asArray13(stack.apps);
2530
2928
  if (apps.length === 0) return findings;
2531
2929
  const declared = /* @__PURE__ */ new Map();
@@ -2537,7 +2935,7 @@ function validateNavTargetRefs(stack) {
2537
2935
  const walk = (items, basePath) => {
2538
2936
  if (!Array.isArray(items)) return;
2539
2937
  for (const [ni, raw] of items.entries()) {
2540
- if (!isRec6(raw)) continue;
2938
+ if (!isRec7(raw)) continue;
2541
2939
  const nav = raw;
2542
2940
  const navPath = `${basePath}[${ni}]`;
2543
2941
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
@@ -2568,34 +2966,141 @@ function validateNavTargetRefs(stack) {
2568
2966
  return findings;
2569
2967
  }
2570
2968
 
2571
- // src/validate-translation-references.ts
2572
- import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
2573
- var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
2574
- var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
2575
- function isRec7(v) {
2576
- return !!v && typeof v === "object" && !Array.isArray(v);
2577
- }
2969
+ // src/validate-nav-object-servability.ts
2970
+ import { canServeApiOperation } from "@objectstack/spec/data";
2971
+ var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
2972
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2578
2973
  function asArray14(v) {
2579
- if (Array.isArray(v)) return v;
2580
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
2974
+ if (Array.isArray(v)) return v.filter(isRec8);
2975
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
2581
2976
  return [];
2582
2977
  }
2583
2978
  function strName10(v) {
2584
2979
  return typeof v === "string" && v.length > 0 ? v : void 0;
2585
2980
  }
2586
- function distance5(a, b) {
2587
- const m = a.length;
2588
- const n = b.length;
2589
- if (m === 0) return n;
2590
- if (n === 0) return m;
2591
- let prev = Array.from({ length: n + 1 }, (_, j) => j);
2592
- for (let i = 1; i <= m; i++) {
2593
- const curr = [i, ...new Array(n).fill(0)];
2594
- for (let j = 1; j <= n; j++) {
2595
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2596
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
2981
+ var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
2982
+ function validateNavObjectServability(stack) {
2983
+ const findings = [];
2984
+ if (!isRec8(stack)) return findings;
2985
+ const apps = asArray14(stack.apps);
2986
+ if (apps.length === 0) return findings;
2987
+ const ownEnable = /* @__PURE__ */ new Map();
2988
+ const objects = asArray14(stack.objects);
2989
+ for (const [oi, obj] of objects.entries()) {
2990
+ const n = strName10(obj.name);
2991
+ if (!n) continue;
2992
+ ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
2993
+ }
2994
+ if (ownEnable.size === 0) return findings;
2995
+ for (const [ai, app] of apps.entries()) {
2996
+ const appName = strName10(app.name) ?? `#${ai}`;
2997
+ const walk = (items, basePath) => {
2998
+ if (!Array.isArray(items)) return;
2999
+ for (const [ni, raw] of items.entries()) {
3000
+ if (!isRec8(raw)) continue;
3001
+ const nav = raw;
3002
+ const navPath = `${basePath}[${ni}]`;
3003
+ if (nav.type === "object") {
3004
+ const target = strName10(nav.objectName);
3005
+ const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
3006
+ if (target && declared && !canServeApiOperation(declared.enable, "list")) {
3007
+ const enable = isRec8(declared.enable) ? declared.enable : {};
3008
+ const apiDisabled = enable.apiEnabled === false;
3009
+ 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(", ")})` : "");
3010
+ const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
3011
+ const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
3012
+ findings.push({
3013
+ severity: "error",
3014
+ rule: NAV_OBJECT_UNSERVABLE,
3015
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
3016
+ // The nav entry is where the dead row is authored; the `enable`
3017
+ // key that condemns it is named in the message, because the fix
3018
+ // may belong at either end.
3019
+ path: `${navPath}.objectName`,
3020
+ message: `Navigation targets object "${target}", which cannot serve a list: ${condition} (\`${offendingKey}\`), so the list request answers ${answer} for EVERY user \u2014 platform administrators included, since that gate reads only the object's \`enable\` block and never the caller. The entry cannot be rescued with \`requiredPermissions\`: they are independent conditions. The server prunes this entry from the served \`/meta\` payload (#7912), so publishing it ships a menu row that silently is not there.`,
3021
+ hint: `Remove the nav entry, or make "${target}" listable by setting \`enable.apiEnabled: true\` and granting \`list\` in \`enable.apiMethods\`. \u26D4 Do NOT open the API on an object that is disabled on purpose \u2014 several platform objects hold credential material and are API-disabled deliberately; for those the entry is the mistake, not the \`enable\` block.`
3022
+ });
3023
+ }
3024
+ }
3025
+ if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
3026
+ }
3027
+ };
3028
+ walk(app.navigation, `apps[${ai}].navigation`);
3029
+ for (const [ari, area] of asArray14(app.areas).entries()) {
3030
+ walk(area.items, `apps[${ai}].areas[${ari}].items`);
3031
+ walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
2597
3032
  }
2598
- prev = curr;
3033
+ }
3034
+ return findings;
3035
+ }
3036
+
3037
+ // src/validate-translation-references.ts
3038
+ import { expandViewContainer } from "@objectstack/spec";
3039
+ import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
3040
+
3041
+ // src/view-walk.ts
3042
+ function isRec9(v) {
3043
+ return !!v && typeof v === "object" && !Array.isArray(v);
3044
+ }
3045
+ function strName11(v) {
3046
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3047
+ }
3048
+ function viewObjectName(view) {
3049
+ return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
3050
+ }
3051
+ function viewContainerSites(view, basePath) {
3052
+ if (!isRec9(view)) return [];
3053
+ const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3054
+ if (isRec9(view.form)) {
3055
+ sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3056
+ }
3057
+ for (const key of ["listViews", "formViews"]) {
3058
+ const container = view[key];
3059
+ if (!isRec9(container)) continue;
3060
+ const kind = key === "listViews" ? "listView" : "formView";
3061
+ for (const [subKey, sub] of Object.entries(container)) {
3062
+ if (!isRec9(sub)) continue;
3063
+ sites.push({
3064
+ view: sub,
3065
+ path: `${basePath}.${key}.${subKey}`,
3066
+ surface: `${key}.${subKey}`,
3067
+ kind
3068
+ });
3069
+ }
3070
+ }
3071
+ return sites;
3072
+ }
3073
+ function formViewSites(view, basePath) {
3074
+ return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
3075
+ }
3076
+
3077
+ // src/validate-translation-references.ts
3078
+ var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
3079
+ var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
3080
+ function isRec10(v) {
3081
+ return !!v && typeof v === "object" && !Array.isArray(v);
3082
+ }
3083
+ function asArray15(v) {
3084
+ if (Array.isArray(v)) return v;
3085
+ if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
3086
+ return [];
3087
+ }
3088
+ function strName12(v) {
3089
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3090
+ }
3091
+ function distance5(a, b) {
3092
+ const m = a.length;
3093
+ const n = b.length;
3094
+ if (m === 0) return n;
3095
+ if (n === 0) return m;
3096
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
3097
+ for (let i = 1; i <= m; i++) {
3098
+ const curr = [i, ...new Array(n).fill(0)];
3099
+ for (let j = 1; j <= n; j++) {
3100
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3101
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
3102
+ }
3103
+ prev = curr;
2599
3104
  }
2600
3105
  return prev[n];
2601
3106
  }
@@ -2640,30 +3145,52 @@ function collectViewRecord(view, factsFor) {
2640
3145
  };
2641
3146
  const addSections = (container, binding) => {
2642
3147
  if (!binding) return;
2643
- for (const section of asArray14(container.sections)) {
2644
- const sectionName = strName10(section.name);
3148
+ for (const section of asArray15(container.sections)) {
3149
+ const sectionName = strName12(section.name);
2645
3150
  if (sectionName) factsFor(binding).sections.add(sectionName);
2646
3151
  }
2647
3152
  };
2648
- const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
2649
- if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
2650
- addView(recordObject ?? listBinding, strName10(view.name));
2651
- for (const key of ["listViews", "formViews"]) {
2652
- const container = view[key];
2653
- if (!isRec7(container)) continue;
2654
- for (const [subKey, sub] of Object.entries(container)) {
2655
- if (!isRec7(sub)) continue;
3153
+ const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
3154
+ if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3155
+ addView(recordObject ?? listBinding, strName12(view.name));
3156
+ const named = namedViewKeys(view);
3157
+ for (const family of ["listViews", "formViews"]) {
3158
+ const container = view[family];
3159
+ if (!isRec10(container)) continue;
3160
+ const registryKeys = family === "listViews" ? named.list : named.form;
3161
+ let at = 0;
3162
+ for (const sub of Object.values(container)) {
3163
+ if (!sub || typeof sub !== "object") continue;
3164
+ const registryKey = registryKeys[at++];
3165
+ if (!isRec10(sub)) continue;
2656
3166
  const binding = bindingOf(sub) ?? listBinding;
2657
- addView(binding, subKey);
2658
- addView(binding, strName10(sub.name));
3167
+ addView(binding, registryKey);
2659
3168
  addSections(sub, binding);
2660
3169
  }
2661
3170
  }
2662
- if (isRec7(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3171
+ if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
2663
3172
  addSections(view, recordObject ?? listBinding);
2664
3173
  }
2665
- function viewObjectName(view) {
2666
- return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
3174
+ function defaultListViewKey(object, container) {
3175
+ if (!object || !isRec10(container.list)) return void 0;
3176
+ const item = expandViewContainer(object, container).find(
3177
+ (i) => i.viewKind === "list" && i.isDefault
3178
+ );
3179
+ if (!item) return void 0;
3180
+ const prefix = `${object}.`;
3181
+ return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
3182
+ }
3183
+ function namedViewKeys(container) {
3184
+ const object = "probe";
3185
+ const prefix = `${object}.`;
3186
+ const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
3187
+ const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3188
+ const listCount = countEntries(container.listViews);
3189
+ const formCount = countEntries(container.formViews);
3190
+ if (!listCount && !formCount) return { list: [], form: [] };
3191
+ const items = expandViewContainer(object, container);
3192
+ const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
3193
+ return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
2667
3194
  }
2668
3195
  function readOptions(field) {
2669
3196
  const raw = field.options;
@@ -2675,14 +3202,14 @@ function readOptions(field) {
2675
3202
  values.add(opt);
2676
3203
  continue;
2677
3204
  }
2678
- if (!isRec7(opt)) continue;
2679
- const value = strName10(opt.value);
3205
+ if (!isRec10(opt)) continue;
3206
+ const value = strName12(opt.value);
2680
3207
  if (!value) continue;
2681
3208
  values.add(value);
2682
- const label2 = strName10(opt.label);
3209
+ const label2 = strName12(opt.label);
2683
3210
  if (label2) byLabel.set(label2.toLowerCase(), value);
2684
3211
  }
2685
- } else if (isRec7(raw)) {
3212
+ } else if (isRec10(raw)) {
2686
3213
  for (const [value, label2] of Object.entries(raw)) {
2687
3214
  values.add(value);
2688
3215
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -2702,48 +3229,48 @@ function buildUniverse(stack) {
2702
3229
  }
2703
3230
  return facts;
2704
3231
  };
2705
- for (const obj of asArray14(stack.objects)) {
2706
- const objectName = strName10(obj.name);
3232
+ for (const obj of asArray15(stack.objects)) {
3233
+ const objectName = strName12(obj.name);
2707
3234
  if (!objectName) continue;
2708
3235
  const facts = factsFor(objectName);
2709
- for (const field of asArray14(obj.fields)) {
2710
- const fieldName = strName10(field.name);
3236
+ for (const field of asArray15(obj.fields)) {
3237
+ const fieldName = strName12(field.name);
2711
3238
  if (fieldName) facts.fields.set(fieldName, field);
2712
3239
  }
2713
- for (const action of asArray14(obj.actions)) {
2714
- const actionName = strName10(action.name);
3240
+ for (const action of asArray15(obj.actions)) {
3241
+ const actionName = strName12(action.name);
2715
3242
  if (actionName) facts.actions.set(actionName, action);
2716
3243
  }
2717
- for (const view of asArray14(obj.views)) {
2718
- collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
3244
+ for (const view of asArray15(obj.views)) {
3245
+ collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
2719
3246
  }
2720
3247
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
2721
- for (const group of asArray14(obj.fieldGroups)) {
2722
- const key = strName10(group.key) ?? strName10(group.name);
3248
+ for (const group of asArray15(obj.fieldGroups)) {
3249
+ const key = strName12(group.key) ?? strName12(group.name);
2723
3250
  if (key) facts.sections.add(key);
2724
3251
  }
2725
3252
  }
2726
- for (const view of asArray14(stack.views)) {
3253
+ for (const view of asArray15(stack.views)) {
2727
3254
  collectViewRecord(view, factsFor);
2728
3255
  }
2729
- const pages = asArray14(stack.pages);
3256
+ const pages = asArray15(stack.pages);
2730
3257
  for (let pi = 0; pi < pages.length; pi++) {
2731
3258
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
2732
3259
  if (!walked.objectName) continue;
2733
- const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
3260
+ const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
2734
3261
  if (!props) continue;
2735
- for (const section of asArray14(props.sections)) {
2736
- const sectionName = strName10(section.name);
3262
+ for (const section of asArray15(props.sections)) {
3263
+ const sectionName = strName12(section.name);
2737
3264
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
2738
3265
  }
2739
3266
  }
2740
3267
  }
2741
3268
  const globalActions = /* @__PURE__ */ new Map();
2742
3269
  const actionOwners = /* @__PURE__ */ new Map();
2743
- for (const action of asArray14(stack.actions)) {
2744
- const actionName = strName10(action.name);
3270
+ for (const action of asArray15(stack.actions)) {
3271
+ const actionName = strName12(action.name);
2745
3272
  if (!actionName) continue;
2746
- const owner = strName10(action.objectName) ?? strName10(action.object);
3273
+ const owner = strName12(action.objectName) ?? strName12(action.object);
2747
3274
  if (owner) {
2748
3275
  factsFor(owner).actions.set(actionName, action);
2749
3276
  actionOwners.set(actionName, owner);
@@ -2757,41 +3284,41 @@ function buildUniverse(stack) {
2757
3284
  }
2758
3285
  }
2759
3286
  const apps = /* @__PURE__ */ new Map();
2760
- for (const app of asArray14(stack.apps)) {
2761
- const appName = strName10(app.name);
3287
+ for (const app of asArray15(stack.apps)) {
3288
+ const appName = strName12(app.name);
2762
3289
  if (!appName) continue;
2763
3290
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
2764
3291
  const walkNav = (items) => {
2765
- for (const item of asArray14(items)) {
2766
- const id = strName10(item.id);
3292
+ for (const item of asArray15(items)) {
3293
+ const id = strName12(item.id);
2767
3294
  if (id) navIds.add(id);
2768
3295
  if (item.children) walkNav(item.children);
2769
3296
  }
2770
3297
  };
2771
3298
  walkNav(app.navigation);
2772
- for (const area of asArray14(app.areas)) {
2773
- const areaId = strName10(area.id);
3299
+ for (const area of asArray15(app.areas)) {
3300
+ const areaId = strName12(area.id);
2774
3301
  if (areaId) navIds.add(areaId);
2775
3302
  walkNav(area.navigation);
2776
3303
  }
2777
3304
  apps.set(appName, navIds);
2778
3305
  }
2779
3306
  const dashboards = /* @__PURE__ */ new Map();
2780
- for (const dash of asArray14(stack.dashboards)) {
2781
- const dashName = strName10(dash.name);
3307
+ for (const dash of asArray15(stack.dashboards)) {
3308
+ const dashName = strName12(dash.name);
2782
3309
  if (!dashName) continue;
2783
3310
  const widgets = /* @__PURE__ */ new Set();
2784
- for (const widget of asArray14(dash.widgets)) {
2785
- const id = strName10(widget.id) ?? strName10(widget.name);
3311
+ for (const widget of asArray15(dash.widgets)) {
3312
+ const id = strName12(widget.id) ?? strName12(widget.name);
2786
3313
  if (id) widgets.add(id);
2787
3314
  }
2788
3315
  const actions = /* @__PURE__ */ new Set();
2789
3316
  const headerActions = [
2790
- ...asArray14(isRec7(dash.header) ? dash.header.actions : void 0),
2791
- ...asArray14(dash.actions)
3317
+ ...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
3318
+ ...asArray15(dash.actions)
2792
3319
  ];
2793
3320
  for (const action of headerActions) {
2794
- const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
3321
+ const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
2795
3322
  if (key) actions.add(key);
2796
3323
  }
2797
3324
  dashboards.set(dashName, { widgets, actions });
@@ -2803,7 +3330,7 @@ function localePath(bundleIndex, locale) {
2803
3330
  }
2804
3331
  function validateTranslationReferences(stack) {
2805
3332
  const findings = [];
2806
- if (!isRec7(stack)) return findings;
3333
+ if (!isRec10(stack)) return findings;
2807
3334
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
2808
3335
  if (bundles.length === 0) return findings;
2809
3336
  const universe = buildUniverse(stack);
@@ -2812,13 +3339,13 @@ function validateTranslationReferences(stack) {
2812
3339
  };
2813
3340
  for (let bi = 0; bi < bundles.length; bi++) {
2814
3341
  const bundle = bundles[bi];
2815
- if (!isRec7(bundle)) continue;
3342
+ if (!isRec10(bundle)) continue;
2816
3343
  for (const [locale, rawData] of Object.entries(bundle)) {
2817
- if (!isRec7(rawData)) continue;
3344
+ if (!isRec10(rawData)) continue;
2818
3345
  const base = localePath(bi, locale);
2819
3346
  const inLocale = `locale "${locale}"`;
2820
3347
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
2821
- if (!isRec7(rawNode)) continue;
3348
+ if (!isRec10(rawNode)) continue;
2822
3349
  const objPath = `${base}.objects.${objectName}`;
2823
3350
  const facts = universe.objects.get(objectName);
2824
3351
  if (!facts) {
@@ -2844,7 +3371,7 @@ function validateTranslationReferences(stack) {
2844
3371
  );
2845
3372
  continue;
2846
3373
  }
2847
- if (!isRec7(rawField)) continue;
3374
+ if (!isRec10(rawField)) continue;
2848
3375
  checkOptionKeys(findings, {
2849
3376
  optionMap: rawField.options,
2850
3377
  field,
@@ -2926,7 +3453,7 @@ function validateTranslationReferences(stack) {
2926
3453
  );
2927
3454
  continue;
2928
3455
  }
2929
- if (!isRec7(rawApp)) continue;
3456
+ if (!isRec10(rawApp)) continue;
2930
3457
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
2931
3458
  if (navIds.has(navId)) continue;
2932
3459
  orphan(
@@ -2949,7 +3476,7 @@ function validateTranslationReferences(stack) {
2949
3476
  );
2950
3477
  continue;
2951
3478
  }
2952
- if (!isRec7(rawDash)) continue;
3479
+ if (!isRec10(rawDash)) continue;
2953
3480
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
2954
3481
  if (dash.widgets.has(widgetId)) continue;
2955
3482
  orphan(
@@ -2974,7 +3501,7 @@ function validateTranslationReferences(stack) {
2974
3501
  return findings;
2975
3502
  }
2976
3503
  function asRecord(v) {
2977
- return isRec7(v) ? v : {};
3504
+ return isRec10(v) ? v : {};
2978
3505
  }
2979
3506
  function checkOptionKeys(findings, ctx) {
2980
3507
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -2986,7 +3513,7 @@ function checkOptionKeys(findings, ctx) {
2986
3513
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
2987
3514
  where: ctx.where,
2988
3515
  path: ctx.path,
2989
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName10(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
3516
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
2990
3517
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
2991
3518
  });
2992
3519
  return;
@@ -3005,11 +3532,11 @@ function checkOptionKeys(findings, ctx) {
3005
3532
  }
3006
3533
  }
3007
3534
  function checkActionParams(findings, ctx) {
3008
- const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
3535
+ const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
3009
3536
  if (rawParams.length === 0) return;
3010
3537
  const declared = /* @__PURE__ */ new Set();
3011
- for (const param of asArray14(ctx.action.params)) {
3012
- const name = strName10(param.name) ?? strName10(param.field);
3538
+ for (const param of asArray15(ctx.action.params)) {
3539
+ const name = strName12(param.name) ?? strName12(param.field);
3013
3540
  if (name) declared.add(name);
3014
3541
  }
3015
3542
  for (const paramName of rawParams) {
@@ -3025,78 +3552,60 @@ function checkActionParams(findings, ctx) {
3025
3552
  }
3026
3553
  }
3027
3554
 
3028
- // src/validate-translatable-sections.ts
3029
- var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3030
- function isRec8(v) {
3555
+ // src/collection-entries.ts
3556
+ function isRec11(v) {
3031
3557
  return !!v && typeof v === "object" && !Array.isArray(v);
3032
3558
  }
3033
- function strName11(v) {
3034
- return typeof v === "string" && v.length > 0 ? v : void 0;
3035
- }
3036
- function viewObjectName2(view) {
3037
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
3038
- }
3039
3559
  function collectionEntries(v, base) {
3040
3560
  if (Array.isArray(v)) {
3041
3561
  const out = [];
3042
3562
  for (let i = 0; i < v.length; i++) {
3043
- if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3563
+ if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3044
3564
  }
3045
3565
  return out;
3046
3566
  }
3047
- if (isRec8(v)) {
3048
- return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3567
+ if (isRec11(v)) {
3568
+ return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3049
3569
  }
3050
3570
  return [];
3051
3571
  }
3572
+
3573
+ // src/validate-translatable-sections.ts
3574
+ var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3575
+ function isRec12(v) {
3576
+ return !!v && typeof v === "object" && !Array.isArray(v);
3577
+ }
3578
+ function strName13(v) {
3579
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3580
+ }
3052
3581
  function viewLabel(view) {
3053
- const name = strName11(view.name);
3582
+ const name = strName13(view.name);
3054
3583
  return name ? `view "${name}"` : "";
3055
3584
  }
3056
3585
  function joinWhere(...parts) {
3057
3586
  return parts.filter((p) => p.length > 0).join(" \xB7 ");
3058
3587
  }
3059
3588
  function collectViewSites(view, basePath, label2, sites) {
3060
- const recordObject = viewObjectName2(view);
3061
- const listBinding = isRec8(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
3062
- const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
3063
- sites.push({
3064
- path: `${basePath}.sections`,
3065
- surface: label2,
3066
- objectName: recordObject ?? listBinding,
3067
- sections: view.sections
3068
- });
3069
- if (isRec8(view.form)) {
3589
+ const recordObject = viewObjectName(view);
3590
+ const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3591
+ for (const site of viewContainerSites(view, basePath)) {
3070
3592
  sites.push({
3071
- path: `${basePath}.form.sections`,
3072
- surface: joinWhere(label2, "form"),
3073
- objectName: bindingOf(view.form) ?? listBinding,
3074
- sections: view.form.sections
3593
+ path: `${site.path}.sections`,
3594
+ surface: joinWhere(label2, site.surface),
3595
+ objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
3596
+ sections: site.view.sections
3075
3597
  });
3076
3598
  }
3077
- for (const key of ["listViews", "formViews"]) {
3078
- const container = view[key];
3079
- if (!isRec8(container)) continue;
3080
- for (const [subKey, sub] of Object.entries(container)) {
3081
- if (!isRec8(sub)) continue;
3082
- sites.push({
3083
- path: `${basePath}.${key}.${subKey}.sections`,
3084
- surface: joinWhere(label2, `${key}.${subKey}`),
3085
- objectName: bindingOf(sub) ?? listBinding,
3086
- sections: sub.sections
3087
- });
3088
- }
3089
- }
3090
3599
  }
3091
3600
  function translatedObjectNames(stack) {
3092
3601
  const out = /* @__PURE__ */ new Set();
3093
3602
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3094
3603
  for (const bundle of bundles) {
3095
- if (!isRec8(bundle)) continue;
3604
+ if (!isRec12(bundle)) continue;
3096
3605
  for (const data of Object.values(bundle)) {
3097
- if (!isRec8(data) || !isRec8(data.objects)) continue;
3606
+ if (!isRec12(data) || !isRec12(data.objects)) continue;
3098
3607
  for (const [objectName, node] of Object.entries(data.objects)) {
3099
- if (isRec8(node)) out.add(objectName);
3608
+ if (isRec12(node)) out.add(objectName);
3100
3609
  }
3101
3610
  }
3102
3611
  }
@@ -3108,22 +3617,22 @@ function suggestedName(label2) {
3108
3617
  }
3109
3618
  function validateTranslatableSections(stack) {
3110
3619
  const findings = [];
3111
- if (!isRec8(stack)) return findings;
3620
+ if (!isRec12(stack)) return findings;
3112
3621
  const translated = translatedObjectNames(stack);
3113
3622
  if (translated.size === 0) return findings;
3114
3623
  const sites = [];
3115
3624
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3116
- const objectName = strName11(obj.name);
3625
+ const objectName = strName13(obj.name);
3117
3626
  if (!objectName) continue;
3118
3627
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3119
3628
  collectViewSites(
3120
- { ...view, object: strName11(view.object) ?? objectName },
3629
+ { ...view, object: strName13(view.object) ?? objectName },
3121
3630
  path,
3122
3631
  viewLabel(view),
3123
3632
  sites
3124
3633
  );
3125
3634
  }
3126
- if (isRec8(obj.listViews)) {
3635
+ if (isRec12(obj.listViews)) {
3127
3636
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3128
3637
  }
3129
3638
  }
@@ -3131,13 +3640,13 @@ function validateTranslatableSections(stack) {
3131
3640
  collectViewSites(view, path, viewLabel(view), sites);
3132
3641
  }
3133
3642
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3134
- const pageName = strName11(page.name);
3643
+ const pageName = strName13(page.name);
3135
3644
  const pageLabel = pageName ? `page "${pageName}"` : "";
3136
3645
  for (const walked of walkPageComponents(page, pagePath)) {
3137
3646
  if (!walked.objectName) continue;
3138
- const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
3647
+ const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
3139
3648
  if (!props) continue;
3140
- const type = strName11(walked.component.type) ?? "component";
3649
+ const type = strName13(walked.component.type) ?? "component";
3141
3650
  sites.push({
3142
3651
  path: `${walked.path}.properties.sections`,
3143
3652
  surface: joinWhere(pageLabel, type),
@@ -3152,9 +3661,9 @@ function validateTranslatableSections(stack) {
3152
3661
  if (!Array.isArray(site.sections)) continue;
3153
3662
  for (let i = 0; i < site.sections.length; i++) {
3154
3663
  const section = site.sections[i];
3155
- if (!isRec8(section)) continue;
3156
- if (strName11(section.name)) continue;
3157
- const heading = strName11(section.label) ?? strName11(section.title);
3664
+ if (!isRec12(section)) continue;
3665
+ if (strName13(section.name)) continue;
3666
+ const heading = strName13(section.label);
3158
3667
  if (!heading) continue;
3159
3668
  const slug = suggestedName(heading);
3160
3669
  findings.push({
@@ -3172,10 +3681,10 @@ function validateTranslatableSections(stack) {
3172
3681
 
3173
3682
  // src/flow-walk.ts
3174
3683
  import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
3175
- function isRec9(v) {
3684
+ function isRec13(v) {
3176
3685
  return !!v && typeof v === "object" && !Array.isArray(v);
3177
3686
  }
3178
- function strName12(v) {
3687
+ function strName14(v) {
3179
3688
  return typeof v === "string" && v.length > 0 ? v : void 0;
3180
3689
  }
3181
3690
  var REGION_SLOTS = new Map(
@@ -3184,10 +3693,10 @@ var REGION_SLOTS = new Map(
3184
3693
  var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
3185
3694
  var MAX_REGION_DEPTH = 16;
3186
3695
  function flowNodeLabel(node, index) {
3187
- return strName12(node.label) ?? strName12(node.id) ?? `#${index}`;
3696
+ return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
3188
3697
  }
3189
3698
  function stripRegions(config) {
3190
- if (!isRec9(config)) return void 0;
3699
+ if (!isRec13(config)) return void 0;
3191
3700
  let out;
3192
3701
  for (const key of Object.keys(config)) {
3193
3702
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3198,11 +3707,11 @@ function stripRegions(config) {
3198
3707
  }
3199
3708
  function walkFlowNodes(flow, flowPath) {
3200
3709
  const out = [];
3201
- if (!isRec9(flow)) return out;
3710
+ if (!isRec13(flow)) return out;
3202
3711
  const visitList = (nodes, basePath, trail, depth) => {
3203
3712
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3204
3713
  nodes.forEach((raw, index) => {
3205
- if (!isRec9(raw)) return;
3714
+ if (!isRec13(raw)) return;
3206
3715
  const path = `${basePath}[${index}]`;
3207
3716
  out.push({
3208
3717
  node: raw,
@@ -3211,9 +3720,9 @@ function walkFlowNodes(flow, flowPath) {
3211
3720
  regionTrail: trail,
3212
3721
  depth
3213
3722
  });
3214
- const type = strName12(raw.type);
3723
+ const type = strName14(raw.type);
3215
3724
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3216
- if (!slots || !isRec9(raw.config)) return;
3725
+ if (!slots || !isRec13(raw.config)) return;
3217
3726
  const config = raw.config;
3218
3727
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3219
3728
  for (const slot of slots) {
@@ -3221,8 +3730,8 @@ function walkFlowNodes(flow, flowPath) {
3221
3730
  if (slot === "branches") {
3222
3731
  if (!Array.isArray(value)) continue;
3223
3732
  value.forEach((branch, b) => {
3224
- if (!isRec9(branch)) return;
3225
- const branchName = strName12(branch.name) ?? `#${b}`;
3733
+ if (!isRec13(branch)) return;
3734
+ const branchName = strName14(branch.name) ?? `#${b}`;
3226
3735
  visitList(
3227
3736
  branch.nodes,
3228
3737
  `${path}.config.branches[${b}].nodes`,
@@ -3232,7 +3741,7 @@ function walkFlowNodes(flow, flowPath) {
3232
3741
  });
3233
3742
  continue;
3234
3743
  }
3235
- if (!isRec9(value)) continue;
3744
+ if (!isRec13(value)) continue;
3236
3745
  visitList(
3237
3746
  value.nodes,
3238
3747
  `${path}.config.${slot}.nodes`,
@@ -3252,7 +3761,8 @@ function joinTrail(trail, segment) {
3252
3761
  // src/validate-flow-template-paths.ts
3253
3762
  var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
3254
3763
  var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
3255
- function asArray15(v) {
3764
+ var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
3765
+ function asArray16(v) {
3256
3766
  if (Array.isArray(v)) return v;
3257
3767
  if (v && typeof v === "object") {
3258
3768
  return Object.entries(v).map(([name, def]) => ({
@@ -3281,7 +3791,7 @@ var FILTER_GUARDED_NODE_TYPES = /* @__PURE__ */ new Set([
3281
3791
  ]);
3282
3792
  function fieldTypesOf(obj) {
3283
3793
  const types = /* @__PURE__ */ new Map();
3284
- for (const f of asArray15(obj.fields)) {
3794
+ for (const f of asArray16(obj.fields)) {
3285
3795
  if (typeof f.name === "string") {
3286
3796
  types.set(f.name, typeof f.type === "string" ? f.type : "");
3287
3797
  }
@@ -3378,10 +3888,10 @@ function declaredExpandOf(flow) {
3378
3888
  }
3379
3889
  function validateFlowTemplatePaths(stack) {
3380
3890
  const findings = [];
3381
- const flows = asArray15(stack.flows);
3891
+ const flows = asArray16(stack.flows);
3382
3892
  if (flows.length === 0) return findings;
3383
3893
  const objectsByName = /* @__PURE__ */ new Map();
3384
- for (const obj of asArray15(stack.objects)) {
3894
+ for (const obj of asArray16(stack.objects)) {
3385
3895
  if (typeof obj.name === "string") objectsByName.set(obj.name, obj);
3386
3896
  }
3387
3897
  flows.forEach((flow, flowIndex) => {
@@ -3394,6 +3904,7 @@ function validateFlowTemplatePaths(stack) {
3394
3904
  const obj = objectsByName.get(objectName);
3395
3905
  if (!obj) return;
3396
3906
  const fieldTypes = fieldTypesOf(obj);
3907
+ const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
3397
3908
  const expandSet = declaredExpandOf(flow);
3398
3909
  walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
3399
3910
  const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
@@ -3405,6 +3916,7 @@ function validateFlowTemplatePaths(stack) {
3405
3916
  if (leaves.length === 0) return;
3406
3917
  const seenUnknown = /* @__PURE__ */ new Set();
3407
3918
  const seenTraversal = /* @__PURE__ */ new Set();
3919
+ const seenUnprovisioned = /* @__PURE__ */ new Set();
3408
3920
  for (const leaf of leaves) {
3409
3921
  const inFilter = leaf.inFilter;
3410
3922
  for (const rest of recordRefsIn(leaf.text)) {
@@ -3412,6 +3924,19 @@ function validateFlowTemplatePaths(stack) {
3412
3924
  const hasSubPath = rest.length > 1;
3413
3925
  const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
3414
3926
  const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
3927
+ if (unprovisionedAnchors.has(head)) {
3928
+ if (!seenUnprovisioned.has(head)) {
3929
+ seenUnprovisioned.add(head);
3930
+ findings.push({
3931
+ severity: "warning",
3932
+ rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
3933
+ where,
3934
+ path: nodePath,
3935
+ message: (inFilter ? `${nodeType} filter references ` : "template references ") + `'{record.${rest.join(".")}}', and ${unprovisionedAnchorCause(objectName, head)} \u2014 ` + (inFilter ? `the token resolves to nothing on every run, which DROPS the condition from the query instead of narrowing it; the node then refuses to run at execution time (#3810).` : `the token resolves to an empty string on every run (silently).`),
3936
+ hint: unprovisionedAnchorHint(objectName, head)
3937
+ });
3938
+ }
3939
+ }
3415
3940
  if (!isKnown) {
3416
3941
  if (seenUnknown.has(head)) continue;
3417
3942
  seenUnknown.add(head);
@@ -3450,14 +3975,14 @@ function validateFlowTemplatePaths(stack) {
3450
3975
 
3451
3976
  // src/validate-ai-surface-affinity.ts
3452
3977
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
3453
- function asArray16(v) {
3978
+ function asArray17(v) {
3454
3979
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3455
3980
  if (v && typeof v === "object") {
3456
3981
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3457
3982
  }
3458
3983
  return [];
3459
3984
  }
3460
- function strName13(v) {
3985
+ function strName15(v) {
3461
3986
  return typeof v === "string" && v.length > 0 ? v : void 0;
3462
3987
  }
3463
3988
  function surfaceOf(v) {
@@ -3467,18 +3992,18 @@ function validateAiSurfaceAffinity(stack) {
3467
3992
  const findings = [];
3468
3993
  if (!stack || typeof stack !== "object") return findings;
3469
3994
  const skillsByName = /* @__PURE__ */ new Map();
3470
- for (const skill of asArray16(stack.skills)) {
3471
- const n = strName13(skill.name);
3995
+ for (const skill of asArray17(stack.skills)) {
3996
+ const n = strName15(skill.name);
3472
3997
  if (n) skillsByName.set(n, skill);
3473
3998
  }
3474
- const agents = asArray16(stack.agents);
3999
+ const agents = asArray17(stack.agents);
3475
4000
  for (let ai = 0; ai < agents.length; ai++) {
3476
4001
  const agent = agents[ai];
3477
- const agentName = strName13(agent.name) ?? `#${ai}`;
4002
+ const agentName = strName15(agent.name) ?? `#${ai}`;
3478
4003
  const agentSurface = surfaceOf(agent.surface);
3479
4004
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
3480
4005
  for (let si = 0; si < skillRefs.length; si++) {
3481
- const ref = strName13(skillRefs[si]);
4006
+ const ref = strName15(skillRefs[si]);
3482
4007
  if (!ref) continue;
3483
4008
  const skill = skillsByName.get(ref);
3484
4009
  if (!skill) continue;
@@ -3500,14 +4025,14 @@ function validateAiSurfaceAffinity(stack) {
3500
4025
  // src/validate-ai-tool-references.ts
3501
4026
  import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
3502
4027
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
3503
- function asArray17(v) {
4028
+ function asArray18(v) {
3504
4029
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3505
4030
  if (v && typeof v === "object") {
3506
4031
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3507
4032
  }
3508
4033
  return [];
3509
4034
  }
3510
- function strName14(v) {
4035
+ function strName16(v) {
3511
4036
  return typeof v === "string" && v.length > 0 ? v : void 0;
3512
4037
  }
3513
4038
  function distance6(a, b) {
@@ -3548,26 +4073,26 @@ function materialisesAsTool(action) {
3548
4073
  if (!ai || typeof ai !== "object") return false;
3549
4074
  const aiRec = ai;
3550
4075
  if (aiRec.exposed !== true) return false;
3551
- if (!strName14(aiRec.description)) return false;
3552
- const type = strName14(action.type);
4076
+ if (!strName16(aiRec.description)) return false;
4077
+ const type = strName16(action.type);
3553
4078
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
3554
4079
  if (type === "script") return Boolean(action.target || action.body);
3555
4080
  return Boolean(action.target);
3556
4081
  }
3557
4082
  function collectToolUniverse(stack) {
3558
4083
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
3559
- for (const tool of asArray17(stack.tools)) {
3560
- const n = strName14(tool.name);
4084
+ for (const tool of asArray18(stack.tools)) {
4085
+ const n = strName16(tool.name);
3561
4086
  if (n) universe.add(n);
3562
4087
  }
3563
4088
  const addActionFamily = (actions) => {
3564
- for (const action of asArray17(actions)) {
3565
- const n = strName14(action.name);
4089
+ for (const action of asArray18(actions)) {
4090
+ const n = strName16(action.name);
3566
4091
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
3567
4092
  }
3568
4093
  };
3569
4094
  addActionFamily(stack.actions);
3570
- for (const obj of asArray17(stack.objects)) {
4095
+ for (const obj of asArray18(stack.objects)) {
3571
4096
  addActionFamily(obj.actions);
3572
4097
  }
3573
4098
  return universe;
@@ -3575,13 +4100,13 @@ function collectToolUniverse(stack) {
3575
4100
  function collectUnexposedActionNames(stack) {
3576
4101
  const names = /* @__PURE__ */ new Set();
3577
4102
  const scan = (actions) => {
3578
- for (const action of asArray17(actions)) {
3579
- const n = strName14(action.name);
4103
+ for (const action of asArray18(actions)) {
4104
+ const n = strName16(action.name);
3580
4105
  if (n && !materialisesAsTool(action)) names.add(n);
3581
4106
  }
3582
4107
  };
3583
4108
  scan(stack.actions);
3584
- for (const obj of asArray17(stack.objects)) scan(obj.actions);
4109
+ for (const obj of asArray18(stack.objects)) scan(obj.actions);
3585
4110
  return names;
3586
4111
  }
3587
4112
  function validateAiToolReferences(stack) {
@@ -3599,13 +4124,13 @@ function validateAiToolReferences(stack) {
3599
4124
  }
3600
4125
  return universe.has(ref);
3601
4126
  };
3602
- const skills = asArray17(stack.skills);
4127
+ const skills = asArray18(stack.skills);
3603
4128
  for (let si = 0; si < skills.length; si++) {
3604
4129
  const skill = skills[si];
3605
- const skillName = strName14(skill.name) ?? `#${si}`;
4130
+ const skillName = strName16(skill.name) ?? `#${si}`;
3606
4131
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
3607
4132
  for (let ti = 0; ti < refs.length; ti++) {
3608
- const ref = strName14(refs[ti]);
4133
+ const ref = strName16(refs[ti]);
3609
4134
  if (!ref || resolves(ref)) continue;
3610
4135
  const isPattern = ref.endsWith("*");
3611
4136
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -3624,24 +4149,25 @@ function validateAiToolReferences(stack) {
3624
4149
 
3625
4150
  // src/validate-ai-agent-authoring.ts
3626
4151
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
3627
- function asArray18(v) {
4152
+ var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
4153
+ function asArray19(v) {
3628
4154
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3629
4155
  if (v && typeof v === "object") {
3630
4156
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3631
4157
  }
3632
4158
  return [];
3633
4159
  }
3634
- function strName15(v) {
4160
+ function strName17(v) {
3635
4161
  return typeof v === "string" && v.length > 0 ? v : void 0;
3636
4162
  }
3637
4163
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
3638
4164
  function validateAiAgentAuthoring(stack) {
3639
4165
  const findings = [];
3640
4166
  if (!stack || typeof stack !== "object") return findings;
3641
- const agents = asArray18(stack.agents);
4167
+ const agents = asArray19(stack.agents);
3642
4168
  for (let ai = 0; ai < agents.length; ai++) {
3643
4169
  const agent = agents[ai];
3644
- const name = strName15(agent.name) ?? `#${ai}`;
4170
+ const name = strName17(agent.name) ?? `#${ai}`;
3645
4171
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
3646
4172
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
3647
4173
  findings.push({
@@ -3653,6 +4179,22 @@ function validateAiAgentAuthoring(stack) {
3653
4179
  hint: isPlatformName ? `Remove the declaration; the platform owns "${name}". Extend it with skills instead.` : `Delete the agent and express its capability as skills. Everything an agent carried that a skill does not is persona text: move the useful parts of \`instructions\` into the skills' own instructions.` + (skillCount > 0 ? ` The ${skillCount} skill${skillCount === 1 ? "" : "s"} this agent references already carry the capability \u2014 they attach to the platform agent by \`surface\` affinity, so nothing is lost by dropping the persona.` : ``)
3654
4180
  });
3655
4181
  }
4182
+ const roster = [...PLATFORM_AGENT_NAMES].join(", ");
4183
+ const apps = asArray19(stack.apps);
4184
+ for (let appIdx = 0; appIdx < apps.length; appIdx++) {
4185
+ const app = apps[appIdx];
4186
+ const defaultAgent = strName17(app.defaultAgent);
4187
+ if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
4188
+ const appName = strName17(app.name) ?? `#${appIdx}`;
4189
+ findings.push({
4190
+ severity: "warning",
4191
+ rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
4192
+ where: `app "${appName}".defaultAgent`,
4193
+ path: `apps[${appIdx}].defaultAgent`,
4194
+ message: `app "${appName}" pins \`defaultAgent\` to "${defaultAgent}", which is not in the platform agent roster (${roster}). The kernel ships exactly two agents (ADR-0063 \xA72) and resolves this key against them and their legacy aliases only \u2014 an unrecognized name is not rejected, it silently falls back to the platform default at runtime, so the pin has no effect and the value drifts from what actually serves the app.`,
4195
+ hint: `Set \`defaultAgent\` to one of the platform agent names: ${roster}. If the goal is a dedicated persona or capability, express it as skills instead \u2014 they attach to "ask" / "build" by surface affinity, not as a custom \`defaultAgent\` value.`
4196
+ });
4197
+ }
3656
4198
  return findings;
3657
4199
  }
3658
4200
 
@@ -3739,24 +4281,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
3739
4281
  "owner",
3740
4282
  "record_type"
3741
4283
  ]);
3742
- var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3743
- function asArray19(v) {
3744
- if (Array.isArray(v)) return v.filter((x) => isRec10(x));
3745
- if (isRec10(v)) {
4284
+ var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4285
+ function asArray20(v) {
4286
+ if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4287
+ if (isRec14(v)) {
3746
4288
  return Object.entries(v).map(([name, def]) => ({
3747
4289
  name,
3748
- ...isRec10(def) ? def : {}
4290
+ ...isRec14(def) ? def : {}
3749
4291
  }));
3750
4292
  }
3751
4293
  return [];
3752
4294
  }
3753
4295
  function indexObjectFields2(stack) {
3754
4296
  const out = /* @__PURE__ */ new Map();
3755
- for (const obj of asArray19(stack.objects)) {
4297
+ for (const obj of asArray20(stack.objects)) {
3756
4298
  const name = typeof obj.name === "string" ? obj.name : void 0;
3757
4299
  if (!name) continue;
3758
4300
  const names = /* @__PURE__ */ new Set();
3759
- for (const f of asArray19(obj.fields)) {
4301
+ for (const f of asArray20(obj.fields)) {
3760
4302
  if (typeof f.name === "string" && f.name) names.add(f.name);
3761
4303
  }
3762
4304
  out.set(name, names);
@@ -3886,12 +4428,12 @@ ${source}
3886
4428
  }
3887
4429
  function validateHookBodyWrites(stack) {
3888
4430
  const findings = [];
3889
- const hooks = asArray19(stack.hooks);
4431
+ const hooks = asArray20(stack.hooks);
3890
4432
  if (hooks.length === 0) return findings;
3891
4433
  let objectFields = null;
3892
4434
  hooks.forEach((hook, hookIndex) => {
3893
4435
  const body = hook.body;
3894
- if (!isRec10(body) || body.language !== "js") return;
4436
+ if (!isRec14(body) || body.language !== "js") return;
3895
4437
  const source = body.source;
3896
4438
  if (typeof source !== "string" || source.trim() === "") return;
3897
4439
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -3962,13 +4504,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
3962
4504
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
3963
4505
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
3964
4506
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
3965
- var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3966
- function asArray20(v) {
3967
- if (Array.isArray(v)) return v.filter((x) => isRec11(x));
3968
- if (isRec11(v)) {
4507
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4508
+ function asArray21(v) {
4509
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4510
+ if (isRec15(v)) {
3969
4511
  return Object.entries(v).map(([name, def]) => ({
3970
4512
  name,
3971
- ...isRec11(def) ? def : {}
4513
+ ...isRec15(def) ? def : {}
3972
4514
  }));
3973
4515
  }
3974
4516
  return [];
@@ -3982,11 +4524,11 @@ function collectActionBodies(stack) {
3982
4524
  const sites = [];
3983
4525
  const seen = /* @__PURE__ */ new Set();
3984
4526
  const collect = (actions, pathPrefix, parentObject) => {
3985
- asArray20(actions).forEach((action, index) => {
4527
+ asArray21(actions).forEach((action, index) => {
3986
4528
  const type = typeof action.type === "string" ? action.type : "script";
3987
4529
  if (type !== "script") return;
3988
4530
  const body = action.body;
3989
- if (!isRec11(body) || body.language !== "js") return;
4531
+ if (!isRec15(body) || body.language !== "js") return;
3990
4532
  const source = body.source;
3991
4533
  if (typeof source !== "string" || source.trim() === "") return;
3992
4534
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -3997,7 +4539,7 @@ function collectActionBodies(stack) {
3997
4539
  });
3998
4540
  };
3999
4541
  collect(stack.actions, "actions");
4000
- asArray20(stack.objects).forEach((obj, objIndex) => {
4542
+ asArray21(stack.objects).forEach((obj, objIndex) => {
4001
4543
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
4002
4544
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
4003
4545
  });
@@ -4005,7 +4547,7 @@ function collectActionBodies(stack) {
4005
4547
  }
4006
4548
  function validateActionBodyWrites(stack) {
4007
4549
  const findings = [];
4008
- if (!isRec11(stack)) return findings;
4550
+ if (!isRec15(stack)) return findings;
4009
4551
  const sites = collectActionBodies(stack);
4010
4552
  if (sites.length === 0) return findings;
4011
4553
  let objectFields = null;
@@ -4063,13 +4605,13 @@ function fixHint2(field, declared) {
4063
4605
  import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
4064
4606
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
4065
4607
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4066
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4067
- function asArray21(v) {
4068
- if (Array.isArray(v)) return v.filter((x) => isRec12(x));
4069
- if (isRec12(v)) {
4608
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4609
+ function asArray22(v) {
4610
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4611
+ if (isRec16(v)) {
4070
4612
  return Object.entries(v).map(([name, def]) => ({
4071
4613
  name,
4072
- ...isRec12(def) ? def : {}
4614
+ ...isRec16(def) ? def : {}
4073
4615
  }));
4074
4616
  }
4075
4617
  return [];
@@ -4082,8 +4624,8 @@ function readLiteralObjectName(config) {
4082
4624
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4083
4625
  function validateFlowNodeWrites(stack) {
4084
4626
  const findings = [];
4085
- if (!isRec12(stack)) return findings;
4086
- const flows = asArray21(stack.flows);
4627
+ if (!isRec16(stack)) return findings;
4628
+ const flows = asArray22(stack.flows);
4087
4629
  if (flows.length === 0) return findings;
4088
4630
  let objectFields = null;
4089
4631
  flows.forEach((flow, flowIndex) => {
@@ -4091,10 +4633,10 @@ function validateFlowNodeWrites(stack) {
4091
4633
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4092
4634
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4093
4635
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4094
- const config = isRec12(node.config) ? node.config : void 0;
4636
+ const config = isRec16(node.config) ? node.config : void 0;
4095
4637
  if (!config) return;
4096
4638
  const fields = config.fields;
4097
- if (!isRec12(fields)) return;
4639
+ if (!isRec16(fields)) return;
4098
4640
  const written = Object.keys(fields);
4099
4641
  if (written.length === 0) return;
4100
4642
  const objectName = readLiteralObjectName(config);
@@ -4128,7 +4670,7 @@ function fixHint3(field, declared) {
4128
4670
  // src/validate-readonly-flow-writes.ts
4129
4671
  var FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
4130
4672
  var FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
4131
- function asArray22(v) {
4673
+ function asArray23(v) {
4132
4674
  if (Array.isArray(v)) return v;
4133
4675
  if (v && typeof v === "object") {
4134
4676
  return Object.entries(v).map(([name, def]) => ({
@@ -4169,9 +4711,9 @@ function readLiteralObjectName2(config) {
4169
4711
  }
4170
4712
  function validateReadonlyFlowWrites(stack) {
4171
4713
  const findings = [];
4172
- const flows = asArray22(stack.flows);
4714
+ const flows = asArray23(stack.flows);
4173
4715
  if (flows.length === 0) return findings;
4174
- const roIndex = buildReadonlyIndex(asArray22(stack.objects));
4716
+ const roIndex = buildReadonlyIndex(asArray23(stack.objects));
4175
4717
  flows.forEach((flow, flowIndex) => {
4176
4718
  if (flow.runAs === "system") return;
4177
4719
  const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
@@ -4230,11 +4772,11 @@ import {
4230
4772
  import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
4231
4773
 
4232
4774
  // src/zod-issue-format.ts
4233
- var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4775
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4234
4776
  var valueAtPath = (root, path) => {
4235
4777
  let cur = root;
4236
4778
  for (const key of path) {
4237
- if (!isRec13(cur) && !Array.isArray(cur)) return void 0;
4779
+ if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
4238
4780
  cur = cur[key];
4239
4781
  }
4240
4782
  return cur;
@@ -4279,7 +4821,7 @@ function loadTypeScript2() {
4279
4821
  }
4280
4822
  return cachedTs2;
4281
4823
  }
4282
- var asArray23 = (v) => Array.isArray(v) ? v : [];
4824
+ var asArray24 = (v) => Array.isArray(v) ? v : [];
4283
4825
  var BLOCKS = new Map(
4284
4826
  REACT_BLOCKS.map((b) => [
4285
4827
  b.tag,
@@ -4367,12 +4909,13 @@ function filterAttrValue(tsc, sf, attr) {
4367
4909
  return perPosition(init.expression);
4368
4910
  }
4369
4911
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
4912
+ var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
4370
4913
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
4371
4914
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4372
4915
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4373
4916
  function checkChartDrillDown(raw, push2) {
4374
4917
  if (raw === void 0 || raw === NOT_STATIC) return;
4375
- if (!isRec14(raw)) {
4918
+ if (!isRec18(raw)) {
4376
4919
  push2(
4377
4920
  "error",
4378
4921
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4395,7 +4938,7 @@ function checkChartDrillDown(raw, push2) {
4395
4938
  }
4396
4939
  function checkChartAggregate(raw, push2) {
4397
4940
  if (raw === void 0 || raw === NOT_STATIC) return;
4398
- if (!isRec14(raw)) {
4941
+ if (!isRec18(raw)) {
4399
4942
  push2(
4400
4943
  "error",
4401
4944
  REACT_CHART_AGGREGATE_INVALID,
@@ -4410,7 +4953,7 @@ function checkChartAggregate(raw, push2) {
4410
4953
  "warning",
4411
4954
  REACT_CHART_AGGREGATE_INVALID,
4412
4955
  "aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
4413
- "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. Deliberate single-value charts are tolerated at warning level for now: ChartAggregateSchema declares groupBy required while ObjectChart honours its absence by falling back to xAxisKey \u2014 objectstack#5583 decides which of the two moves."
4956
+ "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
4414
4957
  );
4415
4958
  }
4416
4959
  const parsed = ChartAggregateSchema.safeParse(raw);
@@ -4426,9 +4969,9 @@ function checkChartAggregate(raw, push2) {
4426
4969
  );
4427
4970
  }
4428
4971
  }
4429
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4972
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4430
4973
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4431
- function checkObjectChart(attrs, objectFields, findings) {
4974
+ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
4432
4975
  const { values, where, path } = attrs;
4433
4976
  const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
4434
4977
  checkChartDrillDown(values.get("drillDown"), push2);
@@ -4436,18 +4979,29 @@ function checkObjectChart(attrs, objectFields, findings) {
4436
4979
  const aggregate = values.get("aggregate");
4437
4980
  checkChartAggregate(aggregate, push2);
4438
4981
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4439
- if (!isRec14(aggregate)) return;
4982
+ if (!isRec18(aggregate)) return;
4440
4983
  const fn = strOf(aggregate.function);
4441
4984
  const field = strOf(aggregate.field);
4442
4985
  const groupBy = aggregate.groupBy;
4443
- const groupByField = strOf(groupBy) ?? (isRec14(groupBy) ? strOf(groupBy.field) : void 0);
4986
+ const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
4444
4987
  const objectName = strOf(values.get("objectName"));
4445
4988
  const known = objectName ? objectFields.get(objectName) : void 0;
4446
4989
  if (objectName && known) {
4990
+ const anchors = unprovisionedAnchors.get(objectName);
4447
4991
  const fieldRef = (name, prop) => {
4448
4992
  if (!name) return;
4449
4993
  if (name.includes(".")) return;
4450
- if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
4994
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) {
4995
+ if (anchors?.has(name)) {
4996
+ push2(
4997
+ "warning",
4998
+ REACT_CHART_FIELD_UNPROVISIONED,
4999
+ `aggregate.${prop} "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 the aggregate query reads a column that is empty on every row, so the chart ${prop === "groupBy" ? "groups everything into one empty bucket" : "aggregates nothing"} instead of failing.`,
5000
+ unprovisionedAnchorHint(objectName, name)
5001
+ );
5002
+ }
5003
+ return;
5004
+ }
4451
5005
  push2(
4452
5006
  "error",
4453
5007
  REACT_CHART_FIELD_UNKNOWN,
@@ -4473,18 +5027,18 @@ function checkObjectChart(attrs, objectFields, findings) {
4473
5027
  );
4474
5028
  };
4475
5029
  const xAxisRaw = values.get("xAxis");
4476
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec14(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5030
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
4477
5031
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
4478
5032
  axisRef(categoryAxis, categoryProp);
4479
5033
  const yAxisRaw = values.get("yAxis");
4480
5034
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
4481
5035
  for (const a of yAxisList) {
4482
- axisRef(strOf(a) ?? (isRec14(a) ? strOf(a.field) : void 0), "yAxis[].field");
5036
+ axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
4483
5037
  }
4484
5038
  const series = values.get("series");
4485
5039
  if (Array.isArray(series)) {
4486
5040
  for (const s of series) {
4487
- if (!isRec14(s)) continue;
5041
+ if (!isRec18(s)) continue;
4488
5042
  const dataKey = strOf(s.dataKey);
4489
5043
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
4490
5044
  }
@@ -4540,7 +5094,7 @@ function subformFieldRefs(value, basePath) {
4540
5094
  if (!Array.isArray(value)) return { child, parent };
4541
5095
  for (let i = 0; i < value.length; i++) {
4542
5096
  const sub = value[i];
4543
- if (!isRec14(sub)) continue;
5097
+ if (!isRec18(sub)) continue;
4544
5098
  const at = (key) => `${basePath}[${i}].${key}`;
4545
5099
  child.push({
4546
5100
  objectName: strOf(sub.childObject),
@@ -4585,20 +5139,20 @@ function reactFieldRefs(spec, values, basePath) {
4585
5139
  }
4586
5140
  for (const key of spec.nestedFields ?? []) {
4587
5141
  const v = readable(key);
4588
- if (isRec14(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5142
+ if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
4589
5143
  }
4590
5144
  for (const key of spec.sections ?? []) {
4591
5145
  const v = readable(key);
4592
5146
  if (!Array.isArray(v)) continue;
4593
5147
  for (let i = 0; i < v.length; i++) {
4594
5148
  const section = v[i];
4595
- if (!isRec14(section)) continue;
5149
+ if (!isRec18(section)) continue;
4596
5150
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
4597
5151
  }
4598
5152
  }
4599
5153
  for (const key of spec.keyedByField ?? []) {
4600
5154
  const v = readable(key);
4601
- if (!isRec14(v)) continue;
5155
+ if (!isRec18(v)) continue;
4602
5156
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
4603
5157
  }
4604
5158
  for (const key of spec.filterArrays ?? []) {
@@ -4606,22 +5160,30 @@ function reactFieldRefs(spec, values, basePath) {
4606
5160
  }
4607
5161
  return { own, queried };
4608
5162
  }
4609
- function checkBlockFieldProps(tag, values, objectFields, where, path) {
5163
+ function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
4610
5164
  const objectName = strOf(values.get("objectName"));
4611
5165
  const out = [];
4612
5166
  const spec = REACT_FIELD_SPECS[tag];
4613
5167
  if (spec) {
4614
5168
  const { own, queried } = reactFieldRefs(spec, values, path);
4615
- out.push(...checkFieldRefs(own, objectName, objectFields, where));
4616
- out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
5169
+ out.push(
5170
+ ...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
5171
+ );
5172
+ out.push(
5173
+ ...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
5174
+ );
4617
5175
  }
4618
5176
  if (tag === "ObjectForm") {
4619
5177
  const raw = values.get("subforms");
4620
5178
  const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
4621
5179
  for (const sub of subs.child) {
4622
- out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
5180
+ out.push(
5181
+ ...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
5182
+ );
4623
5183
  }
4624
- out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
5184
+ out.push(
5185
+ ...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
5186
+ );
4625
5187
  }
4626
5188
  const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
4627
5189
  if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
@@ -4630,7 +5192,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
4630
5192
  componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
4631
5193
  objectName,
4632
5194
  objectFields,
4633
- where
5195
+ where,
5196
+ "skipped",
5197
+ unprovisionedAnchors
4634
5198
  )
4635
5199
  );
4636
5200
  }
@@ -4662,8 +5226,9 @@ function localComponentNames(tsc, sf) {
4662
5226
  function validateReactPageProps(stack) {
4663
5227
  const findings = [];
4664
5228
  const objectFields = indexObjectFields(stack);
5229
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
4665
5230
  const searchTargets = indexObjectSearchTargets(stack);
4666
- const pages = asArray23(stack.pages);
5231
+ const pages = asArray24(stack.pages);
4667
5232
  for (let p = 0; p < pages.length; p++) {
4668
5233
  const page = pages[p];
4669
5234
  if (!page || page.kind !== "react") continue;
@@ -4744,7 +5309,7 @@ function validateReactPageProps(stack) {
4744
5309
  }
4745
5310
  }
4746
5311
  if (tag === "ObjectChart" && !hasSpread) {
4747
- checkObjectChart({ values, where, path }, objectFields, findings);
5312
+ checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
4748
5313
  }
4749
5314
  if (tag === "ListView" && !hasSpread) {
4750
5315
  findings.push(
@@ -4760,7 +5325,7 @@ function validateReactPageProps(stack) {
4760
5325
  }
4761
5326
  if (!hasSpread) {
4762
5327
  findings.push(
4763
- ...checkBlockFieldProps(tag, values, objectFields, where, path)
5328
+ ...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
4764
5329
  );
4765
5330
  }
4766
5331
  }
@@ -4787,6 +5352,15 @@ var REFERENCE_INTEGRITY_RULES = [
4787
5352
  // `action` is deliberately absent (validateActionNameRefs owns it) and so is
4788
5353
  // `component` (an unregistered ref renders a named diagnostic, not silence).
4789
5354
  { name: "validateNavTargetRefs", run: validateNavTargetRefs },
5355
+ // [#7912] The THIRD question about a nav entry, after "does the target
5356
+ // resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
5357
+ // destination serve at all? An object's own `enable` block can make its list
5358
+ // answer 404/405 for every persona, and no gate authorable on the entry
5359
+ // expresses that — which is how #7544's dead row survived review for a year.
5360
+ // The server now prunes such an entry from the `/meta` payload; the
5361
+ // maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
5362
+ // prune is never silent to the author who wrote the row.
5363
+ { name: "validateNavObjectServability", run: validateNavObjectServability },
4790
5364
  { name: "validateTranslationReferences", run: validateTranslationReferences },
4791
5365
  // The same family from the other end (#5417). Its sibling above asks "does
4792
5366
  // this bundle key resolve?"; this one asks "is there a key at all?" — a form
@@ -4885,13 +5459,13 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
4885
5459
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
4886
5460
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
4887
5461
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
4888
- function isRec15(v) {
5462
+ function isRec19(v) {
4889
5463
  return !!v && typeof v === "object" && !Array.isArray(v);
4890
5464
  }
4891
- function strName16(v) {
5465
+ function strName18(v) {
4892
5466
  return typeof v === "string" && v.length > 0 ? v : void 0;
4893
5467
  }
4894
- function asArray24(v) {
5468
+ function asArray25(v) {
4895
5469
  if (Array.isArray(v)) return v;
4896
5470
  if (v && typeof v === "object") {
4897
5471
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4902,23 +5476,36 @@ var PROPS_SCHEMAS = ComponentPropsMap;
4902
5476
  var DATASOURCE_SUPPLIED_PROP = "object";
4903
5477
  function suppliedByDataSource(issue, component) {
4904
5478
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
4905
- const dataSource = isRec15(component.dataSource) ? component.dataSource : void 0;
4906
- return strName16(dataSource?.object) !== void 0;
5479
+ const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
5480
+ return strName18(dataSource?.object) !== void 0;
5481
+ }
5482
+ function unrecognizedKeysFromUnionArm(issue) {
5483
+ if (issue.code !== "invalid_union") return void 0;
5484
+ const arms = issue.errors;
5485
+ if (!arms || arms.length === 0) return void 0;
5486
+ let found;
5487
+ for (const arm of arms) {
5488
+ const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
5489
+ if (keyIssues.length === 0) continue;
5490
+ if (keyIssues.length !== arm.length || found) return void 0;
5491
+ found = keyIssues[0];
5492
+ }
5493
+ return found;
4907
5494
  }
4908
5495
  function validateComponentProps(stack) {
4909
5496
  const findings = [];
4910
- if (!isRec15(stack)) return findings;
4911
- const pages = asArray24(stack.pages);
5497
+ if (!isRec19(stack)) return findings;
5498
+ const pages = asArray25(stack.pages);
4912
5499
  for (let pi = 0; pi < pages.length; pi++) {
4913
5500
  const page = pages[pi];
4914
- if (!isRec15(page)) continue;
4915
- const pageName = strName16(page.name) ?? `#${pi}`;
5501
+ if (!isRec19(page)) continue;
5502
+ const pageName = strName18(page.name) ?? `#${pi}`;
4916
5503
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4917
- const type = strName16(component.type);
5504
+ const type = strName18(component.type);
4918
5505
  if (!type) continue;
4919
5506
  const schema = PROPS_SCHEMAS[type];
4920
5507
  if (!schema) continue;
4921
- const props = isRec15(component.properties) ? component.properties : void 0;
5508
+ const props = isRec19(component.properties) ? component.properties : void 0;
4922
5509
  if (!props) continue;
4923
5510
  const where = `page "${pageName}" \xB7 ${type}`;
4924
5511
  const base = `${path}.properties`;
@@ -4937,6 +5524,20 @@ function validateComponentProps(stack) {
4937
5524
  for (const issue of parsed.error?.issues ?? []) {
4938
5525
  if (suppliedByDataSource(issue, component)) continue;
4939
5526
  const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
5527
+ const armIssue = unrecognizedKeysFromUnionArm(issue);
5528
+ if (armIssue) {
5529
+ for (const key of armIssue.keys ?? []) {
5530
+ findings.push({
5531
+ severity: "warning",
5532
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
5533
+ where,
5534
+ path: `${at}.${key}`,
5535
+ message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
5536
+ hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
5537
+ });
5538
+ }
5539
+ continue;
5540
+ }
4940
5541
  if (issue.code === "unrecognized_keys") {
4941
5542
  for (const key of issue.keys ?? []) {
4942
5543
  findings.push({
@@ -5167,7 +5768,7 @@ function looksLikeTailwind(className) {
5167
5768
  return false;
5168
5769
  });
5169
5770
  }
5170
- function asArray25(v) {
5771
+ function asArray26(v) {
5171
5772
  if (Array.isArray(v)) return v;
5172
5773
  if (v && typeof v === "object") {
5173
5774
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5260,13 +5861,13 @@ function checkNode(node, pageName, path, findings) {
5260
5861
  }
5261
5862
  function validateResponsiveStyles(stack) {
5262
5863
  const findings = [];
5263
- const pages = asArray25(stack.pages);
5864
+ const pages = asArray26(stack.pages);
5264
5865
  for (let p = 0; p < pages.length; p++) {
5265
5866
  const page = pages[p];
5266
5867
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
5267
- const regions = asArray25(page.regions);
5868
+ const regions = asArray26(page.regions);
5268
5869
  for (let r = 0; r < regions.length; r++) {
5269
- const components = asArray25(regions[r].components);
5870
+ const components = asArray26(regions[r].components);
5270
5871
  for (let c = 0; c < components.length; c++) {
5271
5872
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
5272
5873
  }
@@ -5277,10 +5878,10 @@ function validateResponsiveStyles(stack) {
5277
5878
 
5278
5879
  // src/validate-jsx-pages.ts
5279
5880
  import { parseJsx, compile } from "@objectstack/sdui-parser";
5280
- var asArray26 = (v) => Array.isArray(v) ? v : [];
5881
+ var asArray27 = (v) => Array.isArray(v) ? v : [];
5281
5882
  function validateJsxPages(stack, opts = {}) {
5282
5883
  const findings = [];
5283
- const pages = asArray26(stack.pages);
5884
+ const pages = asArray27(stack.pages);
5284
5885
  for (let p = 0; p < pages.length; p++) {
5285
5886
  const page = pages[p];
5286
5887
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -5327,10 +5928,10 @@ function loadSucraseTransform() {
5327
5928
  }
5328
5929
  return cachedTransform;
5329
5930
  }
5330
- var asArray27 = (v) => Array.isArray(v) ? v : [];
5931
+ var asArray28 = (v) => Array.isArray(v) ? v : [];
5331
5932
  function validateReactPages(stack) {
5332
5933
  const findings = [];
5333
- const pages = asArray27(stack.pages);
5934
+ const pages = asArray28(stack.pages);
5334
5935
  for (let p = 0; p < pages.length; p++) {
5335
5936
  const page = pages[p];
5336
5937
  if (!page || page.kind !== "react") continue;
@@ -5367,11 +5968,11 @@ function validateReactPages(stack) {
5367
5968
 
5368
5969
  // src/validate-page-source-styling.ts
5369
5970
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
5370
- var asArray28 = (v) => Array.isArray(v) ? v : [];
5971
+ var asArray29 = (v) => Array.isArray(v) ? v : [];
5371
5972
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
5372
5973
  function validatePageSourceStyling(stack) {
5373
5974
  const findings = [];
5374
- const pages = asArray28(stack.pages);
5975
+ const pages = asArray29(stack.pages);
5375
5976
  for (let p = 0; p < pages.length; p++) {
5376
5977
  const page = pages[p];
5377
5978
  if (!page) continue;
@@ -5399,7 +6000,7 @@ function validatePageSourceStyling(stack) {
5399
6000
  // src/validate-capability-references.ts
5400
6001
  import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
5401
6002
  var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
5402
- function asArray29(v) {
6003
+ function asArray30(v) {
5403
6004
  if (Array.isArray(v)) return v;
5404
6005
  if (v && typeof v === "object") {
5405
6006
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5424,13 +6025,13 @@ function validateCapabilityReferences(stack) {
5424
6025
  const findings = [];
5425
6026
  if (!stack || typeof stack !== "object") return findings;
5426
6027
  const known = new Set(PLATFORM_CAPABILITY_NAMES);
5427
- for (const cap of asArray29(stack.capabilities)) {
6028
+ for (const cap of asArray30(stack.capabilities)) {
5428
6029
  if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
5429
6030
  }
5430
- for (const ps of asArray29(stack.permissions)) {
6031
+ for (const ps of asArray30(stack.permissions)) {
5431
6032
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
5432
6033
  }
5433
- for (const seed of asArray29(stack.data)) {
6034
+ for (const seed of asArray30(stack.data)) {
5434
6035
  if (seed.object !== "sys_capability") continue;
5435
6036
  for (const rec of Array.isArray(seed.records) ? seed.records : []) {
5436
6037
  const name = rec?.name;
@@ -5449,7 +6050,7 @@ function validateCapabilityReferences(stack) {
5449
6050
  hint
5450
6051
  });
5451
6052
  };
5452
- const objects = asArray29(stack.objects);
6053
+ const objects = asArray30(stack.objects);
5453
6054
  for (let i = 0; i < objects.length; i++) {
5454
6055
  const obj = objects[i];
5455
6056
  if (!obj || typeof obj !== "object") continue;
@@ -5458,27 +6059,27 @@ function validateCapabilityReferences(stack) {
5458
6059
  for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
5459
6060
  flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
5460
6061
  }
5461
- const fields = asArray29(obj.fields);
6062
+ const fields = asArray30(obj.fields);
5462
6063
  for (const f of fields) {
5463
6064
  const fname = typeof f.name === "string" ? f.name : "(field)";
5464
6065
  for (const cap of asCapArray(f.requiredPermissions)) {
5465
6066
  flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
5466
6067
  }
5467
6068
  }
5468
- for (const [ai, action] of asArray29(obj.actions).entries()) {
6069
+ for (const [ai, action] of asArray30(obj.actions).entries()) {
5469
6070
  const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
5470
6071
  for (const cap of asCapArray(action.requiredPermissions)) {
5471
6072
  flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
5472
6073
  }
5473
6074
  }
5474
6075
  }
5475
- for (const [i, action] of asArray29(stack.actions).entries()) {
6076
+ for (const [i, action] of asArray30(stack.actions).entries()) {
5476
6077
  const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
5477
6078
  for (const cap of asCapArray(action.requiredPermissions)) {
5478
6079
  flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
5479
6080
  }
5480
6081
  }
5481
- const apps = asArray29(stack.apps);
6082
+ const apps = asArray30(stack.apps);
5482
6083
  for (let i = 0; i < apps.length; i++) {
5483
6084
  const app = apps[i];
5484
6085
  if (!app || typeof app !== "object") continue;
@@ -5511,8 +6112,9 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
5511
6112
  var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
5512
6113
  var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
5513
6114
  var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
6115
+ var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
5514
6116
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
5515
- function asArray30(v) {
6117
+ function asArray31(v) {
5516
6118
  if (Array.isArray(v)) return v;
5517
6119
  if (v && typeof v === "object") {
5518
6120
  return Object.entries(v).map(([name, def]) => ({
@@ -5528,6 +6130,11 @@ function renderNonObject(v) {
5528
6130
  if (t === "bigint") return `${String(v)}n (a bigint)`;
5529
6131
  return `a ${t}`;
5530
6132
  }
6133
+ function renderTriggerToken(v) {
6134
+ if (typeof v === "string") return `'${v}'`;
6135
+ const json = JSON.stringify(v);
6136
+ return json === void 0 ? `a ${typeof v}` : json;
6137
+ }
5531
6138
  function startNodeOf(flow) {
5532
6139
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
5533
6140
  const index = nodes.findIndex((n) => n?.type === "start");
@@ -5535,10 +6142,10 @@ function startNodeOf(flow) {
5535
6142
  }
5536
6143
  function validateFlowTriggerReadiness(stack) {
5537
6144
  const findings = [];
5538
- const flows = asArray30(stack.flows);
6145
+ const flows = asArray31(stack.flows);
5539
6146
  if (flows.length === 0) return findings;
5540
6147
  const objectNames = new Set(
5541
- asArray30(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
6148
+ asArray31(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
5542
6149
  );
5543
6150
  flows.forEach((flow, flowIndex) => {
5544
6151
  const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
@@ -5647,6 +6254,25 @@ function validateFlowTriggerReadiness(stack) {
5647
6254
  hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
5648
6255
  });
5649
6256
  }
6257
+ const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
6258
+ if (start && flow.type === "record_change" && !routesToSomeTrigger) {
6259
+ const hasTriggerType = config.triggerType != null;
6260
+ findings.push({
6261
+ // `error` (#5762's criterion, applied to a fourth id). The verdict is
6262
+ // the engine's own routing chain — literal `startsWith`/`typeof` tests
6263
+ // with no registry lookup in them — so no installed package can make
6264
+ // this token resolve. `registerTrigger` is keyed by the RESOLVED type,
6265
+ // which is the near-miss worth stating: a plugin can supply the
6266
+ // record-change trigger itself, and it still would not help, because
6267
+ // the flow never reaches the point of asking for one.
6268
+ severity: "error",
6269
+ rule: FLOW_TRIGGER_UNROUTABLE,
6270
+ where: `flow "${flowName}" \u203A start node`,
6271
+ path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
6272
+ message: `declares type: 'record_change' but ` + (hasTriggerType ? `its start node's triggerType is ${renderTriggerToken(config.triggerType)}, which the engine routes to NO trigger` : `its start node has no triggerType at all, so there is nothing for the engine to route`) + ` \u2014 it binds a record-change flow only for a token starting with 'record-', so this flow is demoted to a manual one and never fires. Nothing NAMES it: the unbound-flow audit resolves the same binding and skips the flow as "manual \u2014 nothing to bind", so neither the boot warning nor the startup summary lists it; the only trace is the banner's flow count being one higher than its bound count.`,
6273
+ hint: `Use record-{before,after}-{create,update,delete,write} ('write' is create OR update in one flow, #3427; create/insert are synonyms). If the flow really is launched by hand or from a screen, declare type: 'autolaunched' or 'screen' instead of 'record_change' \u2014 those types have no trigger to be missing.`
6274
+ });
6275
+ }
5650
6276
  if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
5651
6277
  findings.push({
5652
6278
  severity: "warning",
@@ -5692,7 +6318,7 @@ var TYPE_FIX = {
5692
6318
  business_unit: "department",
5693
6319
  bu: "department"
5694
6320
  };
5695
- function asArray31(v) {
6321
+ function asArray32(v) {
5696
6322
  if (Array.isArray(v)) return v;
5697
6323
  if (v && typeof v === "object") {
5698
6324
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5702,7 +6328,7 @@ function asArray31(v) {
5702
6328
  function validateApprovalApprovers(stack) {
5703
6329
  const findings = [];
5704
6330
  if (!stack || typeof stack !== "object") return findings;
5705
- const flows = asArray31(stack.flows);
6331
+ const flows = asArray32(stack.flows);
5706
6332
  const validTypes = new Set(ApproverType.options);
5707
6333
  for (let fi = 0; fi < flows.length; fi++) {
5708
6334
  const flow = flows[fi];
@@ -5885,7 +6511,7 @@ function validateApprovalApprovers(stack) {
5885
6511
  import { objectTitleCompleteness } from "@objectstack/spec/data";
5886
6512
  var TITLE_FORMAT_RETIRED = "title-format-retired";
5887
6513
  var TITLE_UNRESOLVABLE = "title-unresolvable";
5888
- function asArray32(v) {
6514
+ function asArray33(v) {
5889
6515
  if (Array.isArray(v)) return v;
5890
6516
  if (v && typeof v === "object") {
5891
6517
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5894,7 +6520,7 @@ function asArray32(v) {
5894
6520
  }
5895
6521
  function validateRecordTitle(stack) {
5896
6522
  const findings = [];
5897
- const objects = asArray32(stack.objects);
6523
+ const objects = asArray33(stack.objects);
5898
6524
  for (let i = 0; i < objects.length; i++) {
5899
6525
  const obj = objects[i];
5900
6526
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -5930,7 +6556,8 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
5930
6556
  var FIELD_GROUP_EMPTY = "field-group-empty";
5931
6557
  var FIELD_GROUP_SHADOWED = "field-group-shadowed";
5932
6558
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
5933
- function asArray33(v) {
6559
+ var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
6560
+ function asArray34(v) {
5934
6561
  if (Array.isArray(v)) return v;
5935
6562
  if (v && typeof v === "object") {
5936
6563
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5939,7 +6566,7 @@ function asArray33(v) {
5939
6566
  }
5940
6567
  function validateSemanticRoles(stack) {
5941
6568
  const findings = [];
5942
- const objects = asArray33(stack.objects);
6569
+ const objects = asArray34(stack.objects);
5943
6570
  for (let i = 0; i < objects.length; i++) {
5944
6571
  const obj = objects[i];
5945
6572
  if (!obj || typeof obj !== "object") continue;
@@ -5948,6 +6575,15 @@ function validateSemanticRoles(stack) {
5948
6575
  const path = `objects[${i}]`;
5949
6576
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
5950
6577
  const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
6578
+ const unprovisioned = unprovisionedInjectedColumnsFor(obj);
6579
+ const unprovisionedPointer = (slot, entry) => ({
6580
+ severity: "warning",
6581
+ rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
6582
+ where,
6583
+ path: `${path}.${slot}`,
6584
+ message: `${objName}: ${slot} points at "${entry}", an injected system column with no storage behind it \u2014 this object is external (ADR-0015), so the platform registers the anchor but the remote schema owns the table and no column backs it. Every consumer renders it empty on every record.`,
6585
+ hint: `If the remote table really carries "${entry}", declare it in the object's own fields (mapped through the external binding's columnMap); otherwise point ${slot} at a real remote column.`
6586
+ });
5951
6587
  const declaredGroups = new Set(
5952
6588
  (Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
5953
6589
  );
@@ -5989,10 +6625,16 @@ function validateSemanticRoles(stack) {
5989
6625
  message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
5990
6626
  hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
5991
6627
  });
6628
+ } else if (typeof stage === "string" && unprovisioned.has(stage)) {
6629
+ findings.push(unprovisionedPointer("stageField", stage));
5992
6630
  }
5993
6631
  const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
5994
6632
  for (const entry of highlights) {
5995
- if (typeof entry !== "string" || entry.length === 0 || fieldNames.has(entry)) continue;
6633
+ if (typeof entry !== "string" || entry.length === 0) continue;
6634
+ if (fieldNames.has(entry)) {
6635
+ if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
6636
+ continue;
6637
+ }
5996
6638
  findings.push({
5997
6639
  severity: "warning",
5998
6640
  rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -6006,7 +6648,7 @@ function validateSemanticRoles(stack) {
6006
6648
  (h) => typeof h === "string" && h.length > 0
6007
6649
  );
6008
6650
  if (declaredStrings.length > 0 && declaredGroups.size > 0) {
6009
- const declaredTitle = [obj.nameField, obj.primaryField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6651
+ const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6010
6652
  const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
6011
6653
  const stripSet = new Set(
6012
6654
  declaredStrings.filter((h) => h !== titleField).slice(0, 4)
@@ -6034,13 +6676,19 @@ function validateSemanticRoles(stack) {
6034
6676
  // src/validate-form-layout.ts
6035
6677
  var FORM_FIELD_UNKNOWN = "form-field-unknown";
6036
6678
  var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
6037
- function asArray34(v) {
6679
+ function asArray35(v) {
6038
6680
  if (Array.isArray(v)) return v;
6039
6681
  if (v && typeof v === "object") {
6040
6682
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6041
6683
  }
6042
6684
  return [];
6043
6685
  }
6686
+ function isRec20(v) {
6687
+ return !!v && typeof v === "object" && !Array.isArray(v);
6688
+ }
6689
+ function strName19(v) {
6690
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6691
+ }
6044
6692
  function fieldNameOf(entry) {
6045
6693
  if (typeof entry === "string") return entry.length > 0 ? entry : null;
6046
6694
  if (entry && typeof entry === "object" && !Array.isArray(entry)) {
@@ -6049,60 +6697,53 @@ function fieldNameOf(entry) {
6049
6697
  }
6050
6698
  return null;
6051
6699
  }
6052
- function boundObject(view) {
6053
- const data = view.data;
6054
- if (data && typeof data === "object" && typeof data.object === "string") {
6055
- return data.object;
6056
- }
6057
- return typeof view.objectName === "string" ? view.objectName : void 0;
6058
- }
6059
6700
  function validateFormLayout(stack) {
6060
6701
  const findings = [];
6061
6702
  const objectFields = /* @__PURE__ */ new Map();
6062
- for (const obj of asArray34(stack.objects)) {
6703
+ for (const obj of asArray35(stack.objects)) {
6063
6704
  const name = typeof obj.name === "string" ? obj.name : void 0;
6064
6705
  if (!name) continue;
6065
6706
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
6066
6707
  objectFields.set(name, new Set(fields));
6067
6708
  }
6068
- const views = asArray34(stack.views);
6069
- for (let i = 0; i < views.length; i++) {
6070
- const view = views[i];
6071
- if (!view || typeof view !== "object") continue;
6072
- const sections = Array.isArray(view.sections) ? view.sections : null;
6073
- if (!sections) continue;
6074
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6075
- const objName = boundObject(view);
6076
- const known = objName ? objectFields.get(objName) : void 0;
6077
- const where = `view "${viewName}"`;
6078
- const base = `views[${i}]`;
6079
- for (let s = 0; s < sections.length; s++) {
6080
- const sec = sections[s];
6081
- const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
6082
- for (let f = 0; f < secFields.length; f++) {
6083
- const entry = secFields[f];
6084
- const fname = fieldNameOf(entry);
6085
- const fpath = `${base}.sections[${s}].fields[${f}]`;
6086
- if (fname && known && !known.has(fname)) {
6087
- findings.push({
6088
- severity: "warning",
6089
- rule: FORM_FIELD_UNKNOWN,
6090
- where,
6091
- path: fpath,
6092
- message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6093
- hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6094
- });
6095
- }
6096
- const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
6097
- if (colSpan != null) {
6098
- findings.push({
6099
- severity: "warning",
6100
- rule: FORM_COLSPAN_ABSOLUTE,
6101
- where,
6102
- path: `${fpath}.colSpan`,
6103
- message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
6104
- hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6105
- });
6709
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6710
+ const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
6711
+ const containerObject = viewObjectName(view);
6712
+ for (const site of formViewSites(view, viewPath)) {
6713
+ const objName = viewObjectName(site.view) ?? containerObject;
6714
+ const known = objName ? objectFields.get(objName) : void 0;
6715
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
6716
+ for (const bucket of ["sections", "groups"]) {
6717
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6718
+ for (let s = 0; s < sections.length; s++) {
6719
+ const sec = sections[s];
6720
+ const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6721
+ for (let f = 0; f < secFields.length; f++) {
6722
+ const entry = secFields[f];
6723
+ const fname = fieldNameOf(entry);
6724
+ const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
6725
+ if (fname && known && !known.has(fname)) {
6726
+ findings.push({
6727
+ severity: "warning",
6728
+ rule: FORM_FIELD_UNKNOWN,
6729
+ where,
6730
+ path: fpath,
6731
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6732
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6733
+ });
6734
+ }
6735
+ const colSpan = isRec20(entry) ? entry.colSpan : void 0;
6736
+ if (colSpan != null) {
6737
+ findings.push({
6738
+ severity: "warning",
6739
+ rule: FORM_COLSPAN_ABSOLUTE,
6740
+ where,
6741
+ path: `${fpath}.colSpan`,
6742
+ message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
6743
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6744
+ });
6745
+ }
6746
+ }
6106
6747
  }
6107
6748
  }
6108
6749
  }
@@ -6201,17 +6842,75 @@ function validateSeedStateMachine(stack) {
6201
6842
  }
6202
6843
 
6203
6844
  // src/validate-visibility-predicates.ts
6204
- var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
6845
+ import {
6846
+ collectCelRootIdentifiers as collectCelRootIdentifiers3,
6847
+ firstUndeclaredReference,
6848
+ parseCelToAst as parseCelToAst3,
6849
+ parseCelToAstWithReason
6850
+ } from "@objectstack/formula";
6851
+
6852
+ // src/predicate-rhs-position.ts
6853
+ function isNode2(v) {
6854
+ return !!v && typeof v === "object" && typeof v.op === "string";
6855
+ }
6856
+ var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
6857
+ var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
6858
+ function bareId(node) {
6859
+ if (!isNode2(node)) return null;
6860
+ return node.op === "id" && typeof node.args === "string" ? node.args : null;
6861
+ }
6862
+ function bareRhsOnlyIdentifiers(ast) {
6863
+ const rhs = /* @__PURE__ */ new Set();
6864
+ const elsewhere = /* @__PURE__ */ new Set();
6865
+ const walk = (node, suppressible) => {
6866
+ if (Array.isArray(node)) {
6867
+ for (const child of node) walk(child, suppressible);
6868
+ return;
6869
+ }
6870
+ if (!isNode2(node)) return;
6871
+ const args = node.args;
6872
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
6873
+ walk(args[1], suppressible);
6874
+ walk(args[2], false);
6875
+ return;
6876
+ }
6877
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
6878
+ const right = suppressible ? bareId(args[1]) : null;
6879
+ walk(args[0], suppressible);
6880
+ if (right !== null) {
6881
+ rhs.add(right);
6882
+ return;
6883
+ }
6884
+ walk(args[1], suppressible);
6885
+ return;
6886
+ }
6887
+ const name = bareId(node);
6888
+ if (name !== null) {
6889
+ elsewhere.add(name);
6890
+ return;
6891
+ }
6892
+ walk(args, suppressible);
6893
+ };
6894
+ walk(ast, true);
6895
+ for (const name of elsewhere) rhs.delete(name);
6896
+ return rhs;
6897
+ }
6898
+ function isRec21(v) {
6899
+ return !!v && typeof v === "object" && !Array.isArray(v);
6900
+ }
6901
+ function schemaIdOf(view) {
6902
+ const data = view.data;
6903
+ if (!isRec21(data)) return void 0;
6904
+ if (data.provider !== "schema") return void 0;
6905
+ return typeof data.schemaId === "string" ? data.schemaId : void 0;
6906
+ }
6907
+
6908
+ // src/validate-visibility-predicates.ts
6205
6909
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
6910
+ var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
6911
+ var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
6912
+ var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
6206
6913
  var CANONICAL = "visibleWhen";
6207
- var ALIASES = ["visibleOn", "visibility"];
6208
- function asArray35(v) {
6209
- if (Array.isArray(v)) return v;
6210
- if (v && typeof v === "object") {
6211
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6212
- }
6213
- return [];
6214
- }
6215
6914
  function predicateSource(v) {
6216
6915
  if (typeof v === "string") return v;
6217
6916
  if (v && typeof v === "object" && typeof v.source === "string") {
@@ -6222,31 +6921,85 @@ function predicateSource(v) {
6222
6921
  function usesRoot(source, root) {
6223
6922
  return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
6224
6923
  }
6225
- var MISLAYER_BY_LAYER = {
6924
+ function withoutStringLiterals(source) {
6925
+ return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
6926
+ }
6927
+ var NON_CEL_SPELLINGS = [
6928
+ { wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
6929
+ { wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
6930
+ { wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
6931
+ { wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
6932
+ { wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
6933
+ { wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
6934
+ // Assignment where a comparison was meant. Last, and fenced off from every
6935
+ // operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
6936
+ { wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
6937
+ ];
6938
+ function quoteSource(source) {
6939
+ const flat = source.replace(/\s+/g, " ").trim();
6940
+ return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
6941
+ }
6942
+ function celRefusal(source) {
6943
+ if (!source.trim()) return null;
6944
+ const parsed = parseCelToAstWithReason(source);
6945
+ if (parsed.ok || parsed.kind === "empty") return null;
6946
+ if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
6947
+ const identifiers = collectCelRootIdentifiers3(source);
6948
+ const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
6949
+ const scannable = withoutStringLiterals(source);
6950
+ return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
6951
+ }
6952
+ function boundName(overrun) {
6953
+ return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
6954
+ }
6955
+ var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
6956
+ function isNode3(v) {
6957
+ return !!v && typeof v === "object" && typeof v.op === "string";
6958
+ }
6959
+ function namespaceRoots(node, out) {
6960
+ if (Array.isArray(node)) {
6961
+ for (const child of node) namespaceRoots(child, out);
6962
+ return;
6963
+ }
6964
+ if (!isNode3(node)) return;
6965
+ const args = node.args;
6966
+ if (Array.isArray(args)) {
6967
+ const receiver = node.op === "rcall" ? args[1] : args[0];
6968
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6969
+ out.add(receiver.args);
6970
+ }
6971
+ }
6972
+ namespaceRoots(args, out);
6973
+ }
6974
+ function firstBareIdentifier(source, literalRhs) {
6975
+ const ast = parseCelToAst3(source);
6976
+ if (!ast) return null;
6977
+ const rooted = /* @__PURE__ */ new Set();
6978
+ namespaceRoots(ast, rooted);
6979
+ const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
6980
+ return firstUndeclaredReference(source, [
6981
+ ...VIEW_PAGE_EXTRA_ROOTS,
6982
+ ...rooted,
6983
+ ...literalSlot
6984
+ ]);
6985
+ }
6986
+ var CANONICAL_ROOT_BY_LAYER = {
6987
+ runtime: "record",
6988
+ metadata: "data"
6989
+ };
6990
+ var MISLAYER_BY_LAYER = {
6226
6991
  runtime: {
6227
6992
  forbiddenRoot: "data",
6228
- message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing-form root (a `*.form.ts` row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
6993
+ message: "visibility predicate is rooted at `data.` \u2014 that is the root a metadata-editing form binds (the row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
6229
6994
  hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
6230
6995
  },
6231
6996
  metadata: {
6232
6997
  forbiddenRoot: "record",
6233
- message: "visibility predicate is rooted at `record.` \u2014 that is the runtime record-surface root (a `*.view.ts` / `*.page.ts` live record), not a metadata-editing form. A `*.form.ts` predicate that binds `record.` never matches and the element renders unconditionally (ADR-0089).",
6998
+ message: "visibility predicate is rooted at `record.` \u2014 that is the root a runtime view/page surface binds (the live record), not the root a metadata-editing form binds. On a metadata-editing form \u2014 the row under edit \u2014 a `record.`-rooted predicate never matches and the element renders unconditionally (ADR-0089).",
6234
6999
  hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
6235
7000
  }
6236
7001
  };
6237
- function checkElement(el, where, path, layer, findings) {
6238
- for (const alias of ALIASES) {
6239
- if (el[alias] !== void 0) {
6240
- findings.push({
6241
- severity: "warning",
6242
- rule: VISIBILITY_ALIAS_DEPRECATED,
6243
- where,
6244
- path: `${path}.${alias}`,
6245
- message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
6246
- hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
6247
- });
6248
- }
6249
- }
7002
+ function checkElement(el, where, path, layer, findings, literalRhs = false) {
6250
7003
  const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
6251
7004
  const source = predicateSource(raw);
6252
7005
  const rule = MISLAYER_BY_LAYER[layer];
@@ -6260,50 +7013,380 @@ function checkElement(el, where, path, layer, findings) {
6260
7013
  hint: rule.hint
6261
7014
  });
6262
7015
  }
7016
+ const refusal = source ? celRefusal(source) : null;
7017
+ if (source && refusal?.kind === "bounds") {
7018
+ const bound = boundName(refusal.overrun);
7019
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
7020
+ findings.push({
7021
+ severity: "error",
7022
+ rule: VISIBILITY_PREDICATE_OVER_BUDGET,
7023
+ where,
7024
+ path,
7025
+ message: `visibility predicate is syntactically valid CEL but overruns ${bound} (${refusal.overrun.summary}) (predicate: \`${quoteSource(source)}\`). The canonical front end refuses it, so it can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
7026
+ hint: `There is no syntax or dialect error to correct here \u2014 this is a SIZE fault, not a dialect mistake, so re-spelling the predicate will not fix it. Make it smaller, or move the work off the predicate: (1) collapse a long \`${root}.f == 'a' || ${root}.f == 'b' || \u2026\` chain into a single \`${root}.f in ['a', 'b', \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2); (2) precompute the heavy part into a formula/rollup field on the object and test that one field instead. Logic genuinely this large is not element visibility \u2014 compute it once on the record rather than re-deriving it in every predicate that needs it.`
7027
+ });
7028
+ }
7029
+ if (source && refusal?.kind === "syntax") {
7030
+ findings.push({
7031
+ severity: "error",
7032
+ rule: VISIBILITY_PREDICATE_SYNTAX,
7033
+ where,
7034
+ path,
7035
+ message: `visibility predicate is not valid CEL \u2014 ${refusal.detail} (predicate: \`${quoteSource(source)}\`). A predicate that does not parse can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
7036
+ hint: refusal.token ? `\`${refusal.token.wrote}\` is not a CEL operator \u2014 CEL spells it \`${refusal.token.cel}\`. Replace \`${refusal.token.wrote}\` with \`${refusal.token.cel}\`, e.g. \`${refusal.token.example}\`.` : `Visibility predicates are bare CEL, e.g. \`record.status == 'open'\`. Spellings from other languages do not parse: write \`==\` (not \`===\`), \`!=\` (not \`!==\` or \`<>\`), \`&&\` (not \`and\`), \`||\` (not \`or\`), \`!\` (not \`not\`).`
7037
+ });
7038
+ }
7039
+ if (source && !refusal) {
7040
+ const bare = firstBareIdentifier(source, literalRhs);
7041
+ if (bare) {
7042
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
7043
+ findings.push({
7044
+ severity: "error",
7045
+ rule: VISIBILITY_BARE_IDENTIFIER,
7046
+ where,
7047
+ path,
7048
+ message: `visibility predicate references \`${bare}\` as a bare identifier. Values are bound under a namespace on this surface \u2014 they are never flattened to top level \u2014 so \`${bare}\` resolves to nothing, the predicate can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
7049
+ hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a metadata-editing form binds the row under edit as `data`).")
7050
+ });
7051
+ }
7052
+ }
6263
7053
  }
6264
7054
  function isFieldObject(entry) {
6265
7055
  return !!entry && typeof entry === "object" && !Array.isArray(entry);
6266
7056
  }
6267
7057
  function validateVisibilityPredicates(stack, opts = {}) {
6268
- const layer = opts.layer ?? "runtime";
7058
+ const declaredLayer = opts.layer ?? "runtime";
6269
7059
  const findings = [];
6270
- const views = asArray35(stack.views);
6271
- for (let i = 0; i < views.length; i++) {
6272
- const view = views[i];
6273
- if (!view || typeof view !== "object") continue;
6274
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6275
- const where = `view "${viewName}"`;
6276
- for (const bucket of ["sections", "groups"]) {
6277
- const sections = Array.isArray(view[bucket]) ? view[bucket] : [];
6278
- for (let s = 0; s < sections.length; s++) {
6279
- const sec = sections[s];
6280
- if (!sec || typeof sec !== "object") continue;
6281
- const secPath = `views[${i}].${bucket}[${s}]`;
6282
- checkElement(sec, where, secPath, layer, findings);
6283
- const secFields = Array.isArray(sec.fields) ? sec.fields : [];
6284
- for (let f = 0; f < secFields.length; f++) {
6285
- const entry = secFields[f];
6286
- if (isFieldObject(entry)) {
6287
- checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
7060
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
7061
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
7062
+ for (const site of formViewSites(view, viewPath)) {
7063
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
7064
+ const schemaBound = schemaIdOf(site.view) !== void 0;
7065
+ const literalRhs = schemaBound;
7066
+ const layer = schemaBound ? "metadata" : declaredLayer;
7067
+ for (const bucket of ["sections", "groups"]) {
7068
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7069
+ for (let s = 0; s < sections.length; s++) {
7070
+ const sec = sections[s];
7071
+ if (!sec || typeof sec !== "object") continue;
7072
+ const secPath = `${site.path}.${bucket}[${s}]`;
7073
+ checkElement(sec, where, secPath, layer, findings, literalRhs);
7074
+ const secFields = Array.isArray(sec.fields) ? sec.fields : [];
7075
+ for (let f = 0; f < secFields.length; f++) {
7076
+ const entry = secFields[f];
7077
+ if (isFieldObject(entry)) {
7078
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
7079
+ }
6288
7080
  }
6289
7081
  }
6290
7082
  }
6291
7083
  }
6292
7084
  }
6293
- const pages = asArray35(stack.pages);
6294
- for (let i = 0; i < pages.length; i++) {
6295
- const page = pages[i];
6296
- if (!page || typeof page !== "object") continue;
6297
- const pageName = typeof page.name === "string" ? page.name : `(page ${i})`;
6298
- const where = `page "${pageName}"`;
6299
- const regions = Array.isArray(page.regions) ? page.regions : [];
6300
- for (let r = 0; r < regions.length; r++) {
6301
- const region = regions[r];
6302
- const components = region && typeof region === "object" && Array.isArray(region.components) ? region.components : [];
6303
- for (let c = 0; c < components.length; c++) {
6304
- const comp = components[c];
6305
- if (comp && typeof comp === "object") {
6306
- checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
7085
+ for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
7086
+ const pageName = typeof page.name === "string" ? page.name : void 0;
7087
+ const where = `page "${pageName ?? pagePath}"`;
7088
+ for (const walked of walkPageComponents(page, pagePath)) {
7089
+ checkElement(walked.component, where, walked.path, declaredLayer, findings);
7090
+ }
7091
+ }
7092
+ return findings;
7093
+ }
7094
+
7095
+ // src/validate-predicate-path-refs.ts
7096
+ import { parseCelToAst as parseCelToAst4 } from "@objectstack/formula";
7097
+ import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
7098
+ import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec";
7099
+ var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
7100
+ var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
7101
+ var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
7102
+ var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
7103
+ var ROOT = "data";
7104
+ function defOf(schema) {
7105
+ if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
7106
+ const s = schema;
7107
+ return s.def ?? s._def;
7108
+ }
7109
+ function peel(schema, depth = 0) {
7110
+ if (!schema || depth > 25) return schema;
7111
+ const d = defOf(schema);
7112
+ if (!d) return schema;
7113
+ switch (d.type) {
7114
+ case "optional":
7115
+ case "nullable":
7116
+ case "default":
7117
+ case "prefault":
7118
+ case "readonly":
7119
+ case "catch":
7120
+ case "nonoptional":
7121
+ return peel(d.innerType, depth + 1);
7122
+ case "lazy":
7123
+ return peel(d.getter(), depth + 1);
7124
+ case "pipe": {
7125
+ const inner = peel(d.in, depth + 1);
7126
+ return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
7127
+ }
7128
+ default:
7129
+ return schema;
7130
+ }
7131
+ }
7132
+ function optionsOf(d) {
7133
+ return Array.isArray(d?.options) ? d.options : [];
7134
+ }
7135
+ function keysOf(schema, depth = 0) {
7136
+ if (depth > 25) return null;
7137
+ const u = peel(schema);
7138
+ const d = defOf(u);
7139
+ if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
7140
+ if (d?.type === "union" || d?.type === "discriminated_union") {
7141
+ const all = /* @__PURE__ */ new Set();
7142
+ let keyBearing = false;
7143
+ for (const option of optionsOf(d)) {
7144
+ const k = keysOf(option, depth + 1);
7145
+ if (!k) continue;
7146
+ keyBearing = true;
7147
+ for (const key of k) all.add(key);
7148
+ }
7149
+ return keyBearing ? [...all] : null;
7150
+ }
7151
+ if (d?.type === "intersection") {
7152
+ const left = keysOf(d.left, depth + 1);
7153
+ const right = keysOf(d.right, depth + 1);
7154
+ if (!left && !right) return null;
7155
+ return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
7156
+ }
7157
+ return null;
7158
+ }
7159
+ function propertyOf(schema, key, depth = 0) {
7160
+ if (depth > 25) return void 0;
7161
+ const u = peel(schema);
7162
+ const d = defOf(u);
7163
+ if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
7164
+ if (d?.type === "union" || d?.type === "discriminated_union") {
7165
+ for (const option of optionsOf(d)) {
7166
+ const found = propertyOf(option, key, depth + 1);
7167
+ if (found !== void 0) return found;
7168
+ }
7169
+ }
7170
+ if (d?.type === "intersection") {
7171
+ return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
7172
+ }
7173
+ return void 0;
7174
+ }
7175
+ function rowScopeOf(scope, key) {
7176
+ const prop = propertyOf(scope, key);
7177
+ if (prop === void 0) return void 0;
7178
+ let node = peel(prop);
7179
+ for (let i = 0; i < 25; i++) {
7180
+ const d = defOf(node);
7181
+ if (d?.type === "array") node = peel(d.element);
7182
+ else if (d?.type === "record") node = peel(d.valueType);
7183
+ else return node;
7184
+ }
7185
+ return node;
7186
+ }
7187
+ function stepInto(scope, segment) {
7188
+ const u = peel(scope);
7189
+ const d = defOf(u);
7190
+ if (d?.type === "record") return { kind: "declared", next: d.valueType };
7191
+ const declared = keysOf(u);
7192
+ if (declared === null) return { kind: "opaque" };
7193
+ if (!declared.includes(segment)) return { kind: "undeclared", declared };
7194
+ return { kind: "declared", next: propertyOf(u, segment) };
7195
+ }
7196
+ function isNode4(v) {
7197
+ return !!v && typeof v === "object" && typeof v.op === "string";
7198
+ }
7199
+ function memberChain(node) {
7200
+ if (!isNode4(node)) return null;
7201
+ if (node.op === "id" && typeof node.args === "string") return [node.args];
7202
+ if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
7203
+ const head = memberChain(node.args[0]);
7204
+ return head ? [...head, node.args[1]] : null;
7205
+ }
7206
+ return null;
7207
+ }
7208
+ function rootedPaths(node, out) {
7209
+ if (Array.isArray(node)) {
7210
+ for (const child of node) rootedPaths(child, out);
7211
+ return;
7212
+ }
7213
+ if (!isNode4(node)) return;
7214
+ if (node.op === ".") {
7215
+ const chain = memberChain(node);
7216
+ if (chain && chain[0] === ROOT && chain.length > 1) {
7217
+ out.push(chain.slice(1));
7218
+ return;
7219
+ }
7220
+ }
7221
+ rootedPaths(node.args, out);
7222
+ }
7223
+ var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
7224
+ function equalitySites(node, out) {
7225
+ if (Array.isArray(node)) {
7226
+ for (const child of node) equalitySites(child, out);
7227
+ return;
7228
+ }
7229
+ if (!isNode4(node)) return;
7230
+ const args = node.args;
7231
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
7232
+ equalitySites(args[1], out);
7233
+ return;
7234
+ }
7235
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
7236
+ out.push({ op: node.op, right: args[1] });
7237
+ }
7238
+ equalitySites(args, out);
7239
+ }
7240
+ function classifyIdentifiers(node, values, excluded) {
7241
+ if (Array.isArray(node)) {
7242
+ for (const child of node) classifyIdentifiers(child, values, excluded);
7243
+ return;
7244
+ }
7245
+ if (!isNode4(node)) return;
7246
+ const args = node.args;
7247
+ if (Array.isArray(args)) {
7248
+ const receiver = node.op === "rcall" ? args[1] : args[0];
7249
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
7250
+ excluded.add(receiver.args);
7251
+ }
7252
+ if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
7253
+ const macroArgs = args[2];
7254
+ if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
7255
+ const bound = macroArgs[0];
7256
+ if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
7257
+ excluded.add(bound.args);
7258
+ }
7259
+ }
7260
+ }
7261
+ }
7262
+ if (node.op === "id" && typeof node.args === "string") {
7263
+ values.add(node.args);
7264
+ return;
7265
+ }
7266
+ classifyIdentifiers(args, values, excluded);
7267
+ }
7268
+ function predicateSource2(v) {
7269
+ if (typeof v === "string") return v;
7270
+ if (v && typeof v === "object" && typeof v.source === "string") {
7271
+ return v.source;
7272
+ }
7273
+ return void 0;
7274
+ }
7275
+ function isRec22(v) {
7276
+ return !!v && typeof v === "object" && !Array.isArray(v);
7277
+ }
7278
+ function checkPredicate(source, scope, where, path, findings) {
7279
+ const ast = parseCelToAst4(source);
7280
+ if (!ast) return;
7281
+ const paths = [];
7282
+ rootedPaths(ast, paths);
7283
+ for (const segments of paths) {
7284
+ let cursor = scope;
7285
+ const walked = [];
7286
+ for (const segment of segments) {
7287
+ const step = stepInto(cursor, segment);
7288
+ if (step.kind === "opaque") break;
7289
+ if (step.kind === "undeclared") {
7290
+ const full = [ROOT, ...walked, segment].join(".");
7291
+ const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
7292
+ findings.push({
7293
+ severity: "error",
7294
+ rule: PREDICATE_PATH_UNRESOLVED,
7295
+ where,
7296
+ path,
7297
+ message: `predicate references \`${full}\`, which the target schema does not declare \u2014 \`${segment}\` is not a key of \`${container}\`. The reference resolves to nothing, so the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149).`,
7298
+ hint: `${formatSuggestion4(findClosestMatches4(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
7299
+ });
7300
+ break;
7301
+ }
7302
+ walked.push(segment);
7303
+ cursor = step.next;
7304
+ }
7305
+ }
7306
+ const declaredHere = keysOf(scope);
7307
+ const rhsOnly = bareRhsOnlyIdentifiers(ast);
7308
+ if (declaredHere) {
7309
+ const values = /* @__PURE__ */ new Set();
7310
+ const excluded = /* @__PURE__ */ new Set();
7311
+ classifyIdentifiers(ast, values, excluded);
7312
+ for (const id of values) {
7313
+ if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
7314
+ findings.push({
7315
+ severity: "error",
7316
+ rule: PREDICATE_PATH_UNROOTED,
7317
+ where,
7318
+ path,
7319
+ message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
7320
+ hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
7321
+ });
7322
+ }
7323
+ }
7324
+ const sites = [];
7325
+ equalitySites(ast, sites);
7326
+ for (const { op, right } of sites) {
7327
+ const chain = memberChain(right);
7328
+ if (!chain) continue;
7329
+ const text = chain.join(".");
7330
+ if (!PATH_SHAPED_RHS.test(text)) continue;
7331
+ const dotted = chain.length > 1;
7332
+ findings.push({
7333
+ severity: dotted ? "error" : "warning",
7334
+ rule: PREDICATE_RHS_PATH_SHAPED,
7335
+ where,
7336
+ path,
7337
+ message: dotted ? `predicate compares against \`${text}\` on the RIGHT of \`${op}\`, which is a path but is not evaluated as one. A metadata-editing form resolves paths on the LEFT of \`${op}\` only; the right-hand side goes to the literal parser, so \`${text}\` is compared as the literal string "${text}". The verdict therefore does not depend on the right-hand path at all: \`a == ${text}\` is FALSE even when both sides hold the same value, and \`a != ${text}\` is correspondingly TRUE. An \`==\` written this way hides the element on every row, and nothing in the console says why (objectui#4049).` : `predicate compares against the unquoted word \`${text}\` on the RIGHT of \`${op}\`. The right-hand side of \`${op}\` is a literal, never a reference, so this is read as the literal string "${text}" \u2014 which is probably what you meant, and is why it appears to work. It is outside the declared subset all the same (\`path == 'literal'\`), and it stops working when this surface moves to the real CEL evaluator, where a bare \`${text}\` resolves to nothing (objectui#4049). The token also reads as a \`${ROOT}.\` root someone dropped, so this one finding carries BOTH readings: which one you meant is the thing no linter can know, and it changes the fix (#7696).`,
7338
+ hint: dotted ? `Two sanctioned spellings. (1) If you meant the TEXT, quote it: \`${op} '${text}'\`. (2) If you meant the PATH, restructure so the path is on the LEFT and a literal is on the right \u2014 comparing one path against another is outside the subset this surface renders, which is \`path == 'literal'\` / \`path != 'literal'\` and nothing wider. There is no third spelling that compares two paths here.` : `Two sanctioned spellings, and you must pick \u2014 they are not the same predicate. (1) If you meant the TEXT \`${text}\`, quote it: \`${op} '${text}'\`. That is what this renders as today, so it changes no behaviour and is the fix unless you know otherwise. (2) If you meant the FIELD \`${ROOT}.${text}\`, move it to the LEFT and put a literal on the right, e.g. \`${ROOT}.${text} == 'yes'\`. \u26D4 Do NOT simply add the root in place: \`${op} ${ROOT}.${text}\` is a path on the RIGHT, which this surface parses as the literal string "${ROOT}.${text}" \u2014 it is refused by this same rule at \`error\`, and it is FALSE on every row. The subset here is \`path == 'literal'\` and nothing wider.`
7339
+ });
7340
+ }
7341
+ }
7342
+ function walkFields(entries, scope, where, base, findings, depth) {
7343
+ if (!Array.isArray(entries) || depth > 12) return;
7344
+ for (let i = 0; i < entries.length; i++) {
7345
+ const entry = entries[i];
7346
+ if (!isRec22(entry)) continue;
7347
+ const path = `${base}[${i}]`;
7348
+ for (const key of PREDICATE_KEYS) {
7349
+ const source = predicateSource2(entry[key]);
7350
+ if (source !== void 0 && source.trim()) {
7351
+ checkPredicate(source, scope, where, `${path}.${key}`, findings);
7352
+ break;
7353
+ }
7354
+ }
7355
+ if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
7356
+ const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
7357
+ walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
7358
+ }
7359
+ }
7360
+ }
7361
+ function validatePredicatePathRefs(stack, opts = {}) {
7362
+ const resolveSchema = opts.resolveSchema ?? ((schemaId) => getMetadataTypeSchema(schemaId));
7363
+ const findings = [];
7364
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
7365
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
7366
+ for (const site of formViewSites(view, viewPath)) {
7367
+ const schemaId = schemaIdOf(site.view);
7368
+ if (!schemaId) continue;
7369
+ let root;
7370
+ try {
7371
+ root = resolveSchema(schemaId);
7372
+ } catch {
7373
+ root = void 0;
7374
+ }
7375
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
7376
+ for (const bucket of ["sections", "groups"]) {
7377
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7378
+ for (let s = 0; s < sections.length; s++) {
7379
+ const section = sections[s];
7380
+ if (!isRec22(section)) continue;
7381
+ const sectionPath = `${site.path}.${bucket}[${s}]`;
7382
+ for (const key of PREDICATE_KEYS) {
7383
+ const source = predicateSource2(section[key]);
7384
+ if (source !== void 0 && source.trim()) {
7385
+ checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
7386
+ break;
7387
+ }
7388
+ }
7389
+ walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
6307
7390
  }
6308
7391
  }
6309
7392
  }
@@ -6325,6 +7408,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
6325
7408
  var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
6326
7409
  var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
6327
7410
  var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
7411
+ var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
6328
7412
  var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
6329
7413
  var OWD_ALIAS_FIX = {
6330
7414
  read: "public_read",
@@ -6370,6 +7454,13 @@ function firstMasterDetailField(obj) {
6370
7454
  }
6371
7455
  return void 0;
6372
7456
  }
7457
+ function resolveCbpRelation(obj) {
7458
+ const entries = asArray36(obj.fields);
7459
+ const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
7460
+ const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
7461
+ if (!found) return void 0;
7462
+ return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
7463
+ }
6373
7464
  function grantsObjectAccess(p) {
6374
7465
  return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
6375
7466
  }
@@ -6415,6 +7506,16 @@ function validateSecurityPosture(stack, opts) {
6415
7506
  });
6416
7507
  }
6417
7508
  }
7509
+ if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
7510
+ findings.push({
7511
+ severity: "error",
7512
+ rule: SECURITY_CBP_NO_RELATION,
7513
+ where: `object "${objName}"`,
7514
+ path: `${objPath}.sharingModel`,
7515
+ message: `"${objName}" declares sharingModel 'controlled_by_parent' but has no relation the platform can derive access from. ADR-0055 resolves the master through a required master_detail, then any master_detail, then a required lookup \u2014 each of which must also name a reference target \u2014 and this object matches none of the three. At runtime every read is DENIED and every write is refused with 422 INVALID_METADATA (#7474), so the object is unusable rather than merely locked down.`,
7516
+ hint: `Add the master relation this object is derived from, e.g. fields.parent: { type: 'master_detail', reference: '<master_object>', required: true }. If the object has no master, its baseline is its own decision \u2014 use sharingModel: 'private' (owner + shares), 'public_read', or 'public_read_write'.`
7517
+ });
7518
+ }
6418
7519
  if (typeof external === "string") {
6419
7520
  if (OWD_ALIAS_FIX[external]) {
6420
7521
  findings.push({
@@ -6480,53 +7581,6 @@ function validateSecurityPosture(stack, opts) {
6480
7581
  }
6481
7582
  }
6482
7583
  }
6483
- const flagRole = (kind, name, label2, where, path) => {
6484
- if (identifierHasRoleToken(name)) {
6485
- findings.push({
6486
- severity: "error",
6487
- rule: SECURITY_ROLE_WORD,
6488
- where,
6489
- path,
6490
- message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
6491
- hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
6492
- });
6493
- } else if (labelHasRoleWord(label2)) {
6494
- findings.push({
6495
- severity: "error",
6496
- rule: SECURITY_ROLE_WORD,
6497
- where,
6498
- path: `${path.replace(/\.name$/, "")}.label`,
6499
- message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
6500
- hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
6501
- });
6502
- }
6503
- };
6504
- for (let i = 0; i < objects.length; i++) {
6505
- const obj = objects[i];
6506
- if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
6507
- const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
6508
- flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
6509
- for (const f of asArray36(obj.fields)) {
6510
- flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
6511
- }
6512
- for (const [ai, action] of asArray36(obj.actions).entries()) {
6513
- flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
6514
- }
6515
- }
6516
- for (let i = 0; i < permissionSets.length; i++) {
6517
- const ps = permissionSets[i];
6518
- if (!ps || typeof ps !== "object") continue;
6519
- flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
6520
- }
6521
- for (const [i, pos] of asArray36(stack.positions).entries()) {
6522
- flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
6523
- }
6524
- for (const [i, app] of asArray36(stack.apps).entries()) {
6525
- flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
6526
- }
6527
- for (const [i, book] of asArray36(stack.books).entries()) {
6528
- flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
6529
- }
6530
7584
  const stackSetNames = new Set(
6531
7585
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
6532
7586
  );
@@ -6647,6 +7701,60 @@ function validateSecurityPosture(stack, opts) {
6647
7701
  }
6648
7702
  return findings;
6649
7703
  }
7704
+ function validateSecurityRoleWord(stack) {
7705
+ const findings = [];
7706
+ if (!stack || typeof stack !== "object") return findings;
7707
+ const objects = asArray36(stack.objects);
7708
+ const permissionSets = asArray36(stack.permissions);
7709
+ const flagRole = (kind, name, label2, where, path) => {
7710
+ if (identifierHasRoleToken(name)) {
7711
+ findings.push({
7712
+ severity: "error",
7713
+ rule: SECURITY_ROLE_WORD,
7714
+ where,
7715
+ path,
7716
+ message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
7717
+ hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
7718
+ });
7719
+ } else if (labelHasRoleWord(label2)) {
7720
+ findings.push({
7721
+ severity: "error",
7722
+ rule: SECURITY_ROLE_WORD,
7723
+ where,
7724
+ path: `${path.replace(/\.name$/, "")}.label`,
7725
+ message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
7726
+ hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
7727
+ });
7728
+ }
7729
+ };
7730
+ for (let i = 0; i < objects.length; i++) {
7731
+ const obj = objects[i];
7732
+ if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
7733
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
7734
+ flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
7735
+ for (const f of asArray36(obj.fields)) {
7736
+ flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
7737
+ }
7738
+ for (const [ai, action] of asArray36(obj.actions).entries()) {
7739
+ flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
7740
+ }
7741
+ }
7742
+ for (let i = 0; i < permissionSets.length; i++) {
7743
+ const ps = permissionSets[i];
7744
+ if (!ps || typeof ps !== "object") continue;
7745
+ flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
7746
+ }
7747
+ for (const [i, pos] of asArray36(stack.positions).entries()) {
7748
+ flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
7749
+ }
7750
+ for (const [i, app] of asArray36(stack.apps).entries()) {
7751
+ flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
7752
+ }
7753
+ for (const [i, book] of asArray36(stack.books).entries()) {
7754
+ flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
7755
+ }
7756
+ return findings;
7757
+ }
6650
7758
 
6651
7759
  // src/validate-org-axis-red-lines.ts
6652
7760
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
@@ -6805,9 +7913,15 @@ function validateSharingRuleEnforceability(stack) {
6805
7913
  }
6806
7914
 
6807
7915
  // src/validate-rls-predicate-enforceability.ts
6808
- import { isPushdownableCel, isSupportedRlsExpression, sqlPredicateToCel } from "@objectstack/formula";
7916
+ import {
7917
+ isPushdownableCel,
7918
+ isSupportedRlsExpression,
7919
+ parseCelToAstWithReason as parseCelToAstWithReason2,
7920
+ sqlPredicateToCel
7921
+ } from "@objectstack/formula";
6809
7922
  var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
6810
7923
  var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
7924
+ var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
6811
7925
  function asArray39(v) {
6812
7926
  if (Array.isArray(v)) return v;
6813
7927
  if (v && typeof v === "object") {
@@ -6819,6 +7933,13 @@ function str3(v) {
6819
7933
  return typeof v === "string" ? v : "";
6820
7934
  }
6821
7935
  var PUSHDOWN_SUBSET2 = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column field paths (ADR-0058 D2), compared against a literal or a `current_user.*` value.";
7936
+ function boundsOverrunOf(bridged) {
7937
+ const parsed = parseCelToAstWithReason2(bridged);
7938
+ return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
7939
+ }
7940
+ function quote(source) {
7941
+ return source.length > 200 ? `${source.slice(0, 197)}...` : source;
7942
+ }
6822
7943
  function consequence(clause) {
6823
7944
  const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
6824
7945
  return clause === "using" ? dropped + "When it is the only applicable policy for that object and operation, `compileFilter` returns the `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause: every select / update / delete on the object matches ZERO rows. When other policies also apply, this one just vanishes from the OR and grants none of the access it appears to." : dropped + "On the ADR-0058 D4 write path that leaves the post-image `check` as the `RLS_DENY_FILTER` sentinel, which no record can satisfy: every insert / update the policy governs fails with `PermissionDeniedError`. The policy reads as a write rule and behaves as a blanket refusal.";
@@ -6832,14 +7953,30 @@ function validateRlsPredicateEnforceability(stack) {
6832
7953
  const source = str3(policy[clause]);
6833
7954
  if (!source.trim()) continue;
6834
7955
  if (isSupportedRlsExpression(source)) continue;
6835
- const why = isPushdownableCel(sqlPredicateToCel(source));
7956
+ const bridged = sqlPredicateToCel(source);
7957
+ const why = isPushdownableCel(bridged);
6836
7958
  const detail = why.ok ? "" : why.detail;
6837
7959
  const parseError = !why.ok && why.reason === "parse-error";
7960
+ const overrun = parseError ? boundsOverrunOf(bridged) : null;
6838
7961
  const psName = str3(ps.name) || String(psIndex);
6839
7962
  const policyName = str3(policy.name) || String(pIndex);
6840
7963
  const object = str3(policy.object);
6841
7964
  const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
6842
7965
  const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
7966
+ if (overrun) {
7967
+ const bound = overrun.limit ?? "an unnamed platform CEL bound";
7968
+ const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
7969
+ const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
7970
+ findings.push({
7971
+ severity: "error",
7972
+ rule: RLS_PREDICATE_OVER_BUDGET,
7973
+ where,
7974
+ path,
7975
+ message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
7976
+ hint: `There is no syntax or dialect error to correct here \u2014 the predicate is well-formed CEL and is simply too large for ${bound}${budget}, so the fix is to make it smaller or to move the work off the predicate. (1) Collapse a long \`field == a || field == b || \u2026\` chain into a single \`field in [a, b, \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2). (2) Pre-resolve the set into a membership key the runtime exposes and test \`field in current_user.<key>\` (ADR-0105 D11) \u2014 one comparison whatever the set size. (3) Denormalise a repeated sub-expression onto this object as a formula/rollup field and test that single column. (4) Split a TOP-LEVEL \`||\` across several \`rowLevelSecurity\` policies: applicable policies are OR-ed, so that is equivalent \u2014 but never split a top-level \`&&\` this way, which would WIDEN access rather than preserve it. Logic genuinely this large is not a row filter: move it to a hook or action body (\`ScriptBody { language: 'js' }\`, the L2 sandboxed surface).`
7977
+ });
7978
+ continue;
7979
+ }
6843
7980
  if (parseError) {
6844
7981
  findings.push({
6845
7982
  severity: "error",
@@ -6870,11 +8007,11 @@ import { createRequire as createRequire4 } from "module";
6870
8007
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
6871
8008
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
6872
8009
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
6873
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8010
+ var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6874
8011
  function asArray40(v) {
6875
- if (Array.isArray(v)) return v.filter(isRec16);
6876
- if (isRec16(v)) {
6877
- return Object.entries(v).filter(([, def]) => isRec16(def)).map(([name, def]) => ({ name, ...def }));
8012
+ if (Array.isArray(v)) return v.filter(isRec23);
8013
+ if (isRec23(v)) {
8014
+ return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
6878
8015
  }
6879
8016
  return [];
6880
8017
  }
@@ -6891,7 +8028,7 @@ function loadAjv() {
6891
8028
  `@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.`
6892
8029
  );
6893
8030
  }
6894
- const ctor = isRec16(mod) && "default" in mod ? mod.default : mod;
8031
+ const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
6895
8032
  cachedAjv = ctor;
6896
8033
  return ctor;
6897
8034
  }
@@ -6906,7 +8043,7 @@ function loadAddFormats() {
6906
8043
  `@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.`
6907
8044
  );
6908
8045
  }
6909
- const plugin = isRec16(mod) && "default" in mod ? mod.default : mod;
8046
+ const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
6910
8047
  cachedAddFormats = plugin;
6911
8048
  return plugin;
6912
8049
  }
@@ -6936,13 +8073,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
6936
8073
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
6937
8074
  for (const branch of ["then", "otherwise"]) {
6938
8075
  const nested = rule[branch];
6939
- if (isRec16(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8076
+ if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6940
8077
  }
6941
8078
  return out;
6942
8079
  }
6943
8080
  function walkObjectValidationRules(stack) {
6944
8081
  const walked = [];
6945
- if (!isRec16(stack)) return walked;
8082
+ if (!isRec23(stack)) return walked;
6946
8083
  for (const obj of asArray40(stack.objects)) {
6947
8084
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6948
8085
  const validations = obj.validations;
@@ -6977,7 +8114,7 @@ function validateRuleCompilability(stack) {
6977
8114
  });
6978
8115
  }
6979
8116
  }
6980
- if (rule.type === "json_schema" && isRec16(rule.schema)) {
8117
+ if (rule.type === "json_schema" && isRec23(rule.schema)) {
6981
8118
  try {
6982
8119
  createRuntimeAjv().compile(rule.schema);
6983
8120
  } catch (err) {
@@ -6997,7 +8134,7 @@ function validateRuleCompilability(stack) {
6997
8134
 
6998
8135
  // src/validate-rule-schema-formats.ts
6999
8136
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7000
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8137
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7001
8138
  var SUBSCHEMA_KEYS = [
7002
8139
  "additionalItems",
7003
8140
  "additionalProperties",
@@ -7021,7 +8158,7 @@ var SUBSCHEMA_MAP_KEYS = [
7021
8158
  var MAX_SCHEMA_WALK_DEPTH = 32;
7022
8159
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7023
8160
  function collectFormatUses(schema, pointer, out, depth) {
7024
- if (!isRec17(schema)) return;
8161
+ if (!isRec24(schema)) return;
7025
8162
  if (typeof schema.format === "string") {
7026
8163
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7027
8164
  }
@@ -7040,7 +8177,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7040
8177
  }
7041
8178
  for (const key of SUBSCHEMA_MAP_KEYS) {
7042
8179
  const value = schema[key];
7043
- if (!isRec17(value)) continue;
8180
+ if (!isRec24(value)) continue;
7044
8181
  for (const [name, entry] of Object.entries(value)) {
7045
8182
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7046
8183
  }
@@ -7048,13 +8185,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7048
8185
  const items = schema.items;
7049
8186
  if (Array.isArray(items)) {
7050
8187
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7051
- } else if (isRec17(items)) {
8188
+ } else if (isRec24(items)) {
7052
8189
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7053
8190
  }
7054
8191
  const dependencies = schema.dependencies;
7055
- if (isRec17(dependencies)) {
8192
+ if (isRec24(dependencies)) {
7056
8193
  for (const [name, entry] of Object.entries(dependencies)) {
7057
- if (!isRec17(entry)) continue;
8194
+ if (!isRec24(entry)) continue;
7058
8195
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7059
8196
  }
7060
8197
  }
@@ -7093,7 +8230,7 @@ function validateRuleSchemaFormats(stack) {
7093
8230
  const findings = [];
7094
8231
  const pending = [];
7095
8232
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7096
- if (rule.type !== "json_schema" || !isRec17(rule.schema)) continue;
8233
+ if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
7097
8234
  const uses = [];
7098
8235
  collectFormatUses(rule.schema, "", uses, 0);
7099
8236
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7126,7 +8263,7 @@ function asArray41(v) {
7126
8263
  }
7127
8264
  return [];
7128
8265
  }
7129
- function strName17(v) {
8266
+ function strName20(v) {
7130
8267
  return typeof v === "string" && v.length > 0 ? v : void 0;
7131
8268
  }
7132
8269
  function strList3(v) {
@@ -7141,7 +8278,7 @@ function collectNamePlacedActions(stack) {
7141
8278
  for (const n of strList3(list3[key])) placed.add(n);
7142
8279
  }
7143
8280
  for (const def of asArray41(list3.bulkActionDefs)) {
7144
- const n = strName17(def?.name);
8281
+ const n = strName20(def?.name);
7145
8282
  if (n) placed.add(n);
7146
8283
  }
7147
8284
  };
@@ -7167,7 +8304,7 @@ function validateActionLocations(stack) {
7167
8304
  const check = (action, path) => {
7168
8305
  if (!action || typeof action !== "object") return;
7169
8306
  if ("locations" in action) return;
7170
- const name = strName17(action.name);
8307
+ const name = strName20(action.name);
7171
8308
  if (!name) return;
7172
8309
  if (namePlaced.has(name)) return;
7173
8310
  findings.push({
@@ -7176,7 +8313,7 @@ function validateActionLocations(stack) {
7176
8313
  where: `action "${name}"`,
7177
8314
  path,
7178
8315
  message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
7179
- hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`, `global_nav`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
8316
+ hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
7180
8317
  });
7181
8318
  };
7182
8319
  const actions = asArray41(stack.actions);
@@ -7197,6 +8334,7 @@ import {
7197
8334
  APPROVAL_REVISE_NODE_TYPE,
7198
8335
  collectFlowGraphs as collectFlowGraphs2
7199
8336
  } from "@objectstack/spec/automation";
8337
+ import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
7200
8338
  function asArray42(v) {
7201
8339
  if (Array.isArray(v)) return v;
7202
8340
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7471,7 +8609,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
7471
8609
  function filterCarriesNoCondition(filter) {
7472
8610
  if (filter === void 0 || filter === null) return true;
7473
8611
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
7474
- return Object.keys(filter).length === 0;
8612
+ return reduceFilterVerdict2(filter) === "true";
8613
+ }
8614
+ function describeUnboundedFilter(filter) {
8615
+ if (filter === void 0 || filter === null) return "no `filter` key";
8616
+ if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
8617
+ return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
8618
+ }
8619
+ function previewFilter(filter) {
8620
+ try {
8621
+ const json = JSON.stringify(filter);
8622
+ if (typeof json !== "string") return typeof filter;
8623
+ return json.length > 80 ? `${json.slice(0, 77)}...` : json;
8624
+ } catch {
8625
+ return typeof filter;
8626
+ }
7475
8627
  }
7476
8628
  function scanUnboundedBulkWrites(at, nodes, findings) {
7477
8629
  for (const node of nodes) {
@@ -7482,10 +8634,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
7482
8634
  if (cfg.multi !== true) continue;
7483
8635
  if (!filterCarriesNoCondition(cfg.filter)) continue;
7484
8636
  const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
7485
- const filterState = cfg.filter === void 0 || cfg.filter === null ? "no `filter` key" : "an EMPTY `filter`";
8637
+ const filterState = describeUnboundedFilter(cfg.filter);
7486
8638
  findings.push({
7487
8639
  where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
7488
- message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards \`where: {}\` plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` with no predicate. Nothing refuses it at run time, so the only feedback is the step's \`acted\` row count \u2014 reported AFTER the rows are gone.`,
8640
+ message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards the filter as \`where\` (an absent key becomes \`{}\`) plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` bounded by nothing \u2014 a filter that reduces to TRUE constrains no row. Nothing refuses it at run time, so the only feedback is the step's \`acted\` row count \u2014 reported AFTER the rows are gone.`,
7489
8641
  hint: `Write the constraint you mean into \`filter\` (e.g. \`{ status: 'closed' }\` \u2014 see examples/app-showcase \`showcase_inquiry_purge\`, bulk intent bounded by a predicate). If emptying '${objectName}' really is the intent, keep it: this is a warning, not a gate, and the run-time path stays open. Distinct from the #3810 erased-condition guard, which REFUSES this node at run time when a condition you WROTE interpolated to nothing \u2014 that guard is keyed on "a written condition is gone" and deliberately not on "the filter is empty", which is the fact this rule judges at authoring time. (#5482, #5393)`,
7490
8642
  // Warning, not `error`: see the severity policy at the top of this file.
7491
8643
  // The shape has a legitimate reading the engine grants on purpose, so it is
@@ -7786,6 +8938,13 @@ var TYPE_COLLECTIONS = [
7786
8938
  // checks every widget on the dashboard. Registering it here is not optional
7787
8939
  // bookkeeping: without it the ledger would be newly correct and newly
7788
8940
  // silent, which is the shape this lint exists to prevent.
8941
+ //
8942
+ // As of #6774 the dashboard ledger warns on NOTHING — four of those five were
8943
+ // retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
8944
+ // gave it a renderer. The type STAYS listed, the resolved state `webhook` and
8945
+ // `email_template` already sit in: a zero-warn entry costs one empty map
8946
+ // lookup, and it means a future regression that re-deadens a widget key warns
8947
+ // on its own instead of waiting for someone to notice this list again.
7789
8948
  { type: "dashboard", key: "dashboards" }
7790
8949
  ];
7791
8950
  function lintLivenessProperties(stack) {
@@ -8157,8 +9316,12 @@ var AUTHORING_RULES = [
8157
9316
  }))
8158
9317
  },
8159
9318
  // ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
8160
- // mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
8161
- // read the pre-parse tier or the evidence is already gone.
9319
+ // mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
9320
+ // is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
9321
+ // refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
9322
+ // THROWS on both. `normalized` here therefore means "needs no parsed stack"
9323
+ // (so `os lint`, which never parses, can run it), not "sees evidence the
9324
+ // parse would have eaten".
8162
9325
  {
8163
9326
  name: "validateListViewMode",
8164
9327
  tier: "gating",
@@ -8189,9 +9352,38 @@ var AUTHORING_RULES = [
8189
9352
  surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8190
9353
  run: (stack) => validateFunctionalCompleteness(stack)
8191
9354
  },
8192
- // A flat list-view object in `views: []` parses to an EMPTY container
8193
- // (ViewSchema strips unknown keys): the schema step passes, zero views
8194
- // register, and the Console renders nothing. Pre-parse for the same reason.
9355
+ // [#7521, via cloud#1225] A managed object advertising a generic write verb
9356
+ // in `enable.apiMethods` that its own resolved affordances refuse. Every key
9357
+ // is one we know and each is individually valid, so #4001's unknown-key
9358
+ // rejection and the Zod parse both pass it; the contradiction is only visible
9359
+ // when the two keys are read TOGETHER, which nothing did at authoring time.
9360
+ //
9361
+ // `gating` because the declaration is already false when it ships: objectql's
9362
+ // registry strips the verb at registration, so the metadata advertises an API
9363
+ // the product does not serve. That strip has been correct and silent — a
9364
+ // `console.warn` on every control-plane boot that went unread for the life of
9365
+ // a real divergence (`sys_environment`/`sys_package`). This entry is the
9366
+ // ruling's "close it where the author is"; boot stays warn-and-strip.
9367
+ //
9368
+ // Pre-parse: the predicate reads only authored keys, and the finding must
9369
+ // survive an unrelated schema error elsewhere in the stack.
9370
+ {
9371
+ name: "validateManagedApiMethods",
9372
+ tier: "gating",
9373
+ input: "normalized",
9374
+ commands: ALL,
9375
+ source: "packages/lint/src/validate-managed-api-methods.ts",
9376
+ surfaces: CLI_ONLY,
9377
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9378
+ run: (stack) => validateManagedApiMethods(stack)
9379
+ },
9380
+ // A view container in `views: []` that registers zero views: nothing appears
9381
+ // in the Console, and the schema step cannot tell it from an intentionally
9382
+ // empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
9383
+ // went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
9384
+ // data }` by name with the wrap-it hint (measured under #6073); the arm that
9385
+ // still needs a rule is the all-slots-empty container, whose keys are all
9386
+ // declared and which survives the parse untouched.
8195
9387
  {
8196
9388
  name: "validateViewContainers",
8197
9389
  tier: "gating",
@@ -8241,6 +9433,31 @@ var AUTHORING_RULES = [
8241
9433
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8242
9434
  run: (stack) => validateFilterTokens(stack)
8243
9435
  },
9436
+ // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
9437
+ // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
9438
+ // this rule does not touch it: it refuses the literal SPELLINGS at authoring
9439
+ // time with a per-shape prescription, which is Prime Directive #12's standard
9440
+ // shape (reject at the producer, never tolerate at the consumer) and #5240's
9441
+ // same-direction precedent one shape over.
9442
+ {
9443
+ name: "validateEmptyCombinators",
9444
+ tier: "gating",
9445
+ input: "parsed",
9446
+ commands: ALL,
9447
+ source: "packages/lint/src/validate-empty-combinators.ts",
9448
+ // The one type #4463's P1 slice opened, and the one this rule most needs:
9449
+ // a flow CRUD node's `config.filter` is where an empty combinator has the
9450
+ // largest blast radius, and the write path is the only door an AI author
9451
+ // uses. This rule needs NO resolution context at all — it judges the filter
9452
+ // literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
9453
+ // it, and widening to the other filter-carrying types (`object`, `view`,
9454
+ // `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
9455
+ // opens them at the gate. Making that call here would widen the gate's
9456
+ // dispatch surface on this rule's authority, which is P2's decision.
9457
+ surfaces: CLI_AND_RUNTIME,
9458
+ runtimeTypes: ["flow"],
9459
+ run: (stack) => validateEmptyCombinators(stack)
9460
+ },
8244
9461
  // The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
8245
9462
  // rules that answer "does this name resolve to anything?". It reached all
8246
9463
  // three commands before this file existed; it is an entry here so the two
@@ -8289,6 +9506,16 @@ var AUTHORING_RULES = [
8289
9506
  // `displayField` (#5775) — so gating today would fail the platform's own pages
8290
9507
  // to enforce declarations the platform does not keep. The error upgrade is a
8291
9508
  // separate step, once the warning-period inventory is empty.
9509
+ //
9510
+ // #5775 settled the record picker's half: `displayField` is retired in favour
9511
+ // of the `labelField` the renderer actually reads. Its claim that "the rest of
9512
+ // the keys the renderers honour are declared" did NOT hold — #6776 found five
9513
+ // more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
9514
+ // `page:accordion.variant`, and the tab strip's visual style, whose declared
9515
+ // spelling `page:tabs.type` collided with the component node's own dispatch
9516
+ // key and so was unauthorable in the flat and JSX carriers). All five are
9517
+ // declared as of #6776, the last as the renamed `tabStyle`. What remains
9518
+ // before the error upgrade is #5728 and two page rewrites.
8292
9519
  {
8293
9520
  name: "validateComponentProps",
8294
9521
  tier: "advisory",
@@ -8367,10 +9594,12 @@ var AUTHORING_RULES = [
8367
9594
  //
8368
9595
  // `gating` since #5762, which reviewed the file's rules as one family and
8369
9596
  // split them on a single question: is THIS STACK enough to know the flow is
8370
- // dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
9597
+ // dead? Four rules answer yes and emit `error` — a `config.timeRelative`
8371
9598
  // the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
8372
- // predicate cannot route at all, and a `record-*` triggerType outside the
8373
- // closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
9599
+ // predicate cannot route at all, a `record-*` triggerType outside the
9600
+ // closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
9601
+ // `type: 'record_change'` flow whose triggerType the engine's binding resolver
9602
+ // routes nowhere, silently demoting it to a manual flow. None of those verdicts
8374
9603
  // can be changed by installing a package, so there is no reading under which
8375
9604
  // the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
8376
9605
  // (the object may come from another installed package — a hedge this rule
@@ -8497,19 +9726,117 @@ var AUTHORING_RULES = [
8497
9726
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8498
9727
  run: (stack) => validateSeedStateMachine(stack)
8499
9728
  },
8500
- // ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
8501
- // Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
8502
- // during parse, so the alias the author wrote is gone from `result.data`.
9729
+ // ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
9730
+ // gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
9731
+ // schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
9732
+ // the alias the author wrote is gone from `result.data`". Measured false at
9733
+ // #6073: the ADR-0087 D2 conversions do that fold INSIDE
9734
+ // `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
9735
+ // alias site the alias-KEY rule reported zero here too.
9736
+ //
9737
+ // #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
9738
+ // re-anchored. Re-anchoring would have had to move this entry's input to a
9739
+ // pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
9740
+ // a change to this package's external input contract, and the maintainer's
9741
+ // call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
9742
+ // costs no author a signal: the same D2 conversion already shouts through
9743
+ // `warnConversionNotice` in `defineStack`, naming the site, the conversion and
9744
+ // the protocol-16 retirement window — better wording than the rule ever had.
9745
+ //
9746
+ // Every rule left in the family judges the predicate's VALUE, and the value
9747
+ // moves into `visibleWhen` intact, so all three report normally on this tier.
9748
+ // The tier therefore stays `normalized` on its SURVIVING justification (a
9749
+ // finding still reaches the author when an unrelated schema error would stop
9750
+ // the parse — see `AuthoringRuleInputTier`), never on the retired
9751
+ // "pre-parse evidence" one.
9752
+ //
9753
+ // `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
9754
+ // ADR-0089 rules stay advisory findings within it — the tier is a property of
9755
+ // the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
9756
+ // what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
9757
+ // has worked since #3760. The promotion follows the #5762 precedent: a family
9758
+ // that gains an `error` finding moves its registry tier in the same edit.
9759
+ //
9760
+ // ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
9761
+ //
9762
+ // This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
9763
+ // in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
9764
+ // `advisories` channel landed (PR #7435). Before that move a `view` written
9765
+ // through Studio / REST `/meta` / MCP — the only door most tenants have, and
9766
+ // the door AI authors use — was judged by NONE of the family's rule ids (six
9767
+ // at the time of the move; seven since #7659 added
9768
+ // `predicate-rhs-path-shaped` inside the second entry).
9769
+ //
9770
+ // They move together on purpose, and the two entries carry one comment because
9771
+ // they are one wall: #7214's implementer wired its own rule here alone and then
9772
+ // REVERTED it, because a `view` refused for an unresolvable predicate PATH
9773
+ // while a predicate that does not parse at all walks through the same door is
9774
+ // less predictable than refusing neither. A half-wired wall is worse than an
9775
+ // unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
9776
+ // directly: every id on this surface is gated at the runtime door, or none is.
9777
+ //
9778
+ // The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
9779
+ // and re-measuring it at move time found it false: both rule functions read
9780
+ // `stack.views` and `stack.pages` and NO other collection — never `objects` —
9781
+ // so the per-write snapshot the gate builds is not partial for them, it is
9782
+ // complete. (`pages` is simply absent on a `view` write, so the page half
9783
+ // contributes zero findings to both differential passes rather than inventing
9784
+ // any.) The reason was not describing this rule; it was the default a rule got
9785
+ // when nobody measured, which is the #4409/#4463 defect one layer in.
9786
+ //
9787
+ // Runtime input tier: the gate hands the rules the body as persisted, without
9788
+ // `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
9789
+ // door. That costs the family nothing — `validateVisibilityPredicates` reads
9790
+ // `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
9791
+ // so a caller handing it a raw authored object still gets a verdict.
8503
9792
  {
8504
9793
  name: "validateVisibilityPredicates",
8505
- tier: "advisory",
9794
+ tier: "gating",
8506
9795
  input: "normalized",
8507
9796
  commands: ALL,
8508
9797
  source: "packages/lint/src/validate-visibility-predicates.ts",
8509
- surfaces: CLI_ONLY,
8510
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9798
+ surfaces: CLI_AND_RUNTIME,
9799
+ runtimeTypes: ["view"],
8511
9800
  run: (stack) => validateVisibilityPredicates(stack)
8512
9801
  },
9802
+ // #7010 — the same predicate surface, one question further in. The three
9803
+ // ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
9804
+ // rooted, is the root right for the layer) and never open the target schema,
9805
+ // so `data.tpye == 'formula'` passes all three and still resolves to nothing.
9806
+ // This rule resolves the PATH against the schema the form edits — the closed
9807
+ // `getMetadataTypeSchema` key set — and is therefore immune to the CEL
9808
+ // type-name blind spot that made #6248's gate structurally unable to catch
9809
+ // #6254's 16 bare `type ==` predicates.
9810
+ //
9811
+ // Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
9812
+ // the `record.*` layer is deliberately out of scope because an ObjectQL
9813
+ // object's addressable path set is NOT closed (lookup traversal, system
9814
+ // columns, formula outputs), and an `error` gate over an open set generates
9815
+ // false build errors. See the rule's module note.
9816
+ //
9817
+ // #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
9818
+ // resolution question at all: the metadata-admin renderer resolves paths only
9819
+ // on the LEFT of `==` / `!=` and hands the right side to its literal parser,
9820
+ // so `data.a == data.b` resolves both sides cleanly, passes the two rules
9821
+ // above, and still compares against the string "data.b" — a constant verdict.
9822
+ // It carries `error` on a dotted chain (no reading under which it worked) and
9823
+ // `warning` on a bare word (`status == active` compares as the text today, so
9824
+ // refusing it would fail a build over metadata that renders correctly). The
9825
+ // per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
9826
+ // since #3760; the entry's `gating` tier is unchanged because it already was.
9827
+ {
9828
+ name: "validatePredicatePathRefs",
9829
+ tier: "gating",
9830
+ input: "normalized",
9831
+ commands: ALL,
9832
+ source: "packages/lint/src/validate-predicate-path-refs.ts",
9833
+ // The second half of the #7220 family move — see the block above the
9834
+ // `validateVisibilityPredicates` entry. This is the rule whose solo wiring
9835
+ // was reverted; it is wired now because its siblings are.
9836
+ surfaces: CLI_AND_RUNTIME,
9837
+ runtimeTypes: ["view"],
9838
+ run: (stack) => validatePredicatePathRefs(stack)
9839
+ },
8513
9840
  // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
8514
9841
  // `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
8515
9842
  // runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
@@ -8667,16 +9994,110 @@ var AUTHORING_RULES = [
8667
9994
  // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
8668
9995
  // binding gate, vocabulary freeze), moving the failure from a runtime deny to
8669
9996
  // an author-time fix-it. Per ADR-0049 this is not advisory security.
9997
+ //
9998
+ // [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
9999
+ // was false in both halves — it read: "Already gated at this surface by a
10000
+ // DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
10001
+ // `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
10002
+ // on every runtime write. Running the linter here as well would double-report
10003
+ // one refusal in two vocabularies."
10004
+ //
10005
+ // - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
10006
+ // `externalSharingModel` through a local `OWD_WIDTH`, and never touches
10007
+ // `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
10008
+ // block carries it covers ONE — `security-external-wider-than-internal`
10009
+ // (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
10010
+ // corresponds to no lint rule at all, so it is not coverage in the other
10011
+ // direction either. Twelve rules were enforced at no runtime door while
10012
+ // this field said they were.
10013
+ // - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
10014
+ // `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
10015
+ // `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
10016
+ // both refuse by THROWING. The first to fire ends the write, so an author
10017
+ // sees one refusal, never two. The stated cost of moving was imaginary; the
10018
+ // reason it has not moved is the measured one below.
10019
+ //
10020
+ // The move IS taken now — the #7891 programme's three slices, in order:
10021
+ //
10022
+ // - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
10023
+ // the isolation proof that the differential cancels every finding this
10024
+ // function derives from the sibling collections.
10025
+ // - #8309: the snapshot repair. The gate used to carry `objects` and
10026
+ // nothing else, so the three cross-collection rules judged a universe
10027
+ // missing the collection they compare against (measured: 38 phantom
10028
+ // `security-master-detail-ungranted` per-write vs 4 whole-stack,
10029
+ // PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
10030
+ // BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
10031
+ // - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
10032
+ // `object` measured DIRTY on that tree and was escalated, not forced.
10033
+ // - #8310 slice 2 (this state): `object` crosses under the maintainer
10034
+ // ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
10035
+ // authored OWD is REQUIRED at the runtime object door — an object
10036
+ // publish with no authored `sharingModel` is refused with the 422 lint
10037
+ // envelope (`security-owd-unset`); absence is not a decision. The ~16
10038
+ // objectql/rest suite files that relied on OWD-less publishes were
10039
+ // repaired honestly (fixtures author their posture), and
10040
+ // `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
10041
+ // answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
10042
+ // ADR-0094-seam 403 doors answer for what passes lint. The same ruling
10043
+ // retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
10044
+ // of this door (R1 env-tighten-only STAYS — no lint rule covers it);
10045
+ // see `object-posture-gate.ts` and the ADR-0094 amendment.
10046
+ //
10047
+ // `security-role-word` is NOT in this entry any more — that is what the
10048
+ // `validateSecurityRoleWord` entry below records. It judges six collections
10049
+ // (objects, fields, actions, permission sets, positions, apps — plus books),
10050
+ // and `positions`/`apps` are neither carried by the per-write snapshot nor
10051
+ // mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
10052
+ // function that still contained it would have enforced ONE rule id for a
10053
+ // strict subset of its collections: a door where a permission set named
10054
+ // `role_manager` is refused and a position named `sales_role` walks through
10055
+ // — the #7220 failure this table refuses to build, in either direction. The
10056
+ // rule therefore stays behind WHOLE (#8310's explicit call), as its own
10057
+ // entry.
10058
+ //
10059
+ // This entry remains the rest of the D7 block (12 rule ids) as ONE
10060
+ // registration, not a per-rule split: the baseline/candidate differential is
10061
+ // what keeps a write of one declared type from leaking the other rules'
10062
+ // whole-stack findings — every finding derived from a sibling collection is
10063
+ // produced byte-identically in both passes and cancels in the diff. Only
10064
+ // findings the written item itself adds are attributed to the write.
8670
10065
  {
8671
10066
  name: "validateSecurityPosture",
8672
10067
  tier: "gating",
8673
10068
  input: "parsed",
8674
10069
  commands: ALL,
8675
10070
  source: "packages/lint/src/validate-security-posture.ts",
8676
- surfaces: CLI_ONLY,
8677
- surfaceReason: "Already gated at this surface by a DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on `object` (`registerAuthoringGate`) that enforces the same OWD posture rules on every runtime write. Running the linter here as well would double-report one refusal in two vocabularies. Consolidating the two onto this table is P2 (#4463), and is a merge, not a hole.",
10071
+ surfaces: CLI_AND_RUNTIME,
10072
+ runtimeTypes: ["seed", "permission", "book", "object"],
8678
10073
  run: (stack) => validateSecurityPosture(stack)
8679
10074
  },
10075
+ // [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
10076
+ // `validateSecurityPosture` the day the rest of that block crossed the
10077
+ // runtime wall — so that it could stay behind WHOLE rather than cross for
10078
+ // three of the six collections it judges (#7220: one rule id must sit on ONE
10079
+ // side of the wall). The split is a surface boundary, not taste: the rule's
10080
+ // verdict and findings are byte-identical to before on every CLI command
10081
+ // (both entries run on all three), and the runtime door does not run it for
10082
+ // ANY type.
10083
+ //
10084
+ // The road to crossing is concrete and short, recorded here so the next
10085
+ // seat prices it correctly: carry `positions`/`apps` in
10086
+ // `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
10087
+ // `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
10088
+ // are real), then declare `runtimeTypes: ['object', 'permission', 'book',
10089
+ // 'position', 'app']` on THIS entry — all six collections in one edit, the
10090
+ // #7220 discipline satisfied.
10091
+ {
10092
+ name: "validateSecurityRoleWord",
10093
+ tier: "gating",
10094
+ input: "parsed",
10095
+ commands: ALL,
10096
+ source: "packages/lint/src/validate-security-posture.ts",
10097
+ surfaces: CLI_ONLY,
10098
+ surfaceReason: "P2 (#4463)/#8310: judges six collections (objects, fields, actions, permission sets, positions, apps \u2014 plus books), and the per-write snapshot neither carries nor maps positions/apps. Wiring it for the mapped types alone would enforce one rule id for three of its six collections \u2014 the #7220 split (an object named sales_role refused while a position named sales_role walks through). It crosses whole \u2014 positions/apps carried, mapped and declared \u2014 or stays behind; it stays behind until that wiring exists.",
10099
+ run: (stack) => validateSecurityRoleWord(stack)
10100
+ },
8680
10101
  // ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
8681
10102
  // sharing rule that walks it builds a second permission hierarchy (the
8682
10103
  // dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
@@ -8727,7 +10148,7 @@ var AUTHORING_RULES = [
8727
10148
  commands: ALL,
8728
10149
  source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
8729
10150
  surfaces: CLI_ONLY,
8730
- surfaceReason: "P2 (#4463): the rule reads `stack.permissions[]`, a stack-wide collection the per-write snapshot does not carry, and P1 gates `flow` alone. It is otherwise snapshot-ready \u2014 it needs no other collection \u2014 so widening it is a `runtimeTypes: ['permission_set']` edit once the gate builds that snapshot, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
10151
+ surfaceReason: "The rule reads `stack.permissions[]`, which the per-write snapshot DOES carry since #8309 \u2014 the remaining gap is only the declaration: no `runtimeTypes` names `permission` here, and that flip is a rollout decision on #8310's axis, not a wiring fix. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
8731
10152
  run: (stack) => validateRlsPredicateEnforceability(stack)
8732
10153
  },
8733
10154
  // #4762 — the same "declared but enforces nothing" question, for the two
@@ -8785,8 +10206,34 @@ var TYPE_TO_STACK_KEY = {
8785
10206
  dashboard: "dashboards",
8786
10207
  agent: "agents",
8787
10208
  hook: "hooks",
8788
- seed: "seeds"
10209
+ // [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
10210
+ // holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
10211
+ // — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
10212
+ // mapping onto one either.
10213
+ //
10214
+ // The wrong spelling was INERT rather than harmless, and it is the #4449 shape
10215
+ // one surface over: the wiring guard asks only that a declared type HAS a
10216
+ // mapping, never that the mapping names a key some rule reads. So it would
10217
+ // have stayed green while the gate built `{ objects, seeds: [item] }` for
10218
+ // every seed write and every rule reading `stack.data` saw nothing — wired,
10219
+ // and running on nothing, with `rulesRun` reporting the rules as having run.
10220
+ // Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
10221
+ // changed no behaviour then; it was corrected here, with the measurement
10222
+ // that found it (#7576), rather than left for the rollout card to trip
10223
+ // over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
10224
+ // mapping is load-bearing today, not merely inert-and-correct.
10225
+ seed: "data",
10226
+ // [#8309] `permission`/`book` map ahead of their registration (#8310), the
10227
+ // same order `seed` arrived in: the mapping plus the enriched snapshot below
10228
+ // are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
10229
+ // a mapping without a declaring rule is inert by construction (the gate
10230
+ // filters by `runtimeTypes` before it ever consults this table), while a
10231
+ // declaration without the mapping is the wired-onto-nothing state the wiring
10232
+ // guard refuses. Landing the mapping first keeps #8310 a registry data edit.
10233
+ permission: "permissions",
10234
+ book: "books"
8789
10235
  };
10236
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
8790
10237
  function runtimeAuthoringRulesFor(type) {
8791
10238
  return AUTHORING_RULES.filter(
8792
10239
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -8804,6 +10251,23 @@ function stackKeyForType(type) {
8804
10251
  return TYPE_TO_STACK_KEY[type] ?? null;
8805
10252
  }
8806
10253
  var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
10254
+ function buildRuntimeWriteSnapshots(args) {
10255
+ const stackKey = stackKeyForType(args.type);
10256
+ if (!stackKey) return null;
10257
+ if (!args.item || typeof args.item !== "object") return null;
10258
+ const item = args.item;
10259
+ const itemName = typeof item.name === "string" ? item.name : void 0;
10260
+ const baseline = {};
10261
+ for (const key of CONTEXT_STACK_KEYS) {
10262
+ const collection = args.context?.[key] ?? [];
10263
+ baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10264
+ }
10265
+ const candidate = {
10266
+ ...baseline,
10267
+ [stackKey]: [...baseline[stackKey] ?? [], item]
10268
+ };
10269
+ return { baseline, candidate };
10270
+ }
8807
10271
  function runRules(rules, stack, ctx) {
8808
10272
  const findings = [];
8809
10273
  for (const rule of rules) {
@@ -8826,19 +10290,15 @@ function runRuntimeAuthoringRules(args) {
8826
10290
  const rules = runtimeAuthoringRulesFor(args.type);
8827
10291
  const empty = { errors: [], advisories: [], rulesRun: [] };
8828
10292
  if (rules.length === 0) return empty;
8829
- const stackKey = stackKeyForType(args.type);
8830
- if (!stackKey) return empty;
8831
- if (!args.item || typeof args.item !== "object") return empty;
8832
- const item = args.item;
8833
- const itemName = typeof item.name === "string" ? item.name : void 0;
8834
- const contextObjects = args.context?.objects ?? [];
10293
+ const snapshots = buildRuntimeWriteSnapshots({
10294
+ type: args.type,
10295
+ item: args.item,
10296
+ ...args.context !== void 0 ? { context: args.context } : {}
10297
+ });
10298
+ if (!snapshots) return empty;
8835
10299
  const ctx = { sduiManifest: args.sduiManifest };
8836
- const writesIntoContext = stackKey === "objects";
8837
- const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
8838
- const baseline = { objects: baselineObjects };
8839
- const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
8840
- const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
8841
- const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
10300
+ const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10301
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
8842
10302
  return {
8843
10303
  errors: added.filter((f) => f.severity === "error"),
8844
10304
  advisories: added.filter((f) => f.severity !== "error"),
@@ -8846,6 +10306,7 @@ function runRuntimeAuthoringRules(args) {
8846
10306
  };
8847
10307
  }
8848
10308
  export {
10309
+ buildRuntimeWriteSnapshots,
8849
10310
  runRuntimeAuthoringRules,
8850
10311
  runtimeAuthoringRulesFor,
8851
10312
  runtimeGatedTypes,