@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.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/runtime.ts
21
21
  var runtime_exports = {};
22
22
  __export(runtime_exports, {
23
+ buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
23
24
  runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
24
25
  runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
25
26
  runtimeGatedTypes: () => runtimeGatedTypes,
@@ -41,6 +42,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
41
42
  function injectedColumnsFor(objectDef) {
42
43
  return (0, import_data.resolveInjectedSystemColumns)(objectDef).names;
43
44
  }
45
+ function unprovisionedInjectedColumnsFor(objectDef) {
46
+ return new Set((0, import_data.unprovisionedInjectedColumns)(objectDef));
47
+ }
48
+ function objectDefsOf(stack) {
49
+ if (!stack || typeof stack !== "object") return [];
50
+ const objects = stack.objects;
51
+ if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
52
+ if (objects && typeof objects === "object") {
53
+ return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
54
+ }
55
+ return [];
56
+ }
57
+ function indexUnprovisionedAnchors(stack) {
58
+ const index = /* @__PURE__ */ new Map();
59
+ for (const obj of objectDefsOf(stack)) {
60
+ const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
61
+ if (!name) continue;
62
+ const anchors = unprovisionedInjectedColumnsFor(obj);
63
+ if (anchors.size > 0) index.set(name, anchors);
64
+ }
65
+ return index;
66
+ }
67
+ function unprovisionedAnchorCause(objectName, field) {
68
+ 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`;
69
+ }
70
+ function unprovisionedAnchorHint(objectName, field) {
71
+ 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).`;
72
+ }
44
73
 
45
74
  // src/validate-null-guards.ts
46
75
  var import_formula = require("@objectstack/formula");
@@ -274,6 +303,37 @@ function buildFieldIndex(objects) {
274
303
  }
275
304
  return idx;
276
305
  }
306
+ var BOUND_RECORD_ROOTS = ["record", "previous"];
307
+ function isCelNode(v) {
308
+ return !!v && typeof v === "object" && typeof v.op === "string";
309
+ }
310
+ function collectBoundRecordReads(source) {
311
+ const out = /* @__PURE__ */ new Map();
312
+ const ast = (0, import_formula2.parseCelToAst)(source);
313
+ if (!ast) return out;
314
+ const pending = [ast];
315
+ while (pending.length > 0) {
316
+ const celNode = pending.pop();
317
+ if (!isCelNode(celNode)) continue;
318
+ if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
319
+ const [celRecv, seg] = celNode.args;
320
+ if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
321
+ if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
322
+ }
323
+ }
324
+ const celArgs = celNode.args;
325
+ if (isCelNode(celArgs)) pending.push(celArgs);
326
+ else if (Array.isArray(celArgs)) {
327
+ for (const a of celArgs) {
328
+ if (isCelNode(a)) pending.push(a);
329
+ else if (Array.isArray(a)) {
330
+ for (const b of a) if (isCelNode(b)) pending.push(b);
331
+ }
332
+ }
333
+ }
334
+ }
335
+ return out;
336
+ }
277
337
  function buildFieldTypeIndex(objects) {
278
338
  const idx = /* @__PURE__ */ new Map();
279
339
  for (const obj of objects) {
@@ -373,6 +433,29 @@ function validateStackExpressions(stack) {
373
433
  const fieldIndex = buildFieldIndex(objects);
374
434
  const fieldTypeIndex = buildFieldTypeIndex(objects);
375
435
  const nullableIndex = buildNullableFieldIndex(objects);
436
+ const unprovisionedIndex = /* @__PURE__ */ new Map();
437
+ for (const obj of objects) {
438
+ const name = typeof obj.name === "string" ? obj.name : void 0;
439
+ if (!name) continue;
440
+ const anchors = unprovisionedInjectedColumnsFor(obj);
441
+ if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
442
+ }
443
+ const warnUnprovisionedAnchors = (where, raw, objectName) => {
444
+ if (!objectName) return;
445
+ const anchors = unprovisionedIndex.get(objectName);
446
+ if (!anchors) return;
447
+ const source = celSourceOf(raw);
448
+ if (!source) return;
449
+ for (const [field, operand] of collectBoundRecordReads(source)) {
450
+ if (!anchors.has(field)) continue;
451
+ issues.push({
452
+ where,
453
+ 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).`,
454
+ source,
455
+ severity: "warning"
456
+ });
457
+ }
458
+ };
376
459
  const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
377
460
  if (!objectName) return;
378
461
  const nullableFields = nullableIndex.get(objectName);
@@ -399,6 +482,39 @@ function validateStackExpressions(stack) {
399
482
  );
400
483
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
401
484
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
485
+ warnUnprovisionedAnchors(where, raw, objectName);
486
+ };
487
+ const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
488
+ const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
489
+ 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";
490
+ const FIELD_RULE_SLOT_CONSEQUENCE = {
491
+ 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)",
492
+ 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",
493
+ 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",
494
+ // Listed rather than left to the `??` below, so the map covers every slot
495
+ // the field walk passes and the default stays unreachable. `FieldSchema`
496
+ // declares this key only as a `retiredKey`, which rejects it by name, so
497
+ // there is no fourth runtime to measure — the honest clause is the generic
498
+ // one, not a fabricated fourth cell (#6716).
499
+ conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
500
+ };
501
+ const checkFieldRuleRoot = (where, slot, raw) => {
502
+ const source = celSourceOf(raw);
503
+ if (!source) return;
504
+ const roots = (0, import_formula2.collectCelRootIdentifiers)(source);
505
+ if (!roots.ok) return;
506
+ const kept = import_formula2.SCOPE_ROOTS.filter(
507
+ (r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
508
+ );
509
+ if (kept.length === 0) return;
510
+ const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
511
+ 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}\`.`;
512
+ issues.push({
513
+ where,
514
+ 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,
515
+ source,
516
+ severity: "error"
517
+ });
402
518
  };
403
519
  const checkDeclaredPredicate = (where, raw) => {
404
520
  if (raw == null) return;
@@ -432,7 +548,14 @@ function validateStackExpressions(stack) {
432
548
  if (retired.length > 0) {
433
549
  issues.push({
434
550
  where: `${at} \xB7 node '${node.id}' (script) callable`,
435
- 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.`,
551
+ 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
552
+ // behaviour, never the retired key's fate — "rewrite it" reads two ways
553
+ // over a branch that DELETES the key (template/recipients/variables/script),
554
+ // "rewrite existing sources" only one. Plain-quoted (not a template literal)
555
+ // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
556
+ // widened scan (#7030) on the same textual shape as the spec corpus — no
557
+ // interpolation lives in this clause, so nothing is lost switching quote style.
558
+ "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
436
559
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
437
560
  });
438
561
  } else if (!fn) {
@@ -466,13 +589,27 @@ function validateStackExpressions(stack) {
466
589
  for (const [fname, f] of fieldList) {
467
590
  for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
468
591
  check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
592
+ checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
593
+ }
594
+ for (const [oi, opt] of asArray(f.options).entries()) {
595
+ const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
596
+ check(
597
+ `object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
598
+ opt.visibleWhen,
599
+ objectName,
600
+ "record"
601
+ );
469
602
  }
470
- const roWhenSource = celSourceOf(f.readonlyWhen);
471
- if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
603
+ for (const [slot, raw, consequence2] of [
604
+ ["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
605
+ ["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`]
606
+ ]) {
607
+ const source = celSourceOf(raw);
608
+ if (masters === 1 || !source || !readsParentRoot(source)) continue;
472
609
  issues.push({
473
- where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
474
- 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\`.`),
475
- source: roWhenSource,
610
+ where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
611
+ 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\`.`),
612
+ source,
476
613
  severity: "error"
477
614
  });
478
615
  }
@@ -492,6 +629,7 @@ function validateStackExpressions(stack) {
492
629
  const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
493
630
  for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
494
631
  for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
632
+ warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
495
633
  }
496
634
  }
497
635
  }
@@ -697,6 +835,43 @@ function validateFunctionalCompleteness(stack) {
697
835
  return out;
698
836
  }
699
837
 
838
+ // src/validate-managed-api-methods.ts
839
+ var import_data2 = require("@objectstack/spec/data");
840
+ var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
841
+ var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
842
+ function entriesOf2(v) {
843
+ if (Array.isArray(v)) {
844
+ return v.flatMap(
845
+ (def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
846
+ );
847
+ }
848
+ if (isRec2(v)) {
849
+ return Object.entries(v).flatMap(
850
+ ([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
851
+ );
852
+ }
853
+ return [];
854
+ }
855
+ function validateManagedApiMethods(stack) {
856
+ const out = [];
857
+ if (!isRec2(stack)) return out;
858
+ for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
859
+ const conflicts = (0, import_data2.checkManagedApiMethodAffordances)(obj.def);
860
+ if (conflicts.length === 0) continue;
861
+ const verbs = conflicts.map((c) => c.verb).join(", ");
862
+ const flags = [...new Set(conflicts.map((c) => c.needs))];
863
+ out.push({
864
+ severity: "error",
865
+ rule: MANAGED_API_METHOD_UNAFFORDABLE,
866
+ where: `object "${obj.name}"`,
867
+ path: `objects[${oi}].enable.apiMethods`,
868
+ message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + (0, import_data2.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.`,
869
+ 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.`
870
+ });
871
+ }
872
+ return out;
873
+ }
874
+
700
875
  // src/validate-view-containers.ts
701
876
  var VIEW_CONTAINER_SHAPE = "view-container-shape";
702
877
  var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
@@ -714,10 +889,32 @@ function containerViewCount(rec) {
714
889
  function validateViewContainers(stack) {
715
890
  const out = [];
716
891
  if (!stack || typeof stack !== "object") return out;
892
+ const viewItems = stack.viewItems;
893
+ if (viewItems != null && asEntries(viewItems).length > 0) {
894
+ out.push({
895
+ severity: "error",
896
+ rule: VIEW_CONTAINER_SHAPE,
897
+ where: "viewItems",
898
+ path: "viewItems",
899
+ 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.",
900
+ 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."
901
+ });
902
+ }
717
903
  for (const { key, value } of asEntries(stack.views)) {
718
904
  if (!value || typeof value !== "object" || Array.isArray(value)) continue;
719
905
  const rec = value;
720
- if (rec.viewKind != null) continue;
906
+ if (rec.viewKind != null) {
907
+ const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
908
+ out.push({
909
+ severity: "error",
910
+ rule: VIEW_CONTAINER_SHAPE,
911
+ where: `views${key}${label3}`,
912
+ path: `views${key}`,
913
+ 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).",
914
+ 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:`."
915
+ });
916
+ continue;
917
+ }
721
918
  if (containerViewCount(rec) > 0) continue;
722
919
  const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
723
920
  const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
@@ -735,7 +932,7 @@ function validateViewContainers(stack) {
735
932
  }
736
933
 
737
934
  // src/validate-widget-bindings.ts
738
- var import_data2 = require("@objectstack/spec/data");
935
+ var import_data3 = require("@objectstack/spec/data");
739
936
  var import_ui = require("@objectstack/spec/ui");
740
937
  var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
741
938
  var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
@@ -747,6 +944,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
747
944
  var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
748
945
  var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
749
946
  var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
947
+ var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
750
948
  var LEGACY_ANALYTICS_KEYS = [
751
949
  "categoryField",
752
950
  "valueField",
@@ -866,6 +1064,7 @@ function validateWidgetBindings(stack) {
866
1064
  }
867
1065
  objectFieldTypes.set(o.name, fm);
868
1066
  }
1067
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
869
1068
  const datasetList = asArray3(stack.datasets);
870
1069
  for (let i = 0; i < datasetList.length; i++) {
871
1070
  const ds = datasetList[i];
@@ -878,7 +1077,7 @@ function validateWidgetBindings(stack) {
878
1077
  const aggregate = typeof m.aggregate === "string" ? m.aggregate : void 0;
879
1078
  if (!field || !aggregate) continue;
880
1079
  const ftype = fieldTypes.get(field);
881
- if (ftype && (0, import_data2.isIncoherentAggregate)(aggregate, ftype)) {
1080
+ if (ftype && (0, import_data3.isIncoherentAggregate)(aggregate, ftype)) {
882
1081
  findings.push({
883
1082
  severity: "warning",
884
1083
  rule: MEASURE_AGGREGATE_INCOHERENT,
@@ -952,13 +1151,24 @@ function validateWidgetBindings(stack) {
952
1151
  if (dashFilterDefs.length > 0) {
953
1152
  const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
954
1153
  const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
955
- if (objectFields) {
1154
+ const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
1155
+ if (objectFields && datasetObject) {
956
1156
  for (const def of dashFilterDefs) {
957
1157
  const eff = effectiveFilterField(w, def);
958
1158
  if (!eff) continue;
959
1159
  const field = eff.field;
960
1160
  if (field.includes(".")) continue;
961
- if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
1161
+ if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
1162
+ if (anchors?.has(field)) {
1163
+ push2({
1164
+ severity: "warning",
1165
+ rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
1166
+ 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).`,
1167
+ 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.`
1168
+ });
1169
+ }
1170
+ continue;
1171
+ }
962
1172
  push2({
963
1173
  severity: "error",
964
1174
  rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
@@ -1195,8 +1405,9 @@ function validateDashboardActionRefs(stack) {
1195
1405
  }
1196
1406
 
1197
1407
  // src/validate-filter-tokens.ts
1198
- var import_data3 = require("@objectstack/spec/data");
1199
- var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1408
+ var import_data4 = require("@objectstack/spec/data");
1409
+
1410
+ // src/filter-walk.ts
1200
1411
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
1201
1412
  function asArray5(v) {
1202
1413
  if (Array.isArray(v)) return v;
@@ -1208,11 +1419,66 @@ function asArray5(v) {
1208
1419
  function label(v, fallback) {
1209
1420
  return typeof v === "string" && v.length > 0 ? v : fallback;
1210
1421
  }
1211
- var KNOWN_LIST = import_data3.CONTEXT_TOKENS.join("}, {");
1422
+ function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
1423
+ if (!node || typeof node !== "object") return;
1424
+ if (seen.has(node)) return;
1425
+ seen.add(node);
1426
+ if (Array.isArray(node)) {
1427
+ node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
1428
+ return;
1429
+ }
1430
+ for (const [k, v] of Object.entries(node)) {
1431
+ const childPath = `${path}.${k}`;
1432
+ if (FILTER_KEYS.has(k)) {
1433
+ visit({ value: v, path: childPath, where });
1434
+ continue;
1435
+ }
1436
+ scanForFilters(v, childPath, where, visit, seen);
1437
+ }
1438
+ }
1439
+ function walkAuthoredFilters(stack, surfaces, visit) {
1440
+ if (!stack || typeof stack !== "object") return;
1441
+ for (const { key, kind } of surfaces) {
1442
+ const items = asArray5(stack[key]);
1443
+ items.forEach((item, i) => {
1444
+ const name = label(item.name ?? item.id, `#${i}`);
1445
+ if (kind === "dashboard") {
1446
+ const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1447
+ widgets.forEach((w, wi) => {
1448
+ const wName = label(w.id ?? w.title, `#${wi}`);
1449
+ scanForFilters(
1450
+ w,
1451
+ `${key}[${i}].widgets[${wi}]`,
1452
+ `dashboard "${name}" \xB7 widget "${wName}"`,
1453
+ visit,
1454
+ /* @__PURE__ */ new Set()
1455
+ );
1456
+ });
1457
+ const { widgets: _skip, ...rest } = item;
1458
+ scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
1459
+ return;
1460
+ }
1461
+ scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
1462
+ });
1463
+ }
1464
+ }
1465
+
1466
+ // src/validate-filter-tokens.ts
1467
+ var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1468
+ var KNOWN_LIST = import_data4.CONTEXT_TOKENS.join("}, {");
1469
+ var TOKEN_FILTER_SURFACES = [
1470
+ { key: "dashboards", kind: "dashboard" },
1471
+ { key: "objects", kind: "object" },
1472
+ { key: "views", kind: "view" },
1473
+ { key: "reports", kind: "report" },
1474
+ { key: "datasets", kind: "dataset" },
1475
+ { key: "pages", kind: "page" },
1476
+ { key: "apps", kind: "app" }
1477
+ ];
1212
1478
  function walkFilterValues(node, path, where, out, seen) {
1213
1479
  if (node === null || node === void 0) return;
1214
1480
  if (typeof node === "string") {
1215
- const cls = (0, import_data3.classifyFilterToken)(node);
1481
+ const cls = (0, import_data4.classifyFilterToken)(node);
1216
1482
  if (cls?.kind === "unknown") {
1217
1483
  const suggestion = cls.suggestion;
1218
1484
  out.push({
@@ -1237,58 +1503,132 @@ function walkFilterValues(node, path, where, out, seen) {
1237
1503
  walkFilterValues(v, `${path}.${k}`, where, out, seen);
1238
1504
  }
1239
1505
  }
1240
- function scanForFilters(node, path, where, out, seen) {
1241
- if (!node || typeof node !== "object") return;
1242
- if (seen.has(node)) return;
1243
- seen.add(node);
1244
- if (Array.isArray(node)) {
1245
- node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, out, seen));
1506
+ function validateFilterTokens(stack) {
1507
+ if (!stack || typeof stack !== "object") return [];
1508
+ const out = [];
1509
+ walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
1510
+ walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
1511
+ });
1512
+ return out;
1513
+ }
1514
+
1515
+ // src/validate-empty-combinators.ts
1516
+ var import_data5 = require("@objectstack/spec/data");
1517
+ var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
1518
+ var FILTER_EMPTY_NODE = "filter-empty-node";
1519
+ var EMPTY_COMBINATOR_SURFACES = [
1520
+ { key: "dashboards", kind: "dashboard" },
1521
+ { key: "objects", kind: "object" },
1522
+ { key: "views", kind: "view" },
1523
+ { key: "reports", kind: "report" },
1524
+ { key: "datasets", kind: "dataset" },
1525
+ { key: "pages", kind: "page" },
1526
+ { key: "apps", kind: "app" },
1527
+ { key: "flows", kind: "flow" }
1528
+ ];
1529
+ function isFilterNode(value) {
1530
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1531
+ const proto = Object.getPrototypeOf(value);
1532
+ return proto === Object.prototype || proto === null;
1533
+ }
1534
+ var VERDICT_OF = {
1535
+ $and: (0, import_data5.reduceFilterVerdict)({ $and: [] }),
1536
+ $or: (0, import_data5.reduceFilterVerdict)({ $or: [] }),
1537
+ $not: (0, import_data5.reduceFilterVerdict)({ $not: {} }),
1538
+ node: (0, import_data5.reduceFilterVerdict)({}),
1539
+ /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
1540
+ orWithEmptyBranch: (0, import_data5.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
1541
+ };
1542
+ function rows(verdict) {
1543
+ if (verdict === "true") return "matches EVERY row";
1544
+ if (verdict === "false") return "matches NO row";
1545
+ return "carries a real predicate";
1546
+ }
1547
+ 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.";
1548
+ 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).';
1549
+ function emitEmptyCombinator(key, path, ctx) {
1550
+ const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
1551
+ 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.`;
1552
+ 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}`;
1553
+ ctx.out.push({
1554
+ severity: "error",
1555
+ rule: FILTER_EMPTY_COMBINATOR,
1556
+ where: ctx.where,
1557
+ path,
1558
+ message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
1559
+ 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.`
1560
+ });
1561
+ }
1562
+ function emitEmptyNode(position, path, ctx) {
1563
+ if (position === "root") {
1564
+ ctx.out.push({
1565
+ severity: "error",
1566
+ rule: FILTER_EMPTY_NODE,
1567
+ where: ctx.where,
1568
+ path,
1569
+ 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.`,
1570
+ hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
1571
+ });
1246
1572
  return;
1247
1573
  }
1248
- for (const [k, v] of Object.entries(node)) {
1249
- const childPath = `${path}.${k}`;
1250
- if (FILTER_KEYS.has(k)) {
1251
- walkFilterValues(v, childPath, where, out, /* @__PURE__ */ new Set());
1574
+ if (position === "or-branch") {
1575
+ ctx.out.push({
1576
+ severity: "error",
1577
+ rule: FILTER_EMPTY_NODE,
1578
+ where: ctx.where,
1579
+ path,
1580
+ 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.`,
1581
+ 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.)"
1582
+ });
1583
+ return;
1584
+ }
1585
+ ctx.out.push({
1586
+ severity: "error",
1587
+ rule: FILTER_EMPTY_NODE,
1588
+ where: ctx.where,
1589
+ path,
1590
+ 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.",
1591
+ 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."
1592
+ });
1593
+ }
1594
+ function scanNodeKeys(node, path, ctx) {
1595
+ for (const [key, value] of Object.entries(node)) {
1596
+ if (key === "$and" || key === "$or") {
1597
+ if (!Array.isArray(value)) continue;
1598
+ if (value.length === 0) {
1599
+ emitEmptyCombinator(key, `${path}.${key}`, ctx);
1600
+ continue;
1601
+ }
1602
+ value.forEach((element, index) => {
1603
+ scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
1604
+ });
1605
+ continue;
1606
+ }
1607
+ if (key === "$not") {
1608
+ if (!isFilterNode(value)) continue;
1609
+ if (Object.keys(value).length === 0) {
1610
+ emitEmptyCombinator("$not", `${path}.$not`, ctx);
1611
+ continue;
1612
+ }
1613
+ scanNodeKeys(value, `${path}.$not`, ctx);
1252
1614
  continue;
1253
1615
  }
1254
- scanForFilters(v, childPath, where, out, seen);
1255
1616
  }
1256
1617
  }
1257
- function validateFilterTokens(stack) {
1618
+ function scanBranch(value, path, position, ctx) {
1619
+ if (!isFilterNode(value)) return;
1620
+ if (Object.keys(value).length === 0) {
1621
+ emitEmptyNode(position, path, ctx);
1622
+ return;
1623
+ }
1624
+ scanNodeKeys(value, path, ctx);
1625
+ }
1626
+ function validateEmptyCombinators(stack) {
1258
1627
  if (!stack || typeof stack !== "object") return [];
1259
1628
  const out = [];
1260
- const surfaces = [
1261
- ["dashboards", "dashboard"],
1262
- ["objects", "object"],
1263
- ["views", "view"],
1264
- ["reports", "report"],
1265
- ["datasets", "dataset"],
1266
- ["pages", "page"],
1267
- ["apps", "app"]
1268
- ];
1269
- for (const [key, kind] of surfaces) {
1270
- const items = asArray5(stack[key]);
1271
- items.forEach((item, i) => {
1272
- const name = label(item.name ?? item.id, `#${i}`);
1273
- if (kind === "dashboard") {
1274
- const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1275
- widgets.forEach((w, wi) => {
1276
- const wName = label(w.id ?? w.title, `#${wi}`);
1277
- scanForFilters(
1278
- w,
1279
- `${key}[${i}].widgets[${wi}]`,
1280
- `dashboard "${name}" \xB7 widget "${wName}"`,
1281
- out,
1282
- /* @__PURE__ */ new Set()
1283
- );
1284
- });
1285
- const { widgets: _skip, ...rest } = item;
1286
- scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
1287
- return;
1288
- }
1289
- scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
1290
- });
1291
- }
1629
+ walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
1630
+ scanBranch(value, path, "root", { where, out });
1631
+ });
1292
1632
  return out;
1293
1633
  }
1294
1634
 
@@ -1482,7 +1822,7 @@ function validateObjectReferences(stack) {
1482
1822
  }
1483
1823
 
1484
1824
  // src/validate-searchable-fields.ts
1485
- var import_data4 = require("@objectstack/spec/data");
1825
+ var import_data6 = require("@objectstack/spec/data");
1486
1826
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1487
1827
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1488
1828
  function asArray7(v) {
@@ -1492,7 +1832,7 @@ function asArray7(v) {
1492
1832
  }
1493
1833
  return [];
1494
1834
  }
1495
- function isRec2(v) {
1835
+ function isRec3(v) {
1496
1836
  return !!v && typeof v === "object" && !Array.isArray(v);
1497
1837
  }
1498
1838
  function strName3(v) {
@@ -1530,7 +1870,7 @@ function resolveAllowedSet(target) {
1530
1870
  fields = { ...fields };
1531
1871
  for (const f of systemDeclared) fields[f] = {};
1532
1872
  }
1533
- const { allowed, source } = (0, import_data4.resolveSearchFieldResolution)({
1873
+ const { allowed, source } = (0, import_data6.resolveSearchFieldResolution)({
1534
1874
  fields,
1535
1875
  searchableFields: target.searchableFields,
1536
1876
  displayField: target.displayField
@@ -1568,7 +1908,7 @@ function distance2(a, b) {
1568
1908
  }
1569
1909
  function indexObjectSearchTargets(stack) {
1570
1910
  const fieldsByObject = /* @__PURE__ */ new Map();
1571
- if (!isRec2(stack)) return fieldsByObject;
1911
+ if (!isRec3(stack)) return fieldsByObject;
1572
1912
  for (const obj of asArray7(stack.objects)) {
1573
1913
  const name = strName3(obj.name);
1574
1914
  if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
@@ -1596,7 +1936,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1596
1936
  where,
1597
1937
  path: `${path}[${i}]`,
1598
1938
  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)),
1599
- 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(", ")}.` : "")
1939
+ 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(", ")}.` : "")
1940
+ });
1941
+ continue;
1942
+ }
1943
+ if ((0, import_data6.isVirtualSearchField)(target.fields[name])) {
1944
+ const vtype = target.fields[name]?.type;
1945
+ findings.push({
1946
+ severity: "error",
1947
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1948
+ where,
1949
+ path: `${path}[${i}]`,
1950
+ 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).`,
1951
+ 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).`
1600
1952
  });
1601
1953
  continue;
1602
1954
  }
@@ -1616,7 +1968,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1616
1968
  }
1617
1969
  const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
1618
1970
  let why;
1619
- if (import_data4.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
1971
+ if (import_data6.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
1620
1972
  why = "a system/audit column, which the auto-default set never includes";
1621
1973
  } else if (meta?.hidden) {
1622
1974
  why = "hidden";
@@ -1630,15 +1982,15 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1630
1982
  rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1631
1983
  where,
1632
1984
  path: `${path}[${i}]`,
1633
- message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data4.SEARCHABLE_TEXTUAL_TYPES, ...import_data4.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).`,
1634
- 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.`
1985
+ message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data6.SEARCHABLE_TEXTUAL_TYPES, ...import_data6.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).`,
1986
+ 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.`
1635
1987
  });
1636
1988
  }
1637
1989
  return findings;
1638
1990
  }
1639
1991
  function validateSearchableFields(stack) {
1640
1992
  const findings = [];
1641
- if (!isRec2(stack)) return findings;
1993
+ if (!isRec3(stack)) return findings;
1642
1994
  const objects = asArray7(stack.objects);
1643
1995
  const fieldsByObject = indexObjectSearchTargets(stack);
1644
1996
  const check = (declared, objectName, where, path, subject, role) => {
@@ -1648,7 +2000,7 @@ function validateSearchableFields(stack) {
1648
2000
  };
1649
2001
  for (let oi = 0; oi < objects.length; oi++) {
1650
2002
  const obj = objects[oi];
1651
- if (!isRec2(obj)) continue;
2003
+ if (!isRec3(obj)) continue;
1652
2004
  const objName = strName3(obj.name);
1653
2005
  const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
1654
2006
  check(
@@ -1659,9 +2011,9 @@ function validateSearchableFields(stack) {
1659
2011
  "searchableFields",
1660
2012
  "canonical"
1661
2013
  );
1662
- if (isRec2(obj.listViews)) {
2014
+ if (isRec3(obj.listViews)) {
1663
2015
  for (const [key, lv] of Object.entries(obj.listViews)) {
1664
- if (!isRec2(lv)) continue;
2016
+ if (!isRec3(lv)) continue;
1665
2017
  check(
1666
2018
  lv.searchableFields,
1667
2019
  // A built-in list view belongs to its object; an inline `data.object`
@@ -1678,10 +2030,10 @@ function validateSearchableFields(stack) {
1678
2030
  const views = asArray7(stack.views);
1679
2031
  for (let vi = 0; vi < views.length; vi++) {
1680
2032
  const view = views[vi];
1681
- if (!isRec2(view)) continue;
2033
+ if (!isRec3(view)) continue;
1682
2034
  const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
1683
2035
  const viewObject = strName3(view.objectName) ?? strName3(view.object);
1684
- if (isRec2(view.list)) {
2036
+ if (isRec3(view.list)) {
1685
2037
  check(
1686
2038
  view.list.searchableFields,
1687
2039
  listViewObject(view.list) ?? viewObject,
@@ -1691,9 +2043,9 @@ function validateSearchableFields(stack) {
1691
2043
  "narrowing"
1692
2044
  );
1693
2045
  }
1694
- if (isRec2(view.listViews)) {
2046
+ if (isRec3(view.listViews)) {
1695
2047
  for (const [key, lv] of Object.entries(view.listViews)) {
1696
- if (!isRec2(lv)) continue;
2048
+ if (!isRec3(lv)) continue;
1697
2049
  check(
1698
2050
  lv.searchableFields,
1699
2051
  listViewObject(lv) ?? viewObject,
@@ -1709,11 +2061,11 @@ function validateSearchableFields(stack) {
1709
2061
  }
1710
2062
  function listViewObject(listView) {
1711
2063
  const data = listView.data;
1712
- return isRec2(data) ? strName3(data.object) : void 0;
2064
+ return isRec3(data) ? strName3(data.object) : void 0;
1713
2065
  }
1714
2066
 
1715
2067
  // src/page-walk.ts
1716
- function isRec3(v) {
2068
+ function isRec4(v) {
1717
2069
  return !!v && typeof v === "object" && !Array.isArray(v);
1718
2070
  }
1719
2071
  function strName4(v) {
@@ -1726,19 +2078,19 @@ function isSourceAuthoredPage(page) {
1726
2078
  }
1727
2079
  function walkPageComponents(page, pagePath) {
1728
2080
  const out = [];
1729
- if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
2081
+ if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
1730
2082
  const pageObject = strName4(page.object);
1731
2083
  const visit = (node, path, inheritedObject) => {
1732
- if (!isRec3(node)) return;
1733
- const props = isRec3(node.properties) ? node.properties : void 0;
1734
- const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
2084
+ if (!isRec4(node)) return;
2085
+ const props = isRec4(node.properties) ? node.properties : void 0;
2086
+ const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
1735
2087
  const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
1736
2088
  out.push({ component: node, path, objectName });
1737
2089
  if (!props) return;
1738
2090
  if (Array.isArray(props.items)) {
1739
2091
  for (let i = 0; i < props.items.length; i++) {
1740
2092
  const item = props.items[i];
1741
- if (!isRec3(item) || !Array.isArray(item.children)) continue;
2093
+ if (!isRec4(item) || !Array.isArray(item.children)) continue;
1742
2094
  for (let c = 0; c < item.children.length; c++) {
1743
2095
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
1744
2096
  }
@@ -1760,12 +2112,12 @@ function walkPageComponents(page, pagePath) {
1760
2112
  const regions = Array.isArray(page.regions) ? page.regions : [];
1761
2113
  for (let r = 0; r < regions.length; r++) {
1762
2114
  const region = regions[r];
1763
- if (!isRec3(region) || !Array.isArray(region.components)) continue;
2115
+ if (!isRec4(region) || !Array.isArray(region.components)) continue;
1764
2116
  for (let c = 0; c < region.components.length; c++) {
1765
2117
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
1766
2118
  }
1767
2119
  }
1768
- const slots = isRec3(page.slots) ? page.slots : void 0;
2120
+ const slots = isRec4(page.slots) ? page.slots : void 0;
1769
2121
  if (slots) {
1770
2122
  for (const [slot, value] of Object.entries(slots)) {
1771
2123
  const list3 = Array.isArray(value) ? value : [value];
@@ -1950,6 +2302,15 @@ function validateActionNameRefs(stack) {
1950
2302
  "Navigation action item"
1951
2303
  );
1952
2304
  }
2305
+ const runAction = strName5(nav.runAction);
2306
+ if (nav.type === "object" && runAction) {
2307
+ check(
2308
+ runAction,
2309
+ `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2310
+ `${navPath}.runAction`,
2311
+ "Navigation deep-link auto-run"
2312
+ );
2313
+ }
1953
2314
  if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
1954
2315
  }
1955
2316
  };
@@ -1964,6 +2325,7 @@ function validateActionNameRefs(stack) {
1964
2325
 
1965
2326
  // src/validate-page-field-bindings.ts
1966
2327
  var PAGE_FIELD_UNKNOWN = "page-field-unknown";
2328
+ var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
1967
2329
  function asArray9(v) {
1968
2330
  if (Array.isArray(v)) return v;
1969
2331
  if (v && typeof v === "object") {
@@ -1974,7 +2336,7 @@ function asArray9(v) {
1974
2336
  function strName6(v) {
1975
2337
  return typeof v === "string" && v.length > 0 ? v : void 0;
1976
2338
  }
1977
- function isRec4(v) {
2339
+ function isRec5(v) {
1978
2340
  return !!v && typeof v === "object" && !Array.isArray(v);
1979
2341
  }
1980
2342
  function fieldRefsFrom(value, basePath) {
@@ -1985,7 +2347,7 @@ function fieldRefsFrom(value, basePath) {
1985
2347
  out.push({ name: bare, path });
1986
2348
  return;
1987
2349
  }
1988
- if (!isRec4(v)) return;
2350
+ if (!isRec5(v)) return;
1989
2351
  const named = strName6(v.field) ?? strName6(v.name);
1990
2352
  if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
1991
2353
  };
@@ -2015,8 +2377,21 @@ var COMPONENT_FIELD_SPECS = {
2015
2377
  "element:number": { props: ["field"] },
2016
2378
  "element:filter": { props: ["fields"] },
2017
2379
  "element:form": { props: ["fields"] },
2018
- // The schema says `displayField`; real pages author `labelField`. Accept both.
2019
- "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
2380
+ // `labelField` is the one field-bearing prop this element declares. Its former
2381
+ // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
2382
+ // `searchFields` (deleted, ADR-0049) were retired in #5775 and are
2383
+ // `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
2384
+ // spec-conformant page carries either, and this rule's job (resolve a field
2385
+ // NAME against the object) is not the question a retired key raises (#6629).
2386
+ //
2387
+ // A non-conformant page that writes one anyway is not left unattended: the
2388
+ // #5068 props gate reports the key with its rename/delete prescription. That
2389
+ // gate is advisory and CLI-only and lives in a different registry
2390
+ // (`authoring-rules`) from this suite, so it neither precedes nor suppresses
2391
+ // this rule — what these two entries actually added was a SECOND finding,
2392
+ // saying a field named by a key that no longer exists does not exist either.
2393
+ // The prescription is the useful half; this half was noise on top of it.
2394
+ "element:record_picker": { props: ["labelField"] }
2020
2395
  };
2021
2396
  var RELATED_LIST_TYPE = "record:related_list";
2022
2397
  function componentFieldRefs(type, props, basePath, sep = ".") {
@@ -2030,15 +2405,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2030
2405
  const sections = Array.isArray(props[key]) ? props[key] : [];
2031
2406
  for (let si = 0; si < sections.length; si++) {
2032
2407
  const section = sections[si];
2033
- if (!isRec4(section)) continue;
2408
+ if (!isRec5(section)) continue;
2034
2409
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2035
2410
  }
2036
2411
  }
2037
2412
  return refs;
2038
2413
  }
2039
2414
  function relatedListFieldRefs(props, basePath, sep = ".") {
2040
- const add = isRec4(props.add) ? props.add : void 0;
2041
- const picker = add && isRec4(add.picker) ? add.picker : void 0;
2415
+ const add = isRec5(props.add) ? props.add : void 0;
2416
+ const picker = add && isRec5(add.picker) ? add.picker : void 0;
2042
2417
  const at = (key) => `${basePath}${sep}${key}`;
2043
2418
  return {
2044
2419
  relatedObject: strName6(props.objectName),
@@ -2059,7 +2434,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2059
2434
  }
2060
2435
  function indexObjectFields(stack) {
2061
2436
  const objectFields = /* @__PURE__ */ new Map();
2062
- if (!isRec4(stack)) return objectFields;
2437
+ if (!isRec5(stack)) return objectFields;
2063
2438
  for (const obj of asArray9(stack.objects)) {
2064
2439
  const name = strName6(obj.name);
2065
2440
  if (!name) continue;
@@ -2072,14 +2447,27 @@ function indexObjectFields(stack) {
2072
2447
  }
2073
2448
  return objectFields;
2074
2449
  }
2075
- function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
2450
+ function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
2076
2451
  const findings = [];
2077
2452
  if (!objectName) return findings;
2078
2453
  const known = objectFields.get(objectName);
2079
2454
  if (!known) return findings;
2455
+ const anchors = unprovisionedAnchors?.get(objectName);
2080
2456
  for (const ref of refs) {
2081
2457
  if (ref.name.includes(".")) continue;
2082
- if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
2458
+ if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
2459
+ if (anchors?.has(ref.name)) {
2460
+ findings.push({
2461
+ severity: "warning",
2462
+ rule: PAGE_FIELD_UNPROVISIONED,
2463
+ where,
2464
+ path: ref.path,
2465
+ 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."),
2466
+ hint: unprovisionedAnchorHint(objectName, ref.name)
2467
+ });
2468
+ }
2469
+ continue;
2470
+ }
2083
2471
  findings.push({
2084
2472
  severity: consequence2 === "queried" ? "error" : "warning",
2085
2473
  rule: PAGE_FIELD_UNKNOWN,
@@ -2095,17 +2483,20 @@ function validatePageFieldBindings(stack) {
2095
2483
  const findings = [];
2096
2484
  if (!stack || typeof stack !== "object") return findings;
2097
2485
  const objectFields = indexObjectFields(stack);
2486
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2098
2487
  const pages = asArray9(stack.pages);
2099
2488
  for (let pi = 0; pi < pages.length; pi++) {
2100
2489
  const page = pages[pi];
2101
2490
  if (!page || typeof page !== "object") continue;
2102
2491
  const pageName = strName6(page.name) ?? `#${pi}`;
2103
2492
  const checkRefs = (refs, objectName, where) => {
2104
- findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
2493
+ findings.push(
2494
+ ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
2495
+ );
2105
2496
  };
2106
2497
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2107
2498
  const type = strName6(component.type);
2108
- const props = isRec4(component.properties) ? component.properties : void 0;
2499
+ const props = isRec5(component.properties) ? component.properties : void 0;
2109
2500
  if (!type || !props) continue;
2110
2501
  const where = `page "${pageName}" \xB7 ${type}`;
2111
2502
  const base = `${path}.properties`;
@@ -2120,7 +2511,7 @@ function validatePageFieldBindings(stack) {
2120
2511
  if (!refs) continue;
2121
2512
  checkRefs(refs, objectName, where);
2122
2513
  }
2123
- const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
2514
+ const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2124
2515
  if (cfg) {
2125
2516
  const cfgObject = strName6(cfg.source) ?? strName6(page.object);
2126
2517
  const base = `pages[${pi}].interfaceConfig`;
@@ -2129,7 +2520,7 @@ function validatePageFieldBindings(stack) {
2129
2520
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2130
2521
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2131
2522
  ];
2132
- const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
2523
+ const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2133
2524
  if (userFilters) {
2134
2525
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2135
2526
  }
@@ -2157,7 +2548,7 @@ function strName7(v) {
2157
2548
  function strList2(v) {
2158
2549
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2159
2550
  }
2160
- function isRec5(v) {
2551
+ function isRec6(v) {
2161
2552
  return !!v && typeof v === "object" && !Array.isArray(v);
2162
2553
  }
2163
2554
  function distance4(a, b) {
@@ -2286,10 +2677,10 @@ function validateChartBindings(stack) {
2286
2677
  const reports = asArray10(stack.reports);
2287
2678
  for (let ri = 0; ri < reports.length; ri++) {
2288
2679
  const report = reports[ri];
2289
- if (!isRec5(report)) continue;
2680
+ if (!isRec6(report)) continue;
2290
2681
  const reportName = strName7(report.name) ?? `#${ri}`;
2291
2682
  const checkReportChart = (chart, dataset, values, where, path) => {
2292
- if (!isRec5(chart)) return;
2683
+ if (!isRec6(chart)) return;
2293
2684
  check({
2294
2685
  dataset,
2295
2686
  // `values` is the report's measure SELECTION, not a chart ref; feeding
@@ -2313,7 +2704,7 @@ function validateChartBindings(stack) {
2313
2704
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
2314
2705
  for (let bi = 0; bi < blocks.length; bi++) {
2315
2706
  const block = blocks[bi];
2316
- if (!isRec5(block)) continue;
2707
+ if (!isRec6(block)) continue;
2317
2708
  checkReportChart(
2318
2709
  block.chart,
2319
2710
  strName7(block.dataset),
@@ -2324,9 +2715,9 @@ function validateChartBindings(stack) {
2324
2715
  }
2325
2716
  }
2326
2717
  const checkListChart = (container, where, path) => {
2327
- if (!isRec5(container)) return;
2718
+ if (!isRec6(container)) return;
2328
2719
  const chart = container.chart;
2329
- if (!isRec5(chart)) return;
2720
+ if (!isRec6(chart)) return;
2330
2721
  check({
2331
2722
  dataset: strName7(chart.dataset),
2332
2723
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
@@ -2338,10 +2729,10 @@ function validateChartBindings(stack) {
2338
2729
  const views = asArray10(stack.views);
2339
2730
  for (let vi = 0; vi < views.length; vi++) {
2340
2731
  const view = views[vi];
2341
- if (!isRec5(view)) continue;
2732
+ if (!isRec6(view)) continue;
2342
2733
  const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
2343
2734
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
2344
- if (isRec5(view.listViews)) {
2735
+ if (isRec6(view.listViews)) {
2345
2736
  for (const [key, lv] of Object.entries(view.listViews)) {
2346
2737
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
2347
2738
  }
@@ -2350,7 +2741,7 @@ function validateChartBindings(stack) {
2350
2741
  const objects = asArray10(stack.objects);
2351
2742
  for (let oi = 0; oi < objects.length; oi++) {
2352
2743
  const obj = objects[oi];
2353
- if (!isRec5(obj) || !isRec5(obj.listViews)) continue;
2744
+ if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
2354
2745
  const objName = strName7(obj.name) ?? `#${oi}`;
2355
2746
  for (const [key, lv] of Object.entries(obj.listViews)) {
2356
2747
  checkListChart(
@@ -2363,10 +2754,10 @@ function validateChartBindings(stack) {
2363
2754
  const pages = asArray10(stack.pages);
2364
2755
  for (let pi = 0; pi < pages.length; pi++) {
2365
2756
  const page = pages[pi];
2366
- if (!isRec5(page)) continue;
2757
+ if (!isRec6(page)) continue;
2367
2758
  const pageName = strName7(page.name) ?? `#${pi}`;
2368
2759
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2369
- const props = isRec5(component.properties) ? component.properties : void 0;
2760
+ const props = isRec6(component.properties) ? component.properties : void 0;
2370
2761
  if (!props || !strName7(props.dataset)) continue;
2371
2762
  const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
2372
2763
  const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
@@ -2516,10 +2907,10 @@ function validateNavAccess(stack) {
2516
2907
 
2517
2908
  // src/validate-nav-target-refs.ts
2518
2909
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
2519
- var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2910
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2520
2911
  function asArray13(v) {
2521
- if (Array.isArray(v)) return v.filter(isRec6);
2522
- if (isRec6(v)) return Object.entries(v).map(([name, def]) => isRec6(def) ? { name, ...def } : { name });
2912
+ if (Array.isArray(v)) return v.filter(isRec7);
2913
+ if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
2523
2914
  return [];
2524
2915
  }
2525
2916
  function strName9(v) {
@@ -2541,7 +2932,7 @@ function namesOf(collection) {
2541
2932
  }
2542
2933
  function validateNavTargetRefs(stack) {
2543
2934
  const findings = [];
2544
- if (!isRec6(stack)) return findings;
2935
+ if (!isRec7(stack)) return findings;
2545
2936
  const apps = asArray13(stack.apps);
2546
2937
  if (apps.length === 0) return findings;
2547
2938
  const declared = /* @__PURE__ */ new Map();
@@ -2553,7 +2944,7 @@ function validateNavTargetRefs(stack) {
2553
2944
  const walk = (items, basePath) => {
2554
2945
  if (!Array.isArray(items)) return;
2555
2946
  for (const [ni, raw] of items.entries()) {
2556
- if (!isRec6(raw)) continue;
2947
+ if (!isRec7(raw)) continue;
2557
2948
  const nav = raw;
2558
2949
  const navPath = `${basePath}[${ni}]`;
2559
2950
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
@@ -2584,47 +2975,154 @@ function validateNavTargetRefs(stack) {
2584
2975
  return findings;
2585
2976
  }
2586
2977
 
2587
- // src/validate-translation-references.ts
2588
- var import_system4 = require("@objectstack/spec/system");
2589
- var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
2590
- var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
2591
- function isRec7(v) {
2592
- return !!v && typeof v === "object" && !Array.isArray(v);
2593
- }
2978
+ // src/validate-nav-object-servability.ts
2979
+ var import_data7 = require("@objectstack/spec/data");
2980
+ var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
2981
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2594
2982
  function asArray14(v) {
2595
- if (Array.isArray(v)) return v;
2596
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
2983
+ if (Array.isArray(v)) return v.filter(isRec8);
2984
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
2597
2985
  return [];
2598
2986
  }
2599
2987
  function strName10(v) {
2600
2988
  return typeof v === "string" && v.length > 0 ? v : void 0;
2601
2989
  }
2602
- function distance5(a, b) {
2603
- const m = a.length;
2604
- const n = b.length;
2605
- if (m === 0) return n;
2606
- if (n === 0) return m;
2607
- let prev = Array.from({ length: n + 1 }, (_, j) => j);
2608
- for (let i = 1; i <= m; i++) {
2609
- const curr = [i, ...new Array(n).fill(0)];
2610
- for (let j = 1; j <= n; j++) {
2611
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2612
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
2613
- }
2614
- prev = curr;
2990
+ var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
2991
+ function validateNavObjectServability(stack) {
2992
+ const findings = [];
2993
+ if (!isRec8(stack)) return findings;
2994
+ const apps = asArray14(stack.apps);
2995
+ if (apps.length === 0) return findings;
2996
+ const ownEnable = /* @__PURE__ */ new Map();
2997
+ const objects = asArray14(stack.objects);
2998
+ for (const [oi, obj] of objects.entries()) {
2999
+ const n = strName10(obj.name);
3000
+ if (!n) continue;
3001
+ ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
2615
3002
  }
2616
- return prev[n];
2617
- }
2618
- function suggest6(target, known) {
2619
- const names = [...known];
2620
- const segmentMatch = names.find(
2621
- (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
2622
- );
2623
- if (segmentMatch) return ` Did you mean "${segmentMatch}"?`;
2624
- let best;
2625
- let bestScore = Infinity;
2626
- for (const candidate of names) {
2627
- const d = distance5(target, candidate);
3003
+ if (ownEnable.size === 0) return findings;
3004
+ for (const [ai, app] of apps.entries()) {
3005
+ const appName = strName10(app.name) ?? `#${ai}`;
3006
+ const walk = (items, basePath) => {
3007
+ if (!Array.isArray(items)) return;
3008
+ for (const [ni, raw] of items.entries()) {
3009
+ if (!isRec8(raw)) continue;
3010
+ const nav = raw;
3011
+ const navPath = `${basePath}[${ni}]`;
3012
+ if (nav.type === "object") {
3013
+ const target = strName10(nav.objectName);
3014
+ const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
3015
+ if (target && declared && !(0, import_data7.canServeApiOperation)(declared.enable, "list")) {
3016
+ const enable = isRec8(declared.enable) ? declared.enable : {};
3017
+ const apiDisabled = enable.apiEnabled === false;
3018
+ 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(", ")})` : "");
3019
+ const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
3020
+ const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
3021
+ findings.push({
3022
+ severity: "error",
3023
+ rule: NAV_OBJECT_UNSERVABLE,
3024
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
3025
+ // The nav entry is where the dead row is authored; the `enable`
3026
+ // key that condemns it is named in the message, because the fix
3027
+ // may belong at either end.
3028
+ path: `${navPath}.objectName`,
3029
+ 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.`,
3030
+ 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.`
3031
+ });
3032
+ }
3033
+ }
3034
+ if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
3035
+ }
3036
+ };
3037
+ walk(app.navigation, `apps[${ai}].navigation`);
3038
+ for (const [ari, area] of asArray14(app.areas).entries()) {
3039
+ walk(area.items, `apps[${ai}].areas[${ari}].items`);
3040
+ walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
3041
+ }
3042
+ }
3043
+ return findings;
3044
+ }
3045
+
3046
+ // src/validate-translation-references.ts
3047
+ var import_spec = require("@objectstack/spec");
3048
+ var import_system4 = require("@objectstack/spec/system");
3049
+
3050
+ // src/view-walk.ts
3051
+ function isRec9(v) {
3052
+ return !!v && typeof v === "object" && !Array.isArray(v);
3053
+ }
3054
+ function strName11(v) {
3055
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3056
+ }
3057
+ function viewObjectName(view) {
3058
+ return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
3059
+ }
3060
+ function viewContainerSites(view, basePath) {
3061
+ if (!isRec9(view)) return [];
3062
+ const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3063
+ if (isRec9(view.form)) {
3064
+ sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3065
+ }
3066
+ for (const key of ["listViews", "formViews"]) {
3067
+ const container = view[key];
3068
+ if (!isRec9(container)) continue;
3069
+ const kind = key === "listViews" ? "listView" : "formView";
3070
+ for (const [subKey, sub] of Object.entries(container)) {
3071
+ if (!isRec9(sub)) continue;
3072
+ sites.push({
3073
+ view: sub,
3074
+ path: `${basePath}.${key}.${subKey}`,
3075
+ surface: `${key}.${subKey}`,
3076
+ kind
3077
+ });
3078
+ }
3079
+ }
3080
+ return sites;
3081
+ }
3082
+ function formViewSites(view, basePath) {
3083
+ return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
3084
+ }
3085
+
3086
+ // src/validate-translation-references.ts
3087
+ var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
3088
+ var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
3089
+ function isRec10(v) {
3090
+ return !!v && typeof v === "object" && !Array.isArray(v);
3091
+ }
3092
+ function asArray15(v) {
3093
+ if (Array.isArray(v)) return v;
3094
+ if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
3095
+ return [];
3096
+ }
3097
+ function strName12(v) {
3098
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3099
+ }
3100
+ function distance5(a, b) {
3101
+ const m = a.length;
3102
+ const n = b.length;
3103
+ if (m === 0) return n;
3104
+ if (n === 0) return m;
3105
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
3106
+ for (let i = 1; i <= m; i++) {
3107
+ const curr = [i, ...new Array(n).fill(0)];
3108
+ for (let j = 1; j <= n; j++) {
3109
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3110
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
3111
+ }
3112
+ prev = curr;
3113
+ }
3114
+ return prev[n];
3115
+ }
3116
+ function suggest6(target, known) {
3117
+ const names = [...known];
3118
+ const segmentMatch = names.find(
3119
+ (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
3120
+ );
3121
+ if (segmentMatch) return ` Did you mean "${segmentMatch}"?`;
3122
+ let best;
3123
+ let bestScore = Infinity;
3124
+ for (const candidate of names) {
3125
+ const d = distance5(target, candidate);
2628
3126
  if (d < bestScore) {
2629
3127
  bestScore = d;
2630
3128
  best = candidate;
@@ -2656,30 +3154,52 @@ function collectViewRecord(view, factsFor) {
2656
3154
  };
2657
3155
  const addSections = (container, binding) => {
2658
3156
  if (!binding) return;
2659
- for (const section of asArray14(container.sections)) {
2660
- const sectionName = strName10(section.name);
3157
+ for (const section of asArray15(container.sections)) {
3158
+ const sectionName = strName12(section.name);
2661
3159
  if (sectionName) factsFor(binding).sections.add(sectionName);
2662
3160
  }
2663
3161
  };
2664
- const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
2665
- if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
2666
- addView(recordObject ?? listBinding, strName10(view.name));
2667
- for (const key of ["listViews", "formViews"]) {
2668
- const container = view[key];
2669
- if (!isRec7(container)) continue;
2670
- for (const [subKey, sub] of Object.entries(container)) {
2671
- if (!isRec7(sub)) continue;
3162
+ const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
3163
+ if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3164
+ addView(recordObject ?? listBinding, strName12(view.name));
3165
+ const named = namedViewKeys(view);
3166
+ for (const family of ["listViews", "formViews"]) {
3167
+ const container = view[family];
3168
+ if (!isRec10(container)) continue;
3169
+ const registryKeys = family === "listViews" ? named.list : named.form;
3170
+ let at = 0;
3171
+ for (const sub of Object.values(container)) {
3172
+ if (!sub || typeof sub !== "object") continue;
3173
+ const registryKey = registryKeys[at++];
3174
+ if (!isRec10(sub)) continue;
2672
3175
  const binding = bindingOf(sub) ?? listBinding;
2673
- addView(binding, subKey);
2674
- addView(binding, strName10(sub.name));
3176
+ addView(binding, registryKey);
2675
3177
  addSections(sub, binding);
2676
3178
  }
2677
3179
  }
2678
- if (isRec7(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3180
+ if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
2679
3181
  addSections(view, recordObject ?? listBinding);
2680
3182
  }
2681
- function viewObjectName(view) {
2682
- return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
3183
+ function defaultListViewKey(object, container) {
3184
+ if (!object || !isRec10(container.list)) return void 0;
3185
+ const item = (0, import_spec.expandViewContainer)(object, container).find(
3186
+ (i) => i.viewKind === "list" && i.isDefault
3187
+ );
3188
+ if (!item) return void 0;
3189
+ const prefix = `${object}.`;
3190
+ return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
3191
+ }
3192
+ function namedViewKeys(container) {
3193
+ const object = "probe";
3194
+ const prefix = `${object}.`;
3195
+ const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
3196
+ const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3197
+ const listCount = countEntries(container.listViews);
3198
+ const formCount = countEntries(container.formViews);
3199
+ if (!listCount && !formCount) return { list: [], form: [] };
3200
+ const items = (0, import_spec.expandViewContainer)(object, container);
3201
+ const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
3202
+ return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
2683
3203
  }
2684
3204
  function readOptions(field) {
2685
3205
  const raw = field.options;
@@ -2691,14 +3211,14 @@ function readOptions(field) {
2691
3211
  values.add(opt);
2692
3212
  continue;
2693
3213
  }
2694
- if (!isRec7(opt)) continue;
2695
- const value = strName10(opt.value);
3214
+ if (!isRec10(opt)) continue;
3215
+ const value = strName12(opt.value);
2696
3216
  if (!value) continue;
2697
3217
  values.add(value);
2698
- const label2 = strName10(opt.label);
3218
+ const label2 = strName12(opt.label);
2699
3219
  if (label2) byLabel.set(label2.toLowerCase(), value);
2700
3220
  }
2701
- } else if (isRec7(raw)) {
3221
+ } else if (isRec10(raw)) {
2702
3222
  for (const [value, label2] of Object.entries(raw)) {
2703
3223
  values.add(value);
2704
3224
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -2718,48 +3238,48 @@ function buildUniverse(stack) {
2718
3238
  }
2719
3239
  return facts;
2720
3240
  };
2721
- for (const obj of asArray14(stack.objects)) {
2722
- const objectName = strName10(obj.name);
3241
+ for (const obj of asArray15(stack.objects)) {
3242
+ const objectName = strName12(obj.name);
2723
3243
  if (!objectName) continue;
2724
3244
  const facts = factsFor(objectName);
2725
- for (const field of asArray14(obj.fields)) {
2726
- const fieldName = strName10(field.name);
3245
+ for (const field of asArray15(obj.fields)) {
3246
+ const fieldName = strName12(field.name);
2727
3247
  if (fieldName) facts.fields.set(fieldName, field);
2728
3248
  }
2729
- for (const action of asArray14(obj.actions)) {
2730
- const actionName = strName10(action.name);
3249
+ for (const action of asArray15(obj.actions)) {
3250
+ const actionName = strName12(action.name);
2731
3251
  if (actionName) facts.actions.set(actionName, action);
2732
3252
  }
2733
- for (const view of asArray14(obj.views)) {
2734
- collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
3253
+ for (const view of asArray15(obj.views)) {
3254
+ collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
2735
3255
  }
2736
3256
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
2737
- for (const group of asArray14(obj.fieldGroups)) {
2738
- const key = strName10(group.key) ?? strName10(group.name);
3257
+ for (const group of asArray15(obj.fieldGroups)) {
3258
+ const key = strName12(group.key) ?? strName12(group.name);
2739
3259
  if (key) facts.sections.add(key);
2740
3260
  }
2741
3261
  }
2742
- for (const view of asArray14(stack.views)) {
3262
+ for (const view of asArray15(stack.views)) {
2743
3263
  collectViewRecord(view, factsFor);
2744
3264
  }
2745
- const pages = asArray14(stack.pages);
3265
+ const pages = asArray15(stack.pages);
2746
3266
  for (let pi = 0; pi < pages.length; pi++) {
2747
3267
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
2748
3268
  if (!walked.objectName) continue;
2749
- const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
3269
+ const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
2750
3270
  if (!props) continue;
2751
- for (const section of asArray14(props.sections)) {
2752
- const sectionName = strName10(section.name);
3271
+ for (const section of asArray15(props.sections)) {
3272
+ const sectionName = strName12(section.name);
2753
3273
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
2754
3274
  }
2755
3275
  }
2756
3276
  }
2757
3277
  const globalActions = /* @__PURE__ */ new Map();
2758
3278
  const actionOwners = /* @__PURE__ */ new Map();
2759
- for (const action of asArray14(stack.actions)) {
2760
- const actionName = strName10(action.name);
3279
+ for (const action of asArray15(stack.actions)) {
3280
+ const actionName = strName12(action.name);
2761
3281
  if (!actionName) continue;
2762
- const owner = strName10(action.objectName) ?? strName10(action.object);
3282
+ const owner = strName12(action.objectName) ?? strName12(action.object);
2763
3283
  if (owner) {
2764
3284
  factsFor(owner).actions.set(actionName, action);
2765
3285
  actionOwners.set(actionName, owner);
@@ -2773,41 +3293,41 @@ function buildUniverse(stack) {
2773
3293
  }
2774
3294
  }
2775
3295
  const apps = /* @__PURE__ */ new Map();
2776
- for (const app of asArray14(stack.apps)) {
2777
- const appName = strName10(app.name);
3296
+ for (const app of asArray15(stack.apps)) {
3297
+ const appName = strName12(app.name);
2778
3298
  if (!appName) continue;
2779
3299
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
2780
3300
  const walkNav = (items) => {
2781
- for (const item of asArray14(items)) {
2782
- const id = strName10(item.id);
3301
+ for (const item of asArray15(items)) {
3302
+ const id = strName12(item.id);
2783
3303
  if (id) navIds.add(id);
2784
3304
  if (item.children) walkNav(item.children);
2785
3305
  }
2786
3306
  };
2787
3307
  walkNav(app.navigation);
2788
- for (const area of asArray14(app.areas)) {
2789
- const areaId = strName10(area.id);
3308
+ for (const area of asArray15(app.areas)) {
3309
+ const areaId = strName12(area.id);
2790
3310
  if (areaId) navIds.add(areaId);
2791
3311
  walkNav(area.navigation);
2792
3312
  }
2793
3313
  apps.set(appName, navIds);
2794
3314
  }
2795
3315
  const dashboards = /* @__PURE__ */ new Map();
2796
- for (const dash of asArray14(stack.dashboards)) {
2797
- const dashName = strName10(dash.name);
3316
+ for (const dash of asArray15(stack.dashboards)) {
3317
+ const dashName = strName12(dash.name);
2798
3318
  if (!dashName) continue;
2799
3319
  const widgets = /* @__PURE__ */ new Set();
2800
- for (const widget of asArray14(dash.widgets)) {
2801
- const id = strName10(widget.id) ?? strName10(widget.name);
3320
+ for (const widget of asArray15(dash.widgets)) {
3321
+ const id = strName12(widget.id) ?? strName12(widget.name);
2802
3322
  if (id) widgets.add(id);
2803
3323
  }
2804
3324
  const actions = /* @__PURE__ */ new Set();
2805
3325
  const headerActions = [
2806
- ...asArray14(isRec7(dash.header) ? dash.header.actions : void 0),
2807
- ...asArray14(dash.actions)
3326
+ ...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
3327
+ ...asArray15(dash.actions)
2808
3328
  ];
2809
3329
  for (const action of headerActions) {
2810
- const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
3330
+ const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
2811
3331
  if (key) actions.add(key);
2812
3332
  }
2813
3333
  dashboards.set(dashName, { widgets, actions });
@@ -2819,7 +3339,7 @@ function localePath(bundleIndex, locale) {
2819
3339
  }
2820
3340
  function validateTranslationReferences(stack) {
2821
3341
  const findings = [];
2822
- if (!isRec7(stack)) return findings;
3342
+ if (!isRec10(stack)) return findings;
2823
3343
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
2824
3344
  if (bundles.length === 0) return findings;
2825
3345
  const universe = buildUniverse(stack);
@@ -2828,13 +3348,13 @@ function validateTranslationReferences(stack) {
2828
3348
  };
2829
3349
  for (let bi = 0; bi < bundles.length; bi++) {
2830
3350
  const bundle = bundles[bi];
2831
- if (!isRec7(bundle)) continue;
3351
+ if (!isRec10(bundle)) continue;
2832
3352
  for (const [locale, rawData] of Object.entries(bundle)) {
2833
- if (!isRec7(rawData)) continue;
3353
+ if (!isRec10(rawData)) continue;
2834
3354
  const base = localePath(bi, locale);
2835
3355
  const inLocale = `locale "${locale}"`;
2836
3356
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
2837
- if (!isRec7(rawNode)) continue;
3357
+ if (!isRec10(rawNode)) continue;
2838
3358
  const objPath = `${base}.objects.${objectName}`;
2839
3359
  const facts = universe.objects.get(objectName);
2840
3360
  if (!facts) {
@@ -2860,7 +3380,7 @@ function validateTranslationReferences(stack) {
2860
3380
  );
2861
3381
  continue;
2862
3382
  }
2863
- if (!isRec7(rawField)) continue;
3383
+ if (!isRec10(rawField)) continue;
2864
3384
  checkOptionKeys(findings, {
2865
3385
  optionMap: rawField.options,
2866
3386
  field,
@@ -2942,7 +3462,7 @@ function validateTranslationReferences(stack) {
2942
3462
  );
2943
3463
  continue;
2944
3464
  }
2945
- if (!isRec7(rawApp)) continue;
3465
+ if (!isRec10(rawApp)) continue;
2946
3466
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
2947
3467
  if (navIds.has(navId)) continue;
2948
3468
  orphan(
@@ -2965,7 +3485,7 @@ function validateTranslationReferences(stack) {
2965
3485
  );
2966
3486
  continue;
2967
3487
  }
2968
- if (!isRec7(rawDash)) continue;
3488
+ if (!isRec10(rawDash)) continue;
2969
3489
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
2970
3490
  if (dash.widgets.has(widgetId)) continue;
2971
3491
  orphan(
@@ -2990,7 +3510,7 @@ function validateTranslationReferences(stack) {
2990
3510
  return findings;
2991
3511
  }
2992
3512
  function asRecord(v) {
2993
- return isRec7(v) ? v : {};
3513
+ return isRec10(v) ? v : {};
2994
3514
  }
2995
3515
  function checkOptionKeys(findings, ctx) {
2996
3516
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -3002,7 +3522,7 @@ function checkOptionKeys(findings, ctx) {
3002
3522
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3003
3523
  where: ctx.where,
3004
3524
  path: ctx.path,
3005
- 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.`,
3525
+ 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.`,
3006
3526
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
3007
3527
  });
3008
3528
  return;
@@ -3021,11 +3541,11 @@ function checkOptionKeys(findings, ctx) {
3021
3541
  }
3022
3542
  }
3023
3543
  function checkActionParams(findings, ctx) {
3024
- const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
3544
+ const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
3025
3545
  if (rawParams.length === 0) return;
3026
3546
  const declared = /* @__PURE__ */ new Set();
3027
- for (const param of asArray14(ctx.action.params)) {
3028
- const name = strName10(param.name) ?? strName10(param.field);
3547
+ for (const param of asArray15(ctx.action.params)) {
3548
+ const name = strName12(param.name) ?? strName12(param.field);
3029
3549
  if (name) declared.add(name);
3030
3550
  }
3031
3551
  for (const paramName of rawParams) {
@@ -3041,78 +3561,60 @@ function checkActionParams(findings, ctx) {
3041
3561
  }
3042
3562
  }
3043
3563
 
3044
- // src/validate-translatable-sections.ts
3045
- var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3046
- function isRec8(v) {
3564
+ // src/collection-entries.ts
3565
+ function isRec11(v) {
3047
3566
  return !!v && typeof v === "object" && !Array.isArray(v);
3048
3567
  }
3049
- function strName11(v) {
3050
- return typeof v === "string" && v.length > 0 ? v : void 0;
3051
- }
3052
- function viewObjectName2(view) {
3053
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
3054
- }
3055
3568
  function collectionEntries(v, base) {
3056
3569
  if (Array.isArray(v)) {
3057
3570
  const out = [];
3058
3571
  for (let i = 0; i < v.length; i++) {
3059
- if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3572
+ if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3060
3573
  }
3061
3574
  return out;
3062
3575
  }
3063
- if (isRec8(v)) {
3064
- return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3576
+ if (isRec11(v)) {
3577
+ return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3065
3578
  }
3066
3579
  return [];
3067
3580
  }
3581
+
3582
+ // src/validate-translatable-sections.ts
3583
+ var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3584
+ function isRec12(v) {
3585
+ return !!v && typeof v === "object" && !Array.isArray(v);
3586
+ }
3587
+ function strName13(v) {
3588
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3589
+ }
3068
3590
  function viewLabel(view) {
3069
- const name = strName11(view.name);
3591
+ const name = strName13(view.name);
3070
3592
  return name ? `view "${name}"` : "";
3071
3593
  }
3072
3594
  function joinWhere(...parts) {
3073
3595
  return parts.filter((p) => p.length > 0).join(" \xB7 ");
3074
3596
  }
3075
3597
  function collectViewSites(view, basePath, label2, sites) {
3076
- const recordObject = viewObjectName2(view);
3077
- const listBinding = isRec8(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
3078
- const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
3079
- sites.push({
3080
- path: `${basePath}.sections`,
3081
- surface: label2,
3082
- objectName: recordObject ?? listBinding,
3083
- sections: view.sections
3084
- });
3085
- if (isRec8(view.form)) {
3598
+ const recordObject = viewObjectName(view);
3599
+ const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3600
+ for (const site of viewContainerSites(view, basePath)) {
3086
3601
  sites.push({
3087
- path: `${basePath}.form.sections`,
3088
- surface: joinWhere(label2, "form"),
3089
- objectName: bindingOf(view.form) ?? listBinding,
3090
- sections: view.form.sections
3602
+ path: `${site.path}.sections`,
3603
+ surface: joinWhere(label2, site.surface),
3604
+ objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
3605
+ sections: site.view.sections
3091
3606
  });
3092
3607
  }
3093
- for (const key of ["listViews", "formViews"]) {
3094
- const container = view[key];
3095
- if (!isRec8(container)) continue;
3096
- for (const [subKey, sub] of Object.entries(container)) {
3097
- if (!isRec8(sub)) continue;
3098
- sites.push({
3099
- path: `${basePath}.${key}.${subKey}.sections`,
3100
- surface: joinWhere(label2, `${key}.${subKey}`),
3101
- objectName: bindingOf(sub) ?? listBinding,
3102
- sections: sub.sections
3103
- });
3104
- }
3105
- }
3106
3608
  }
3107
3609
  function translatedObjectNames(stack) {
3108
3610
  const out = /* @__PURE__ */ new Set();
3109
3611
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3110
3612
  for (const bundle of bundles) {
3111
- if (!isRec8(bundle)) continue;
3613
+ if (!isRec12(bundle)) continue;
3112
3614
  for (const data of Object.values(bundle)) {
3113
- if (!isRec8(data) || !isRec8(data.objects)) continue;
3615
+ if (!isRec12(data) || !isRec12(data.objects)) continue;
3114
3616
  for (const [objectName, node] of Object.entries(data.objects)) {
3115
- if (isRec8(node)) out.add(objectName);
3617
+ if (isRec12(node)) out.add(objectName);
3116
3618
  }
3117
3619
  }
3118
3620
  }
@@ -3124,22 +3626,22 @@ function suggestedName(label2) {
3124
3626
  }
3125
3627
  function validateTranslatableSections(stack) {
3126
3628
  const findings = [];
3127
- if (!isRec8(stack)) return findings;
3629
+ if (!isRec12(stack)) return findings;
3128
3630
  const translated = translatedObjectNames(stack);
3129
3631
  if (translated.size === 0) return findings;
3130
3632
  const sites = [];
3131
3633
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3132
- const objectName = strName11(obj.name);
3634
+ const objectName = strName13(obj.name);
3133
3635
  if (!objectName) continue;
3134
3636
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3135
3637
  collectViewSites(
3136
- { ...view, object: strName11(view.object) ?? objectName },
3638
+ { ...view, object: strName13(view.object) ?? objectName },
3137
3639
  path,
3138
3640
  viewLabel(view),
3139
3641
  sites
3140
3642
  );
3141
3643
  }
3142
- if (isRec8(obj.listViews)) {
3644
+ if (isRec12(obj.listViews)) {
3143
3645
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3144
3646
  }
3145
3647
  }
@@ -3147,13 +3649,13 @@ function validateTranslatableSections(stack) {
3147
3649
  collectViewSites(view, path, viewLabel(view), sites);
3148
3650
  }
3149
3651
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3150
- const pageName = strName11(page.name);
3652
+ const pageName = strName13(page.name);
3151
3653
  const pageLabel = pageName ? `page "${pageName}"` : "";
3152
3654
  for (const walked of walkPageComponents(page, pagePath)) {
3153
3655
  if (!walked.objectName) continue;
3154
- const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
3656
+ const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
3155
3657
  if (!props) continue;
3156
- const type = strName11(walked.component.type) ?? "component";
3658
+ const type = strName13(walked.component.type) ?? "component";
3157
3659
  sites.push({
3158
3660
  path: `${walked.path}.properties.sections`,
3159
3661
  surface: joinWhere(pageLabel, type),
@@ -3168,9 +3670,9 @@ function validateTranslatableSections(stack) {
3168
3670
  if (!Array.isArray(site.sections)) continue;
3169
3671
  for (let i = 0; i < site.sections.length; i++) {
3170
3672
  const section = site.sections[i];
3171
- if (!isRec8(section)) continue;
3172
- if (strName11(section.name)) continue;
3173
- const heading = strName11(section.label) ?? strName11(section.title);
3673
+ if (!isRec12(section)) continue;
3674
+ if (strName13(section.name)) continue;
3675
+ const heading = strName13(section.label);
3174
3676
  if (!heading) continue;
3175
3677
  const slug = suggestedName(heading);
3176
3678
  findings.push({
@@ -3188,10 +3690,10 @@ function validateTranslatableSections(stack) {
3188
3690
 
3189
3691
  // src/flow-walk.ts
3190
3692
  var import_automation2 = require("@objectstack/spec/automation");
3191
- function isRec9(v) {
3693
+ function isRec13(v) {
3192
3694
  return !!v && typeof v === "object" && !Array.isArray(v);
3193
3695
  }
3194
- function strName12(v) {
3696
+ function strName14(v) {
3195
3697
  return typeof v === "string" && v.length > 0 ? v : void 0;
3196
3698
  }
3197
3699
  var REGION_SLOTS = new Map(
@@ -3200,10 +3702,10 @@ var REGION_SLOTS = new Map(
3200
3702
  var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
3201
3703
  var MAX_REGION_DEPTH = 16;
3202
3704
  function flowNodeLabel(node, index) {
3203
- return strName12(node.label) ?? strName12(node.id) ?? `#${index}`;
3705
+ return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
3204
3706
  }
3205
3707
  function stripRegions(config) {
3206
- if (!isRec9(config)) return void 0;
3708
+ if (!isRec13(config)) return void 0;
3207
3709
  let out;
3208
3710
  for (const key of Object.keys(config)) {
3209
3711
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3214,11 +3716,11 @@ function stripRegions(config) {
3214
3716
  }
3215
3717
  function walkFlowNodes(flow, flowPath) {
3216
3718
  const out = [];
3217
- if (!isRec9(flow)) return out;
3719
+ if (!isRec13(flow)) return out;
3218
3720
  const visitList = (nodes, basePath, trail, depth) => {
3219
3721
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3220
3722
  nodes.forEach((raw, index) => {
3221
- if (!isRec9(raw)) return;
3723
+ if (!isRec13(raw)) return;
3222
3724
  const path = `${basePath}[${index}]`;
3223
3725
  out.push({
3224
3726
  node: raw,
@@ -3227,9 +3729,9 @@ function walkFlowNodes(flow, flowPath) {
3227
3729
  regionTrail: trail,
3228
3730
  depth
3229
3731
  });
3230
- const type = strName12(raw.type);
3732
+ const type = strName14(raw.type);
3231
3733
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3232
- if (!slots || !isRec9(raw.config)) return;
3734
+ if (!slots || !isRec13(raw.config)) return;
3233
3735
  const config = raw.config;
3234
3736
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3235
3737
  for (const slot of slots) {
@@ -3237,8 +3739,8 @@ function walkFlowNodes(flow, flowPath) {
3237
3739
  if (slot === "branches") {
3238
3740
  if (!Array.isArray(value)) continue;
3239
3741
  value.forEach((branch, b) => {
3240
- if (!isRec9(branch)) return;
3241
- const branchName = strName12(branch.name) ?? `#${b}`;
3742
+ if (!isRec13(branch)) return;
3743
+ const branchName = strName14(branch.name) ?? `#${b}`;
3242
3744
  visitList(
3243
3745
  branch.nodes,
3244
3746
  `${path}.config.branches[${b}].nodes`,
@@ -3248,7 +3750,7 @@ function walkFlowNodes(flow, flowPath) {
3248
3750
  });
3249
3751
  continue;
3250
3752
  }
3251
- if (!isRec9(value)) continue;
3753
+ if (!isRec13(value)) continue;
3252
3754
  visitList(
3253
3755
  value.nodes,
3254
3756
  `${path}.config.${slot}.nodes`,
@@ -3268,7 +3770,8 @@ function joinTrail(trail, segment) {
3268
3770
  // src/validate-flow-template-paths.ts
3269
3771
  var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
3270
3772
  var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
3271
- function asArray15(v) {
3773
+ var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
3774
+ function asArray16(v) {
3272
3775
  if (Array.isArray(v)) return v;
3273
3776
  if (v && typeof v === "object") {
3274
3777
  return Object.entries(v).map(([name, def]) => ({
@@ -3297,7 +3800,7 @@ var FILTER_GUARDED_NODE_TYPES = /* @__PURE__ */ new Set([
3297
3800
  ]);
3298
3801
  function fieldTypesOf(obj) {
3299
3802
  const types = /* @__PURE__ */ new Map();
3300
- for (const f of asArray15(obj.fields)) {
3803
+ for (const f of asArray16(obj.fields)) {
3301
3804
  if (typeof f.name === "string") {
3302
3805
  types.set(f.name, typeof f.type === "string" ? f.type : "");
3303
3806
  }
@@ -3394,10 +3897,10 @@ function declaredExpandOf(flow) {
3394
3897
  }
3395
3898
  function validateFlowTemplatePaths(stack) {
3396
3899
  const findings = [];
3397
- const flows = asArray15(stack.flows);
3900
+ const flows = asArray16(stack.flows);
3398
3901
  if (flows.length === 0) return findings;
3399
3902
  const objectsByName = /* @__PURE__ */ new Map();
3400
- for (const obj of asArray15(stack.objects)) {
3903
+ for (const obj of asArray16(stack.objects)) {
3401
3904
  if (typeof obj.name === "string") objectsByName.set(obj.name, obj);
3402
3905
  }
3403
3906
  flows.forEach((flow, flowIndex) => {
@@ -3410,6 +3913,7 @@ function validateFlowTemplatePaths(stack) {
3410
3913
  const obj = objectsByName.get(objectName);
3411
3914
  if (!obj) return;
3412
3915
  const fieldTypes = fieldTypesOf(obj);
3916
+ const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
3413
3917
  const expandSet = declaredExpandOf(flow);
3414
3918
  walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
3415
3919
  const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
@@ -3421,6 +3925,7 @@ function validateFlowTemplatePaths(stack) {
3421
3925
  if (leaves.length === 0) return;
3422
3926
  const seenUnknown = /* @__PURE__ */ new Set();
3423
3927
  const seenTraversal = /* @__PURE__ */ new Set();
3928
+ const seenUnprovisioned = /* @__PURE__ */ new Set();
3424
3929
  for (const leaf of leaves) {
3425
3930
  const inFilter = leaf.inFilter;
3426
3931
  for (const rest of recordRefsIn(leaf.text)) {
@@ -3428,6 +3933,19 @@ function validateFlowTemplatePaths(stack) {
3428
3933
  const hasSubPath = rest.length > 1;
3429
3934
  const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
3430
3935
  const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
3936
+ if (unprovisionedAnchors.has(head)) {
3937
+ if (!seenUnprovisioned.has(head)) {
3938
+ seenUnprovisioned.add(head);
3939
+ findings.push({
3940
+ severity: "warning",
3941
+ rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
3942
+ where,
3943
+ path: nodePath,
3944
+ 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).`),
3945
+ hint: unprovisionedAnchorHint(objectName, head)
3946
+ });
3947
+ }
3948
+ }
3431
3949
  if (!isKnown) {
3432
3950
  if (seenUnknown.has(head)) continue;
3433
3951
  seenUnknown.add(head);
@@ -3466,14 +3984,14 @@ function validateFlowTemplatePaths(stack) {
3466
3984
 
3467
3985
  // src/validate-ai-surface-affinity.ts
3468
3986
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
3469
- function asArray16(v) {
3987
+ function asArray17(v) {
3470
3988
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3471
3989
  if (v && typeof v === "object") {
3472
3990
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3473
3991
  }
3474
3992
  return [];
3475
3993
  }
3476
- function strName13(v) {
3994
+ function strName15(v) {
3477
3995
  return typeof v === "string" && v.length > 0 ? v : void 0;
3478
3996
  }
3479
3997
  function surfaceOf(v) {
@@ -3483,18 +4001,18 @@ function validateAiSurfaceAffinity(stack) {
3483
4001
  const findings = [];
3484
4002
  if (!stack || typeof stack !== "object") return findings;
3485
4003
  const skillsByName = /* @__PURE__ */ new Map();
3486
- for (const skill of asArray16(stack.skills)) {
3487
- const n = strName13(skill.name);
4004
+ for (const skill of asArray17(stack.skills)) {
4005
+ const n = strName15(skill.name);
3488
4006
  if (n) skillsByName.set(n, skill);
3489
4007
  }
3490
- const agents = asArray16(stack.agents);
4008
+ const agents = asArray17(stack.agents);
3491
4009
  for (let ai = 0; ai < agents.length; ai++) {
3492
4010
  const agent = agents[ai];
3493
- const agentName = strName13(agent.name) ?? `#${ai}`;
4011
+ const agentName = strName15(agent.name) ?? `#${ai}`;
3494
4012
  const agentSurface = surfaceOf(agent.surface);
3495
4013
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
3496
4014
  for (let si = 0; si < skillRefs.length; si++) {
3497
- const ref = strName13(skillRefs[si]);
4015
+ const ref = strName15(skillRefs[si]);
3498
4016
  if (!ref) continue;
3499
4017
  const skill = skillsByName.get(ref);
3500
4018
  if (!skill) continue;
@@ -3516,14 +4034,14 @@ function validateAiSurfaceAffinity(stack) {
3516
4034
  // src/validate-ai-tool-references.ts
3517
4035
  var import_system5 = require("@objectstack/spec/system");
3518
4036
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
3519
- function asArray17(v) {
4037
+ function asArray18(v) {
3520
4038
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3521
4039
  if (v && typeof v === "object") {
3522
4040
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3523
4041
  }
3524
4042
  return [];
3525
4043
  }
3526
- function strName14(v) {
4044
+ function strName16(v) {
3527
4045
  return typeof v === "string" && v.length > 0 ? v : void 0;
3528
4046
  }
3529
4047
  function distance6(a, b) {
@@ -3564,26 +4082,26 @@ function materialisesAsTool(action) {
3564
4082
  if (!ai || typeof ai !== "object") return false;
3565
4083
  const aiRec = ai;
3566
4084
  if (aiRec.exposed !== true) return false;
3567
- if (!strName14(aiRec.description)) return false;
3568
- const type = strName14(action.type);
4085
+ if (!strName16(aiRec.description)) return false;
4086
+ const type = strName16(action.type);
3569
4087
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
3570
4088
  if (type === "script") return Boolean(action.target || action.body);
3571
4089
  return Boolean(action.target);
3572
4090
  }
3573
4091
  function collectToolUniverse(stack) {
3574
4092
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
3575
- for (const tool of asArray17(stack.tools)) {
3576
- const n = strName14(tool.name);
4093
+ for (const tool of asArray18(stack.tools)) {
4094
+ const n = strName16(tool.name);
3577
4095
  if (n) universe.add(n);
3578
4096
  }
3579
4097
  const addActionFamily = (actions) => {
3580
- for (const action of asArray17(actions)) {
3581
- const n = strName14(action.name);
4098
+ for (const action of asArray18(actions)) {
4099
+ const n = strName16(action.name);
3582
4100
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
3583
4101
  }
3584
4102
  };
3585
4103
  addActionFamily(stack.actions);
3586
- for (const obj of asArray17(stack.objects)) {
4104
+ for (const obj of asArray18(stack.objects)) {
3587
4105
  addActionFamily(obj.actions);
3588
4106
  }
3589
4107
  return universe;
@@ -3591,13 +4109,13 @@ function collectToolUniverse(stack) {
3591
4109
  function collectUnexposedActionNames(stack) {
3592
4110
  const names = /* @__PURE__ */ new Set();
3593
4111
  const scan = (actions) => {
3594
- for (const action of asArray17(actions)) {
3595
- const n = strName14(action.name);
4112
+ for (const action of asArray18(actions)) {
4113
+ const n = strName16(action.name);
3596
4114
  if (n && !materialisesAsTool(action)) names.add(n);
3597
4115
  }
3598
4116
  };
3599
4117
  scan(stack.actions);
3600
- for (const obj of asArray17(stack.objects)) scan(obj.actions);
4118
+ for (const obj of asArray18(stack.objects)) scan(obj.actions);
3601
4119
  return names;
3602
4120
  }
3603
4121
  function validateAiToolReferences(stack) {
@@ -3615,13 +4133,13 @@ function validateAiToolReferences(stack) {
3615
4133
  }
3616
4134
  return universe.has(ref);
3617
4135
  };
3618
- const skills = asArray17(stack.skills);
4136
+ const skills = asArray18(stack.skills);
3619
4137
  for (let si = 0; si < skills.length; si++) {
3620
4138
  const skill = skills[si];
3621
- const skillName = strName14(skill.name) ?? `#${si}`;
4139
+ const skillName = strName16(skill.name) ?? `#${si}`;
3622
4140
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
3623
4141
  for (let ti = 0; ti < refs.length; ti++) {
3624
- const ref = strName14(refs[ti]);
4142
+ const ref = strName16(refs[ti]);
3625
4143
  if (!ref || resolves(ref)) continue;
3626
4144
  const isPattern = ref.endsWith("*");
3627
4145
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -3640,24 +4158,25 @@ function validateAiToolReferences(stack) {
3640
4158
 
3641
4159
  // src/validate-ai-agent-authoring.ts
3642
4160
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
3643
- function asArray18(v) {
4161
+ var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
4162
+ function asArray19(v) {
3644
4163
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3645
4164
  if (v && typeof v === "object") {
3646
4165
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
3647
4166
  }
3648
4167
  return [];
3649
4168
  }
3650
- function strName15(v) {
4169
+ function strName17(v) {
3651
4170
  return typeof v === "string" && v.length > 0 ? v : void 0;
3652
4171
  }
3653
4172
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
3654
4173
  function validateAiAgentAuthoring(stack) {
3655
4174
  const findings = [];
3656
4175
  if (!stack || typeof stack !== "object") return findings;
3657
- const agents = asArray18(stack.agents);
4176
+ const agents = asArray19(stack.agents);
3658
4177
  for (let ai = 0; ai < agents.length; ai++) {
3659
4178
  const agent = agents[ai];
3660
- const name = strName15(agent.name) ?? `#${ai}`;
4179
+ const name = strName17(agent.name) ?? `#${ai}`;
3661
4180
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
3662
4181
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
3663
4182
  findings.push({
@@ -3669,6 +4188,22 @@ function validateAiAgentAuthoring(stack) {
3669
4188
  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.` : ``)
3670
4189
  });
3671
4190
  }
4191
+ const roster = [...PLATFORM_AGENT_NAMES].join(", ");
4192
+ const apps = asArray19(stack.apps);
4193
+ for (let appIdx = 0; appIdx < apps.length; appIdx++) {
4194
+ const app = apps[appIdx];
4195
+ const defaultAgent = strName17(app.defaultAgent);
4196
+ if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
4197
+ const appName = strName17(app.name) ?? `#${appIdx}`;
4198
+ findings.push({
4199
+ severity: "warning",
4200
+ rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
4201
+ where: `app "${appName}".defaultAgent`,
4202
+ path: `apps[${appIdx}].defaultAgent`,
4203
+ 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.`,
4204
+ 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.`
4205
+ });
4206
+ }
3672
4207
  return findings;
3673
4208
  }
3674
4209
 
@@ -3756,24 +4291,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
3756
4291
  "owner",
3757
4292
  "record_type"
3758
4293
  ]);
3759
- var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3760
- function asArray19(v) {
3761
- if (Array.isArray(v)) return v.filter((x) => isRec10(x));
3762
- if (isRec10(v)) {
4294
+ var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4295
+ function asArray20(v) {
4296
+ if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4297
+ if (isRec14(v)) {
3763
4298
  return Object.entries(v).map(([name, def]) => ({
3764
4299
  name,
3765
- ...isRec10(def) ? def : {}
4300
+ ...isRec14(def) ? def : {}
3766
4301
  }));
3767
4302
  }
3768
4303
  return [];
3769
4304
  }
3770
4305
  function indexObjectFields2(stack) {
3771
4306
  const out = /* @__PURE__ */ new Map();
3772
- for (const obj of asArray19(stack.objects)) {
4307
+ for (const obj of asArray20(stack.objects)) {
3773
4308
  const name = typeof obj.name === "string" ? obj.name : void 0;
3774
4309
  if (!name) continue;
3775
4310
  const names = /* @__PURE__ */ new Set();
3776
- for (const f of asArray19(obj.fields)) {
4311
+ for (const f of asArray20(obj.fields)) {
3777
4312
  if (typeof f.name === "string" && f.name) names.add(f.name);
3778
4313
  }
3779
4314
  out.set(name, names);
@@ -3903,12 +4438,12 @@ ${source}
3903
4438
  }
3904
4439
  function validateHookBodyWrites(stack) {
3905
4440
  const findings = [];
3906
- const hooks = asArray19(stack.hooks);
4441
+ const hooks = asArray20(stack.hooks);
3907
4442
  if (hooks.length === 0) return findings;
3908
4443
  let objectFields = null;
3909
4444
  hooks.forEach((hook, hookIndex) => {
3910
4445
  const body = hook.body;
3911
- if (!isRec10(body) || body.language !== "js") return;
4446
+ if (!isRec14(body) || body.language !== "js") return;
3912
4447
  const source = body.source;
3913
4448
  if (typeof source !== "string" || source.trim() === "") return;
3914
4449
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -3979,13 +4514,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
3979
4514
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
3980
4515
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
3981
4516
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
3982
- var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3983
- function asArray20(v) {
3984
- if (Array.isArray(v)) return v.filter((x) => isRec11(x));
3985
- if (isRec11(v)) {
4517
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4518
+ function asArray21(v) {
4519
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4520
+ if (isRec15(v)) {
3986
4521
  return Object.entries(v).map(([name, def]) => ({
3987
4522
  name,
3988
- ...isRec11(def) ? def : {}
4523
+ ...isRec15(def) ? def : {}
3989
4524
  }));
3990
4525
  }
3991
4526
  return [];
@@ -3999,11 +4534,11 @@ function collectActionBodies(stack) {
3999
4534
  const sites = [];
4000
4535
  const seen = /* @__PURE__ */ new Set();
4001
4536
  const collect = (actions, pathPrefix, parentObject) => {
4002
- asArray20(actions).forEach((action, index) => {
4537
+ asArray21(actions).forEach((action, index) => {
4003
4538
  const type = typeof action.type === "string" ? action.type : "script";
4004
4539
  if (type !== "script") return;
4005
4540
  const body = action.body;
4006
- if (!isRec11(body) || body.language !== "js") return;
4541
+ if (!isRec15(body) || body.language !== "js") return;
4007
4542
  const source = body.source;
4008
4543
  if (typeof source !== "string" || source.trim() === "") return;
4009
4544
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4014,7 +4549,7 @@ function collectActionBodies(stack) {
4014
4549
  });
4015
4550
  };
4016
4551
  collect(stack.actions, "actions");
4017
- asArray20(stack.objects).forEach((obj, objIndex) => {
4552
+ asArray21(stack.objects).forEach((obj, objIndex) => {
4018
4553
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
4019
4554
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
4020
4555
  });
@@ -4022,7 +4557,7 @@ function collectActionBodies(stack) {
4022
4557
  }
4023
4558
  function validateActionBodyWrites(stack) {
4024
4559
  const findings = [];
4025
- if (!isRec11(stack)) return findings;
4560
+ if (!isRec15(stack)) return findings;
4026
4561
  const sites = collectActionBodies(stack);
4027
4562
  if (sites.length === 0) return findings;
4028
4563
  let objectFields = null;
@@ -4080,13 +4615,13 @@ function fixHint2(field, declared) {
4080
4615
  var import_shared3 = require("@objectstack/spec/shared");
4081
4616
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
4082
4617
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4083
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4084
- function asArray21(v) {
4085
- if (Array.isArray(v)) return v.filter((x) => isRec12(x));
4086
- if (isRec12(v)) {
4618
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4619
+ function asArray22(v) {
4620
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4621
+ if (isRec16(v)) {
4087
4622
  return Object.entries(v).map(([name, def]) => ({
4088
4623
  name,
4089
- ...isRec12(def) ? def : {}
4624
+ ...isRec16(def) ? def : {}
4090
4625
  }));
4091
4626
  }
4092
4627
  return [];
@@ -4099,8 +4634,8 @@ function readLiteralObjectName(config) {
4099
4634
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4100
4635
  function validateFlowNodeWrites(stack) {
4101
4636
  const findings = [];
4102
- if (!isRec12(stack)) return findings;
4103
- const flows = asArray21(stack.flows);
4637
+ if (!isRec16(stack)) return findings;
4638
+ const flows = asArray22(stack.flows);
4104
4639
  if (flows.length === 0) return findings;
4105
4640
  let objectFields = null;
4106
4641
  flows.forEach((flow, flowIndex) => {
@@ -4108,10 +4643,10 @@ function validateFlowNodeWrites(stack) {
4108
4643
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4109
4644
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4110
4645
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4111
- const config = isRec12(node.config) ? node.config : void 0;
4646
+ const config = isRec16(node.config) ? node.config : void 0;
4112
4647
  if (!config) return;
4113
4648
  const fields = config.fields;
4114
- if (!isRec12(fields)) return;
4649
+ if (!isRec16(fields)) return;
4115
4650
  const written = Object.keys(fields);
4116
4651
  if (written.length === 0) return;
4117
4652
  const objectName = readLiteralObjectName(config);
@@ -4145,7 +4680,7 @@ function fixHint3(field, declared) {
4145
4680
  // src/validate-readonly-flow-writes.ts
4146
4681
  var FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
4147
4682
  var FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
4148
- function asArray22(v) {
4683
+ function asArray23(v) {
4149
4684
  if (Array.isArray(v)) return v;
4150
4685
  if (v && typeof v === "object") {
4151
4686
  return Object.entries(v).map(([name, def]) => ({
@@ -4186,9 +4721,9 @@ function readLiteralObjectName2(config) {
4186
4721
  }
4187
4722
  function validateReadonlyFlowWrites(stack) {
4188
4723
  const findings = [];
4189
- const flows = asArray22(stack.flows);
4724
+ const flows = asArray23(stack.flows);
4190
4725
  if (flows.length === 0) return findings;
4191
- const roIndex = buildReadonlyIndex(asArray22(stack.objects));
4726
+ const roIndex = buildReadonlyIndex(asArray23(stack.objects));
4192
4727
  flows.forEach((flow, flowIndex) => {
4193
4728
  if (flow.runAs === "system") return;
4194
4729
  const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
@@ -4236,14 +4771,14 @@ function validateReadonlyFlowWrites(stack) {
4236
4771
  // src/validate-react-page-props.ts
4237
4772
  var import_node_module2 = require("module");
4238
4773
  var import_ui2 = require("@objectstack/spec/ui");
4239
- var import_data5 = require("@objectstack/spec/data");
4774
+ var import_data8 = require("@objectstack/spec/data");
4240
4775
 
4241
4776
  // src/zod-issue-format.ts
4242
- var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4777
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4243
4778
  var valueAtPath = (root, path) => {
4244
4779
  let cur = root;
4245
4780
  for (const key of path) {
4246
- if (!isRec13(cur) && !Array.isArray(cur)) return void 0;
4781
+ if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
4247
4782
  cur = cur[key];
4248
4783
  }
4249
4784
  return cur;
@@ -4289,7 +4824,7 @@ function loadTypeScript2() {
4289
4824
  }
4290
4825
  return cachedTs2;
4291
4826
  }
4292
- var asArray23 = (v) => Array.isArray(v) ? v : [];
4827
+ var asArray24 = (v) => Array.isArray(v) ? v : [];
4293
4828
  var BLOCKS = new Map(
4294
4829
  import_ui2.REACT_BLOCKS.map((b) => [
4295
4830
  b.tag,
@@ -4377,12 +4912,13 @@ function filterAttrValue(tsc, sf, attr) {
4377
4912
  return perPosition(init.expression);
4378
4913
  }
4379
4914
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
4915
+ var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
4380
4916
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
4381
4917
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4382
4918
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4383
4919
  function checkChartDrillDown(raw, push2) {
4384
4920
  if (raw === void 0 || raw === NOT_STATIC) return;
4385
- if (!isRec14(raw)) {
4921
+ if (!isRec18(raw)) {
4386
4922
  push2(
4387
4923
  "error",
4388
4924
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4405,7 +4941,7 @@ function checkChartDrillDown(raw, push2) {
4405
4941
  }
4406
4942
  function checkChartAggregate(raw, push2) {
4407
4943
  if (raw === void 0 || raw === NOT_STATIC) return;
4408
- if (!isRec14(raw)) {
4944
+ if (!isRec18(raw)) {
4409
4945
  push2(
4410
4946
  "error",
4411
4947
  REACT_CHART_AGGREGATE_INVALID,
@@ -4420,7 +4956,7 @@ function checkChartAggregate(raw, push2) {
4420
4956
  "warning",
4421
4957
  REACT_CHART_AGGREGATE_INVALID,
4422
4958
  "aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
4423
- "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."
4959
+ "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."
4424
4960
  );
4425
4961
  }
4426
4962
  const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
@@ -4436,9 +4972,9 @@ function checkChartAggregate(raw, push2) {
4436
4972
  );
4437
4973
  }
4438
4974
  }
4439
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4975
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4440
4976
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4441
- function checkObjectChart(attrs, objectFields, findings) {
4977
+ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
4442
4978
  const { values, where, path } = attrs;
4443
4979
  const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
4444
4980
  checkChartDrillDown(values.get("drillDown"), push2);
@@ -4446,18 +4982,29 @@ function checkObjectChart(attrs, objectFields, findings) {
4446
4982
  const aggregate = values.get("aggregate");
4447
4983
  checkChartAggregate(aggregate, push2);
4448
4984
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4449
- if (!isRec14(aggregate)) return;
4985
+ if (!isRec18(aggregate)) return;
4450
4986
  const fn = strOf(aggregate.function);
4451
4987
  const field = strOf(aggregate.field);
4452
4988
  const groupBy = aggregate.groupBy;
4453
- const groupByField = strOf(groupBy) ?? (isRec14(groupBy) ? strOf(groupBy.field) : void 0);
4989
+ const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
4454
4990
  const objectName = strOf(values.get("objectName"));
4455
4991
  const known = objectName ? objectFields.get(objectName) : void 0;
4456
4992
  if (objectName && known) {
4993
+ const anchors = unprovisionedAnchors.get(objectName);
4457
4994
  const fieldRef = (name, prop) => {
4458
4995
  if (!name) return;
4459
4996
  if (name.includes(".")) return;
4460
- if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
4997
+ if (known.has(name) || SYSTEM_FIELDS.has(name)) {
4998
+ if (anchors?.has(name)) {
4999
+ push2(
5000
+ "warning",
5001
+ REACT_CHART_FIELD_UNPROVISIONED,
5002
+ `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.`,
5003
+ unprovisionedAnchorHint(objectName, name)
5004
+ );
5005
+ }
5006
+ return;
5007
+ }
4461
5008
  push2(
4462
5009
  "error",
4463
5010
  REACT_CHART_FIELD_UNKNOWN,
@@ -4483,18 +5030,18 @@ function checkObjectChart(attrs, objectFields, findings) {
4483
5030
  );
4484
5031
  };
4485
5032
  const xAxisRaw = values.get("xAxis");
4486
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec14(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5033
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
4487
5034
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
4488
5035
  axisRef(categoryAxis, categoryProp);
4489
5036
  const yAxisRaw = values.get("yAxis");
4490
5037
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
4491
5038
  for (const a of yAxisList) {
4492
- axisRef(strOf(a) ?? (isRec14(a) ? strOf(a.field) : void 0), "yAxis[].field");
5039
+ axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
4493
5040
  }
4494
5041
  const series = values.get("series");
4495
5042
  if (Array.isArray(series)) {
4496
5043
  for (const s of series) {
4497
- if (!isRec14(s)) continue;
5044
+ if (!isRec18(s)) continue;
4498
5045
  const dataKey = strOf(s.dataKey);
4499
5046
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
4500
5047
  }
@@ -4550,7 +5097,7 @@ function subformFieldRefs(value, basePath) {
4550
5097
  if (!Array.isArray(value)) return { child, parent };
4551
5098
  for (let i = 0; i < value.length; i++) {
4552
5099
  const sub = value[i];
4553
- if (!isRec14(sub)) continue;
5100
+ if (!isRec18(sub)) continue;
4554
5101
  const at = (key) => `${basePath}[${i}].${key}`;
4555
5102
  child.push({
4556
5103
  objectName: strOf(sub.childObject),
@@ -4575,7 +5122,7 @@ function filterFieldRefs(node, basePath, out) {
4575
5122
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
4576
5123
  return;
4577
5124
  }
4578
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data5.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5125
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
4579
5126
  out.push({ name: head, path: `${basePath}[0]` });
4580
5127
  }
4581
5128
  }
@@ -4595,20 +5142,20 @@ function reactFieldRefs(spec, values, basePath) {
4595
5142
  }
4596
5143
  for (const key of spec.nestedFields ?? []) {
4597
5144
  const v = readable(key);
4598
- if (isRec14(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5145
+ if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
4599
5146
  }
4600
5147
  for (const key of spec.sections ?? []) {
4601
5148
  const v = readable(key);
4602
5149
  if (!Array.isArray(v)) continue;
4603
5150
  for (let i = 0; i < v.length; i++) {
4604
5151
  const section = v[i];
4605
- if (!isRec14(section)) continue;
5152
+ if (!isRec18(section)) continue;
4606
5153
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
4607
5154
  }
4608
5155
  }
4609
5156
  for (const key of spec.keyedByField ?? []) {
4610
5157
  const v = readable(key);
4611
- if (!isRec14(v)) continue;
5158
+ if (!isRec18(v)) continue;
4612
5159
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
4613
5160
  }
4614
5161
  for (const key of spec.filterArrays ?? []) {
@@ -4616,22 +5163,30 @@ function reactFieldRefs(spec, values, basePath) {
4616
5163
  }
4617
5164
  return { own, queried };
4618
5165
  }
4619
- function checkBlockFieldProps(tag, values, objectFields, where, path) {
5166
+ function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
4620
5167
  const objectName = strOf(values.get("objectName"));
4621
5168
  const out = [];
4622
5169
  const spec = REACT_FIELD_SPECS[tag];
4623
5170
  if (spec) {
4624
5171
  const { own, queried } = reactFieldRefs(spec, values, path);
4625
- out.push(...checkFieldRefs(own, objectName, objectFields, where));
4626
- out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
5172
+ out.push(
5173
+ ...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
5174
+ );
5175
+ out.push(
5176
+ ...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
5177
+ );
4627
5178
  }
4628
5179
  if (tag === "ObjectForm") {
4629
5180
  const raw = values.get("subforms");
4630
5181
  const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
4631
5182
  for (const sub of subs.child) {
4632
- out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
5183
+ out.push(
5184
+ ...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
5185
+ );
4633
5186
  }
4634
- out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
5187
+ out.push(
5188
+ ...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
5189
+ );
4635
5190
  }
4636
5191
  const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
4637
5192
  if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
@@ -4640,7 +5195,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
4640
5195
  componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
4641
5196
  objectName,
4642
5197
  objectFields,
4643
- where
5198
+ where,
5199
+ "skipped",
5200
+ unprovisionedAnchors
4644
5201
  )
4645
5202
  );
4646
5203
  }
@@ -4672,8 +5229,9 @@ function localComponentNames(tsc, sf) {
4672
5229
  function validateReactPageProps(stack) {
4673
5230
  const findings = [];
4674
5231
  const objectFields = indexObjectFields(stack);
5232
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
4675
5233
  const searchTargets = indexObjectSearchTargets(stack);
4676
- const pages = asArray23(stack.pages);
5234
+ const pages = asArray24(stack.pages);
4677
5235
  for (let p = 0; p < pages.length; p++) {
4678
5236
  const page = pages[p];
4679
5237
  if (!page || page.kind !== "react") continue;
@@ -4754,7 +5312,7 @@ function validateReactPageProps(stack) {
4754
5312
  }
4755
5313
  }
4756
5314
  if (tag === "ObjectChart" && !hasSpread) {
4757
- checkObjectChart({ values, where, path }, objectFields, findings);
5315
+ checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
4758
5316
  }
4759
5317
  if (tag === "ListView" && !hasSpread) {
4760
5318
  findings.push(
@@ -4770,7 +5328,7 @@ function validateReactPageProps(stack) {
4770
5328
  }
4771
5329
  if (!hasSpread) {
4772
5330
  findings.push(
4773
- ...checkBlockFieldProps(tag, values, objectFields, where, path)
5331
+ ...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
4774
5332
  );
4775
5333
  }
4776
5334
  }
@@ -4797,6 +5355,15 @@ var REFERENCE_INTEGRITY_RULES = [
4797
5355
  // `action` is deliberately absent (validateActionNameRefs owns it) and so is
4798
5356
  // `component` (an unregistered ref renders a named diagnostic, not silence).
4799
5357
  { name: "validateNavTargetRefs", run: validateNavTargetRefs },
5358
+ // [#7912] The THIRD question about a nav entry, after "does the target
5359
+ // resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
5360
+ // destination serve at all? An object's own `enable` block can make its list
5361
+ // answer 404/405 for every persona, and no gate authorable on the entry
5362
+ // expresses that — which is how #7544's dead row survived review for a year.
5363
+ // The server now prunes such an entry from the `/meta` payload; the
5364
+ // maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
5365
+ // prune is never silent to the author who wrote the row.
5366
+ { name: "validateNavObjectServability", run: validateNavObjectServability },
4800
5367
  { name: "validateTranslationReferences", run: validateTranslationReferences },
4801
5368
  // The same family from the other end (#5417). Its sibling above asks "does
4802
5369
  // this bundle key resolve?"; this one asks "is there a key at all?" — a form
@@ -4892,16 +5459,16 @@ function validateReferenceIntegrity(stack) {
4892
5459
 
4893
5460
  // src/validate-component-props.ts
4894
5461
  var import_ui3 = require("@objectstack/spec/ui");
4895
- var import_spec = require("@objectstack/spec");
5462
+ var import_spec2 = require("@objectstack/spec");
4896
5463
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
4897
5464
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
4898
- function isRec15(v) {
5465
+ function isRec19(v) {
4899
5466
  return !!v && typeof v === "object" && !Array.isArray(v);
4900
5467
  }
4901
- function strName16(v) {
5468
+ function strName18(v) {
4902
5469
  return typeof v === "string" && v.length > 0 ? v : void 0;
4903
5470
  }
4904
- function asArray24(v) {
5471
+ function asArray25(v) {
4905
5472
  if (Array.isArray(v)) return v;
4906
5473
  if (v && typeof v === "object") {
4907
5474
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4912,27 +5479,40 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
4912
5479
  var DATASOURCE_SUPPLIED_PROP = "object";
4913
5480
  function suppliedByDataSource(issue, component) {
4914
5481
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
4915
- const dataSource = isRec15(component.dataSource) ? component.dataSource : void 0;
4916
- return strName16(dataSource?.object) !== void 0;
5482
+ const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
5483
+ return strName18(dataSource?.object) !== void 0;
5484
+ }
5485
+ function unrecognizedKeysFromUnionArm(issue) {
5486
+ if (issue.code !== "invalid_union") return void 0;
5487
+ const arms = issue.errors;
5488
+ if (!arms || arms.length === 0) return void 0;
5489
+ let found;
5490
+ for (const arm of arms) {
5491
+ const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
5492
+ if (keyIssues.length === 0) continue;
5493
+ if (keyIssues.length !== arm.length || found) return void 0;
5494
+ found = keyIssues[0];
5495
+ }
5496
+ return found;
4917
5497
  }
4918
5498
  function validateComponentProps(stack) {
4919
5499
  const findings = [];
4920
- if (!isRec15(stack)) return findings;
4921
- const pages = asArray24(stack.pages);
5500
+ if (!isRec19(stack)) return findings;
5501
+ const pages = asArray25(stack.pages);
4922
5502
  for (let pi = 0; pi < pages.length; pi++) {
4923
5503
  const page = pages[pi];
4924
- if (!isRec15(page)) continue;
4925
- const pageName = strName16(page.name) ?? `#${pi}`;
5504
+ if (!isRec19(page)) continue;
5505
+ const pageName = strName18(page.name) ?? `#${pi}`;
4926
5506
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4927
- const type = strName16(component.type);
5507
+ const type = strName18(component.type);
4928
5508
  if (!type) continue;
4929
5509
  const schema = PROPS_SCHEMAS[type];
4930
5510
  if (!schema) continue;
4931
- const props = isRec15(component.properties) ? component.properties : void 0;
5511
+ const props = isRec19(component.properties) ? component.properties : void 0;
4932
5512
  if (!props) continue;
4933
5513
  const where = `page "${pageName}" \xB7 ${type}`;
4934
5514
  const base = `${path}.properties`;
4935
- for (const f of (0, import_spec.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
5515
+ for (const f of (0, import_spec2.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
4936
5516
  findings.push({
4937
5517
  severity: "warning",
4938
5518
  rule: COMPONENT_PROPS_UNKNOWN_KEY,
@@ -4947,6 +5527,20 @@ function validateComponentProps(stack) {
4947
5527
  for (const issue of parsed.error?.issues ?? []) {
4948
5528
  if (suppliedByDataSource(issue, component)) continue;
4949
5529
  const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
5530
+ const armIssue = unrecognizedKeysFromUnionArm(issue);
5531
+ if (armIssue) {
5532
+ for (const key of armIssue.keys ?? []) {
5533
+ findings.push({
5534
+ severity: "warning",
5535
+ rule: COMPONENT_PROPS_UNKNOWN_KEY,
5536
+ where,
5537
+ path: `${at}.${key}`,
5538
+ message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
5539
+ hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
5540
+ });
5541
+ }
5542
+ continue;
5543
+ }
4950
5544
  if (issue.code === "unrecognized_keys") {
4951
5545
  for (const key of issue.keys ?? []) {
4952
5546
  findings.push({
@@ -5177,7 +5771,7 @@ function looksLikeTailwind(className) {
5177
5771
  return false;
5178
5772
  });
5179
5773
  }
5180
- function asArray25(v) {
5774
+ function asArray26(v) {
5181
5775
  if (Array.isArray(v)) return v;
5182
5776
  if (v && typeof v === "object") {
5183
5777
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5270,13 +5864,13 @@ function checkNode(node, pageName, path, findings) {
5270
5864
  }
5271
5865
  function validateResponsiveStyles(stack) {
5272
5866
  const findings = [];
5273
- const pages = asArray25(stack.pages);
5867
+ const pages = asArray26(stack.pages);
5274
5868
  for (let p = 0; p < pages.length; p++) {
5275
5869
  const page = pages[p];
5276
5870
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
5277
- const regions = asArray25(page.regions);
5871
+ const regions = asArray26(page.regions);
5278
5872
  for (let r = 0; r < regions.length; r++) {
5279
- const components = asArray25(regions[r].components);
5873
+ const components = asArray26(regions[r].components);
5280
5874
  for (let c = 0; c < components.length; c++) {
5281
5875
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
5282
5876
  }
@@ -5287,10 +5881,10 @@ function validateResponsiveStyles(stack) {
5287
5881
 
5288
5882
  // src/validate-jsx-pages.ts
5289
5883
  var import_sdui_parser = require("@objectstack/sdui-parser");
5290
- var asArray26 = (v) => Array.isArray(v) ? v : [];
5884
+ var asArray27 = (v) => Array.isArray(v) ? v : [];
5291
5885
  function validateJsxPages(stack, opts = {}) {
5292
5886
  const findings = [];
5293
- const pages = asArray26(stack.pages);
5887
+ const pages = asArray27(stack.pages);
5294
5888
  for (let p = 0; p < pages.length; p++) {
5295
5889
  const page = pages[p];
5296
5890
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -5338,10 +5932,10 @@ function loadSucraseTransform() {
5338
5932
  }
5339
5933
  return cachedTransform;
5340
5934
  }
5341
- var asArray27 = (v) => Array.isArray(v) ? v : [];
5935
+ var asArray28 = (v) => Array.isArray(v) ? v : [];
5342
5936
  function validateReactPages(stack) {
5343
5937
  const findings = [];
5344
- const pages = asArray27(stack.pages);
5938
+ const pages = asArray28(stack.pages);
5345
5939
  for (let p = 0; p < pages.length; p++) {
5346
5940
  const page = pages[p];
5347
5941
  if (!page || page.kind !== "react") continue;
@@ -5378,11 +5972,11 @@ function validateReactPages(stack) {
5378
5972
 
5379
5973
  // src/validate-page-source-styling.ts
5380
5974
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
5381
- var asArray28 = (v) => Array.isArray(v) ? v : [];
5975
+ var asArray29 = (v) => Array.isArray(v) ? v : [];
5382
5976
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
5383
5977
  function validatePageSourceStyling(stack) {
5384
5978
  const findings = [];
5385
- const pages = asArray28(stack.pages);
5979
+ const pages = asArray29(stack.pages);
5386
5980
  for (let p = 0; p < pages.length; p++) {
5387
5981
  const page = pages[p];
5388
5982
  if (!page) continue;
@@ -5410,7 +6004,7 @@ function validatePageSourceStyling(stack) {
5410
6004
  // src/validate-capability-references.ts
5411
6005
  var import_security = require("@objectstack/spec/security");
5412
6006
  var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
5413
- function asArray29(v) {
6007
+ function asArray30(v) {
5414
6008
  if (Array.isArray(v)) return v;
5415
6009
  if (v && typeof v === "object") {
5416
6010
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5435,13 +6029,13 @@ function validateCapabilityReferences(stack) {
5435
6029
  const findings = [];
5436
6030
  if (!stack || typeof stack !== "object") return findings;
5437
6031
  const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
5438
- for (const cap of asArray29(stack.capabilities)) {
6032
+ for (const cap of asArray30(stack.capabilities)) {
5439
6033
  if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
5440
6034
  }
5441
- for (const ps of asArray29(stack.permissions)) {
6035
+ for (const ps of asArray30(stack.permissions)) {
5442
6036
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
5443
6037
  }
5444
- for (const seed of asArray29(stack.data)) {
6038
+ for (const seed of asArray30(stack.data)) {
5445
6039
  if (seed.object !== "sys_capability") continue;
5446
6040
  for (const rec of Array.isArray(seed.records) ? seed.records : []) {
5447
6041
  const name = rec?.name;
@@ -5460,7 +6054,7 @@ function validateCapabilityReferences(stack) {
5460
6054
  hint
5461
6055
  });
5462
6056
  };
5463
- const objects = asArray29(stack.objects);
6057
+ const objects = asArray30(stack.objects);
5464
6058
  for (let i = 0; i < objects.length; i++) {
5465
6059
  const obj = objects[i];
5466
6060
  if (!obj || typeof obj !== "object") continue;
@@ -5469,27 +6063,27 @@ function validateCapabilityReferences(stack) {
5469
6063
  for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
5470
6064
  flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
5471
6065
  }
5472
- const fields = asArray29(obj.fields);
6066
+ const fields = asArray30(obj.fields);
5473
6067
  for (const f of fields) {
5474
6068
  const fname = typeof f.name === "string" ? f.name : "(field)";
5475
6069
  for (const cap of asCapArray(f.requiredPermissions)) {
5476
6070
  flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
5477
6071
  }
5478
6072
  }
5479
- for (const [ai, action] of asArray29(obj.actions).entries()) {
6073
+ for (const [ai, action] of asArray30(obj.actions).entries()) {
5480
6074
  const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
5481
6075
  for (const cap of asCapArray(action.requiredPermissions)) {
5482
6076
  flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
5483
6077
  }
5484
6078
  }
5485
6079
  }
5486
- for (const [i, action] of asArray29(stack.actions).entries()) {
6080
+ for (const [i, action] of asArray30(stack.actions).entries()) {
5487
6081
  const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
5488
6082
  for (const cap of asCapArray(action.requiredPermissions)) {
5489
6083
  flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
5490
6084
  }
5491
6085
  }
5492
- const apps = asArray29(stack.apps);
6086
+ const apps = asArray30(stack.apps);
5493
6087
  for (let i = 0; i < apps.length; i++) {
5494
6088
  const app = apps[i];
5495
6089
  if (!app || typeof app !== "object") continue;
@@ -5522,8 +6116,9 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
5522
6116
  var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
5523
6117
  var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
5524
6118
  var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
6119
+ var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
5525
6120
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
5526
- function asArray30(v) {
6121
+ function asArray31(v) {
5527
6122
  if (Array.isArray(v)) return v;
5528
6123
  if (v && typeof v === "object") {
5529
6124
  return Object.entries(v).map(([name, def]) => ({
@@ -5539,6 +6134,11 @@ function renderNonObject(v) {
5539
6134
  if (t === "bigint") return `${String(v)}n (a bigint)`;
5540
6135
  return `a ${t}`;
5541
6136
  }
6137
+ function renderTriggerToken(v) {
6138
+ if (typeof v === "string") return `'${v}'`;
6139
+ const json = JSON.stringify(v);
6140
+ return json === void 0 ? `a ${typeof v}` : json;
6141
+ }
5542
6142
  function startNodeOf(flow) {
5543
6143
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
5544
6144
  const index = nodes.findIndex((n) => n?.type === "start");
@@ -5546,10 +6146,10 @@ function startNodeOf(flow) {
5546
6146
  }
5547
6147
  function validateFlowTriggerReadiness(stack) {
5548
6148
  const findings = [];
5549
- const flows = asArray30(stack.flows);
6149
+ const flows = asArray31(stack.flows);
5550
6150
  if (flows.length === 0) return findings;
5551
6151
  const objectNames = new Set(
5552
- asArray30(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
6152
+ asArray31(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
5553
6153
  );
5554
6154
  flows.forEach((flow, flowIndex) => {
5555
6155
  const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
@@ -5658,6 +6258,25 @@ function validateFlowTriggerReadiness(stack) {
5658
6258
  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.`
5659
6259
  });
5660
6260
  }
6261
+ const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
6262
+ if (start && flow.type === "record_change" && !routesToSomeTrigger) {
6263
+ const hasTriggerType = config.triggerType != null;
6264
+ findings.push({
6265
+ // `error` (#5762's criterion, applied to a fourth id). The verdict is
6266
+ // the engine's own routing chain — literal `startsWith`/`typeof` tests
6267
+ // with no registry lookup in them — so no installed package can make
6268
+ // this token resolve. `registerTrigger` is keyed by the RESOLVED type,
6269
+ // which is the near-miss worth stating: a plugin can supply the
6270
+ // record-change trigger itself, and it still would not help, because
6271
+ // the flow never reaches the point of asking for one.
6272
+ severity: "error",
6273
+ rule: FLOW_TRIGGER_UNROUTABLE,
6274
+ where: `flow "${flowName}" \u203A start node`,
6275
+ path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
6276
+ 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.`,
6277
+ 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.`
6278
+ });
6279
+ }
5661
6280
  if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
5662
6281
  findings.push({
5663
6282
  severity: "warning",
@@ -5674,7 +6293,7 @@ function validateFlowTriggerReadiness(stack) {
5674
6293
 
5675
6294
  // src/validate-approval-approvers.ts
5676
6295
  var import_automation4 = require("@objectstack/spec/automation");
5677
- var import_spec2 = require("@objectstack/spec");
6296
+ var import_spec3 = require("@objectstack/spec");
5678
6297
  var import_formula3 = require("@objectstack/formula");
5679
6298
  var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
5680
6299
  var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
@@ -5689,13 +6308,13 @@ var APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED = "approval-approver-cross-org-unsup
5689
6308
  var EXPRESSION_ROOTS = /* @__PURE__ */ new Set(["current", "trigger", "vars"]);
5690
6309
  var RESERVED_OUTPUT_KEYS = /* @__PURE__ */ new Set(["decision", "requestId"]);
5691
6310
  var GROUP_ROUTED_TYPES = /* @__PURE__ */ new Set(["position", "team", "department"]);
5692
- var MEMBERSHIP_TIERS = new Set(import_spec2.BUILTIN_MEMBERSHIP_ROLES);
5693
- var MEMBERSHIP_TIER_LIST = import_spec2.BUILTIN_MEMBERSHIP_ROLES.join("/");
6311
+ var MEMBERSHIP_TIERS = new Set(import_spec3.BUILTIN_MEMBERSHIP_ROLES);
6312
+ var MEMBERSHIP_TIER_LIST = import_spec3.BUILTIN_MEMBERSHIP_ROLES.join("/");
5694
6313
  var TYPE_FIX = {
5695
6314
  business_unit: "department",
5696
6315
  bu: "department"
5697
6316
  };
5698
- function asArray31(v) {
6317
+ function asArray32(v) {
5699
6318
  if (Array.isArray(v)) return v;
5700
6319
  if (v && typeof v === "object") {
5701
6320
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5705,7 +6324,7 @@ function asArray31(v) {
5705
6324
  function validateApprovalApprovers(stack) {
5706
6325
  const findings = [];
5707
6326
  if (!stack || typeof stack !== "object") return findings;
5708
- const flows = asArray31(stack.flows);
6327
+ const flows = asArray32(stack.flows);
5709
6328
  const validTypes = new Set(import_automation4.ApproverType.options);
5710
6329
  for (let fi = 0; fi < flows.length; fi++) {
5711
6330
  const flow = flows[fi];
@@ -5885,10 +6504,10 @@ function validateApprovalApprovers(stack) {
5885
6504
  }
5886
6505
 
5887
6506
  // src/validate-record-title.ts
5888
- var import_data6 = require("@objectstack/spec/data");
6507
+ var import_data9 = require("@objectstack/spec/data");
5889
6508
  var TITLE_FORMAT_RETIRED = "title-format-retired";
5890
6509
  var TITLE_UNRESOLVABLE = "title-unresolvable";
5891
- function asArray32(v) {
6510
+ function asArray33(v) {
5892
6511
  if (Array.isArray(v)) return v;
5893
6512
  if (v && typeof v === "object") {
5894
6513
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5897,7 +6516,7 @@ function asArray32(v) {
5897
6516
  }
5898
6517
  function validateRecordTitle(stack) {
5899
6518
  const findings = [];
5900
- const objects = asArray32(stack.objects);
6519
+ const objects = asArray33(stack.objects);
5901
6520
  for (let i = 0; i < objects.length; i++) {
5902
6521
  const obj = objects[i];
5903
6522
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -5913,7 +6532,7 @@ function validateRecordTitle(stack) {
5913
6532
  hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
5914
6533
  });
5915
6534
  }
5916
- const completeness = (0, import_data6.objectTitleCompleteness)(obj);
6535
+ const completeness = (0, import_data9.objectTitleCompleteness)(obj);
5917
6536
  if (completeness.status === "none") {
5918
6537
  findings.push({
5919
6538
  severity: "warning",
@@ -5933,7 +6552,8 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
5933
6552
  var FIELD_GROUP_EMPTY = "field-group-empty";
5934
6553
  var FIELD_GROUP_SHADOWED = "field-group-shadowed";
5935
6554
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
5936
- function asArray33(v) {
6555
+ var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
6556
+ function asArray34(v) {
5937
6557
  if (Array.isArray(v)) return v;
5938
6558
  if (v && typeof v === "object") {
5939
6559
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -5942,7 +6562,7 @@ function asArray33(v) {
5942
6562
  }
5943
6563
  function validateSemanticRoles(stack) {
5944
6564
  const findings = [];
5945
- const objects = asArray33(stack.objects);
6565
+ const objects = asArray34(stack.objects);
5946
6566
  for (let i = 0; i < objects.length; i++) {
5947
6567
  const obj = objects[i];
5948
6568
  if (!obj || typeof obj !== "object") continue;
@@ -5951,6 +6571,15 @@ function validateSemanticRoles(stack) {
5951
6571
  const path = `objects[${i}]`;
5952
6572
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
5953
6573
  const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
6574
+ const unprovisioned = unprovisionedInjectedColumnsFor(obj);
6575
+ const unprovisionedPointer = (slot, entry) => ({
6576
+ severity: "warning",
6577
+ rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
6578
+ where,
6579
+ path: `${path}.${slot}`,
6580
+ 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.`,
6581
+ 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.`
6582
+ });
5954
6583
  const declaredGroups = new Set(
5955
6584
  (Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
5956
6585
  );
@@ -5992,10 +6621,16 @@ function validateSemanticRoles(stack) {
5992
6621
  message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
5993
6622
  hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
5994
6623
  });
6624
+ } else if (typeof stage === "string" && unprovisioned.has(stage)) {
6625
+ findings.push(unprovisionedPointer("stageField", stage));
5995
6626
  }
5996
6627
  const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
5997
6628
  for (const entry of highlights) {
5998
- if (typeof entry !== "string" || entry.length === 0 || fieldNames.has(entry)) continue;
6629
+ if (typeof entry !== "string" || entry.length === 0) continue;
6630
+ if (fieldNames.has(entry)) {
6631
+ if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
6632
+ continue;
6633
+ }
5999
6634
  findings.push({
6000
6635
  severity: "warning",
6001
6636
  rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -6009,7 +6644,7 @@ function validateSemanticRoles(stack) {
6009
6644
  (h) => typeof h === "string" && h.length > 0
6010
6645
  );
6011
6646
  if (declaredStrings.length > 0 && declaredGroups.size > 0) {
6012
- const declaredTitle = [obj.nameField, obj.primaryField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6647
+ const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6013
6648
  const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
6014
6649
  const stripSet = new Set(
6015
6650
  declaredStrings.filter((h) => h !== titleField).slice(0, 4)
@@ -6037,13 +6672,19 @@ function validateSemanticRoles(stack) {
6037
6672
  // src/validate-form-layout.ts
6038
6673
  var FORM_FIELD_UNKNOWN = "form-field-unknown";
6039
6674
  var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
6040
- function asArray34(v) {
6675
+ function asArray35(v) {
6041
6676
  if (Array.isArray(v)) return v;
6042
6677
  if (v && typeof v === "object") {
6043
6678
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6044
6679
  }
6045
6680
  return [];
6046
6681
  }
6682
+ function isRec20(v) {
6683
+ return !!v && typeof v === "object" && !Array.isArray(v);
6684
+ }
6685
+ function strName19(v) {
6686
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6687
+ }
6047
6688
  function fieldNameOf(entry) {
6048
6689
  if (typeof entry === "string") return entry.length > 0 ? entry : null;
6049
6690
  if (entry && typeof entry === "object" && !Array.isArray(entry)) {
@@ -6052,60 +6693,53 @@ function fieldNameOf(entry) {
6052
6693
  }
6053
6694
  return null;
6054
6695
  }
6055
- function boundObject(view) {
6056
- const data = view.data;
6057
- if (data && typeof data === "object" && typeof data.object === "string") {
6058
- return data.object;
6059
- }
6060
- return typeof view.objectName === "string" ? view.objectName : void 0;
6061
- }
6062
6696
  function validateFormLayout(stack) {
6063
6697
  const findings = [];
6064
6698
  const objectFields = /* @__PURE__ */ new Map();
6065
- for (const obj of asArray34(stack.objects)) {
6699
+ for (const obj of asArray35(stack.objects)) {
6066
6700
  const name = typeof obj.name === "string" ? obj.name : void 0;
6067
6701
  if (!name) continue;
6068
6702
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
6069
6703
  objectFields.set(name, new Set(fields));
6070
6704
  }
6071
- const views = asArray34(stack.views);
6072
- for (let i = 0; i < views.length; i++) {
6073
- const view = views[i];
6074
- if (!view || typeof view !== "object") continue;
6075
- const sections = Array.isArray(view.sections) ? view.sections : null;
6076
- if (!sections) continue;
6077
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6078
- const objName = boundObject(view);
6079
- const known = objName ? objectFields.get(objName) : void 0;
6080
- const where = `view "${viewName}"`;
6081
- const base = `views[${i}]`;
6082
- for (let s = 0; s < sections.length; s++) {
6083
- const sec = sections[s];
6084
- const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
6085
- for (let f = 0; f < secFields.length; f++) {
6086
- const entry = secFields[f];
6087
- const fname = fieldNameOf(entry);
6088
- const fpath = `${base}.sections[${s}].fields[${f}]`;
6089
- if (fname && known && !known.has(fname)) {
6090
- findings.push({
6091
- severity: "warning",
6092
- rule: FORM_FIELD_UNKNOWN,
6093
- where,
6094
- path: fpath,
6095
- message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6096
- hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6097
- });
6098
- }
6099
- const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
6100
- if (colSpan != null) {
6101
- findings.push({
6102
- severity: "warning",
6103
- rule: FORM_COLSPAN_ABSOLUTE,
6104
- where,
6105
- path: `${fpath}.colSpan`,
6106
- 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`,
6107
- hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6108
- });
6705
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6706
+ const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
6707
+ const containerObject = viewObjectName(view);
6708
+ for (const site of formViewSites(view, viewPath)) {
6709
+ const objName = viewObjectName(site.view) ?? containerObject;
6710
+ const known = objName ? objectFields.get(objName) : void 0;
6711
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
6712
+ for (const bucket of ["sections", "groups"]) {
6713
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6714
+ for (let s = 0; s < sections.length; s++) {
6715
+ const sec = sections[s];
6716
+ const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6717
+ for (let f = 0; f < secFields.length; f++) {
6718
+ const entry = secFields[f];
6719
+ const fname = fieldNameOf(entry);
6720
+ const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
6721
+ if (fname && known && !known.has(fname)) {
6722
+ findings.push({
6723
+ severity: "warning",
6724
+ rule: FORM_FIELD_UNKNOWN,
6725
+ where,
6726
+ path: fpath,
6727
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6728
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6729
+ });
6730
+ }
6731
+ const colSpan = isRec20(entry) ? entry.colSpan : void 0;
6732
+ if (colSpan != null) {
6733
+ findings.push({
6734
+ severity: "warning",
6735
+ rule: FORM_COLSPAN_ABSOLUTE,
6736
+ where,
6737
+ path: `${fpath}.colSpan`,
6738
+ 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`,
6739
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6740
+ });
6741
+ }
6742
+ }
6109
6743
  }
6110
6744
  }
6111
6745
  }
@@ -6204,109 +6838,546 @@ function validateSeedStateMachine(stack) {
6204
6838
  }
6205
6839
 
6206
6840
  // src/validate-visibility-predicates.ts
6207
- var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
6841
+ var import_formula4 = require("@objectstack/formula");
6842
+
6843
+ // src/predicate-rhs-position.ts
6844
+ function isNode2(v) {
6845
+ return !!v && typeof v === "object" && typeof v.op === "string";
6846
+ }
6847
+ var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
6848
+ var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
6849
+ function bareId(node) {
6850
+ if (!isNode2(node)) return null;
6851
+ return node.op === "id" && typeof node.args === "string" ? node.args : null;
6852
+ }
6853
+ function bareRhsOnlyIdentifiers(ast) {
6854
+ const rhs = /* @__PURE__ */ new Set();
6855
+ const elsewhere = /* @__PURE__ */ new Set();
6856
+ const walk = (node, suppressible) => {
6857
+ if (Array.isArray(node)) {
6858
+ for (const child of node) walk(child, suppressible);
6859
+ return;
6860
+ }
6861
+ if (!isNode2(node)) return;
6862
+ const args = node.args;
6863
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
6864
+ walk(args[1], suppressible);
6865
+ walk(args[2], false);
6866
+ return;
6867
+ }
6868
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
6869
+ const right = suppressible ? bareId(args[1]) : null;
6870
+ walk(args[0], suppressible);
6871
+ if (right !== null) {
6872
+ rhs.add(right);
6873
+ return;
6874
+ }
6875
+ walk(args[1], suppressible);
6876
+ return;
6877
+ }
6878
+ const name = bareId(node);
6879
+ if (name !== null) {
6880
+ elsewhere.add(name);
6881
+ return;
6882
+ }
6883
+ walk(args, suppressible);
6884
+ };
6885
+ walk(ast, true);
6886
+ for (const name of elsewhere) rhs.delete(name);
6887
+ return rhs;
6888
+ }
6889
+ function isRec21(v) {
6890
+ return !!v && typeof v === "object" && !Array.isArray(v);
6891
+ }
6892
+ function schemaIdOf(view) {
6893
+ const data = view.data;
6894
+ if (!isRec21(data)) return void 0;
6895
+ if (data.provider !== "schema") return void 0;
6896
+ return typeof data.schemaId === "string" ? data.schemaId : void 0;
6897
+ }
6898
+
6899
+ // src/validate-visibility-predicates.ts
6208
6900
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
6901
+ var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
6902
+ var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
6903
+ var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
6209
6904
  var CANONICAL = "visibleWhen";
6210
- var ALIASES = ["visibleOn", "visibility"];
6211
- function asArray35(v) {
6212
- if (Array.isArray(v)) return v;
6213
- if (v && typeof v === "object") {
6214
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6905
+ function predicateSource(v) {
6906
+ if (typeof v === "string") return v;
6907
+ if (v && typeof v === "object" && typeof v.source === "string") {
6908
+ return v.source;
6909
+ }
6910
+ return void 0;
6911
+ }
6912
+ function usesRoot(source, root) {
6913
+ return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
6914
+ }
6915
+ function withoutStringLiterals(source) {
6916
+ return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
6917
+ }
6918
+ var NON_CEL_SPELLINGS = [
6919
+ { wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
6920
+ { wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
6921
+ { wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
6922
+ { wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
6923
+ { wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
6924
+ { wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
6925
+ // Assignment where a comparison was meant. Last, and fenced off from every
6926
+ // operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
6927
+ { wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
6928
+ ];
6929
+ function quoteSource(source) {
6930
+ const flat = source.replace(/\s+/g, " ").trim();
6931
+ return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
6932
+ }
6933
+ function celRefusal(source) {
6934
+ if (!source.trim()) return null;
6935
+ const parsed = (0, import_formula4.parseCelToAstWithReason)(source);
6936
+ if (parsed.ok || parsed.kind === "empty") return null;
6937
+ if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
6938
+ const identifiers = (0, import_formula4.collectCelRootIdentifiers)(source);
6939
+ const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
6940
+ const scannable = withoutStringLiterals(source);
6941
+ return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
6942
+ }
6943
+ function boundName(overrun) {
6944
+ return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
6945
+ }
6946
+ var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
6947
+ function isNode3(v) {
6948
+ return !!v && typeof v === "object" && typeof v.op === "string";
6949
+ }
6950
+ function namespaceRoots(node, out) {
6951
+ if (Array.isArray(node)) {
6952
+ for (const child of node) namespaceRoots(child, out);
6953
+ return;
6954
+ }
6955
+ if (!isNode3(node)) return;
6956
+ const args = node.args;
6957
+ if (Array.isArray(args)) {
6958
+ const receiver = node.op === "rcall" ? args[1] : args[0];
6959
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6960
+ out.add(receiver.args);
6961
+ }
6962
+ }
6963
+ namespaceRoots(args, out);
6964
+ }
6965
+ function firstBareIdentifier(source, literalRhs) {
6966
+ const ast = (0, import_formula4.parseCelToAst)(source);
6967
+ if (!ast) return null;
6968
+ const rooted = /* @__PURE__ */ new Set();
6969
+ namespaceRoots(ast, rooted);
6970
+ const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
6971
+ return (0, import_formula4.firstUndeclaredReference)(source, [
6972
+ ...VIEW_PAGE_EXTRA_ROOTS,
6973
+ ...rooted,
6974
+ ...literalSlot
6975
+ ]);
6976
+ }
6977
+ var CANONICAL_ROOT_BY_LAYER = {
6978
+ runtime: "record",
6979
+ metadata: "data"
6980
+ };
6981
+ var MISLAYER_BY_LAYER = {
6982
+ runtime: {
6983
+ forbiddenRoot: "data",
6984
+ 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).",
6985
+ hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
6986
+ },
6987
+ metadata: {
6988
+ forbiddenRoot: "record",
6989
+ 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).",
6990
+ hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
6991
+ }
6992
+ };
6993
+ function checkElement(el, where, path, layer, findings, literalRhs = false) {
6994
+ const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
6995
+ const source = predicateSource(raw);
6996
+ const rule = MISLAYER_BY_LAYER[layer];
6997
+ if (source && usesRoot(source, rule.forbiddenRoot)) {
6998
+ findings.push({
6999
+ severity: "warning",
7000
+ rule: VISIBILITY_ROOT_MISLAYERED,
7001
+ where,
7002
+ path,
7003
+ message: rule.message,
7004
+ hint: rule.hint
7005
+ });
7006
+ }
7007
+ const refusal = source ? celRefusal(source) : null;
7008
+ if (source && refusal?.kind === "bounds") {
7009
+ const bound = boundName(refusal.overrun);
7010
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
7011
+ findings.push({
7012
+ severity: "error",
7013
+ rule: VISIBILITY_PREDICATE_OVER_BUDGET,
7014
+ where,
7015
+ path,
7016
+ 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).`,
7017
+ 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.`
7018
+ });
7019
+ }
7020
+ if (source && refusal?.kind === "syntax") {
7021
+ findings.push({
7022
+ severity: "error",
7023
+ rule: VISIBILITY_PREDICATE_SYNTAX,
7024
+ where,
7025
+ path,
7026
+ 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).`,
7027
+ 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\`).`
7028
+ });
7029
+ }
7030
+ if (source && !refusal) {
7031
+ const bare = firstBareIdentifier(source, literalRhs);
7032
+ if (bare) {
7033
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
7034
+ findings.push({
7035
+ severity: "error",
7036
+ rule: VISIBILITY_BARE_IDENTIFIER,
7037
+ where,
7038
+ path,
7039
+ 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).`,
7040
+ 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`).")
7041
+ });
7042
+ }
7043
+ }
7044
+ }
7045
+ function isFieldObject(entry) {
7046
+ return !!entry && typeof entry === "object" && !Array.isArray(entry);
7047
+ }
7048
+ function validateVisibilityPredicates(stack, opts = {}) {
7049
+ const declaredLayer = opts.layer ?? "runtime";
7050
+ const findings = [];
7051
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
7052
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
7053
+ for (const site of formViewSites(view, viewPath)) {
7054
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
7055
+ const schemaBound = schemaIdOf(site.view) !== void 0;
7056
+ const literalRhs = schemaBound;
7057
+ const layer = schemaBound ? "metadata" : declaredLayer;
7058
+ for (const bucket of ["sections", "groups"]) {
7059
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7060
+ for (let s = 0; s < sections.length; s++) {
7061
+ const sec = sections[s];
7062
+ if (!sec || typeof sec !== "object") continue;
7063
+ const secPath = `${site.path}.${bucket}[${s}]`;
7064
+ checkElement(sec, where, secPath, layer, findings, literalRhs);
7065
+ const secFields = Array.isArray(sec.fields) ? sec.fields : [];
7066
+ for (let f = 0; f < secFields.length; f++) {
7067
+ const entry = secFields[f];
7068
+ if (isFieldObject(entry)) {
7069
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
7070
+ }
7071
+ }
7072
+ }
7073
+ }
7074
+ }
7075
+ }
7076
+ for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
7077
+ const pageName = typeof page.name === "string" ? page.name : void 0;
7078
+ const where = `page "${pageName ?? pagePath}"`;
7079
+ for (const walked of walkPageComponents(page, pagePath)) {
7080
+ checkElement(walked.component, where, walked.path, declaredLayer, findings);
7081
+ }
7082
+ }
7083
+ return findings;
7084
+ }
7085
+
7086
+ // src/validate-predicate-path-refs.ts
7087
+ var import_formula5 = require("@objectstack/formula");
7088
+ var import_kernel2 = require("@objectstack/spec/kernel");
7089
+ var import_spec4 = require("@objectstack/spec");
7090
+ var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
7091
+ var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
7092
+ var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
7093
+ var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
7094
+ var ROOT = "data";
7095
+ function defOf(schema) {
7096
+ if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
7097
+ const s = schema;
7098
+ return s.def ?? s._def;
7099
+ }
7100
+ function peel(schema, depth = 0) {
7101
+ if (!schema || depth > 25) return schema;
7102
+ const d = defOf(schema);
7103
+ if (!d) return schema;
7104
+ switch (d.type) {
7105
+ case "optional":
7106
+ case "nullable":
7107
+ case "default":
7108
+ case "prefault":
7109
+ case "readonly":
7110
+ case "catch":
7111
+ case "nonoptional":
7112
+ return peel(d.innerType, depth + 1);
7113
+ case "lazy":
7114
+ return peel(d.getter(), depth + 1);
7115
+ case "pipe": {
7116
+ const inner = peel(d.in, depth + 1);
7117
+ return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
7118
+ }
7119
+ default:
7120
+ return schema;
7121
+ }
7122
+ }
7123
+ function optionsOf(d) {
7124
+ return Array.isArray(d?.options) ? d.options : [];
7125
+ }
7126
+ function keysOf(schema, depth = 0) {
7127
+ if (depth > 25) return null;
7128
+ const u = peel(schema);
7129
+ const d = defOf(u);
7130
+ if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
7131
+ if (d?.type === "union" || d?.type === "discriminated_union") {
7132
+ const all = /* @__PURE__ */ new Set();
7133
+ let keyBearing = false;
7134
+ for (const option of optionsOf(d)) {
7135
+ const k = keysOf(option, depth + 1);
7136
+ if (!k) continue;
7137
+ keyBearing = true;
7138
+ for (const key of k) all.add(key);
7139
+ }
7140
+ return keyBearing ? [...all] : null;
7141
+ }
7142
+ if (d?.type === "intersection") {
7143
+ const left = keysOf(d.left, depth + 1);
7144
+ const right = keysOf(d.right, depth + 1);
7145
+ if (!left && !right) return null;
7146
+ return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
7147
+ }
7148
+ return null;
7149
+ }
7150
+ function propertyOf(schema, key, depth = 0) {
7151
+ if (depth > 25) return void 0;
7152
+ const u = peel(schema);
7153
+ const d = defOf(u);
7154
+ if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
7155
+ if (d?.type === "union" || d?.type === "discriminated_union") {
7156
+ for (const option of optionsOf(d)) {
7157
+ const found = propertyOf(option, key, depth + 1);
7158
+ if (found !== void 0) return found;
7159
+ }
7160
+ }
7161
+ if (d?.type === "intersection") {
7162
+ return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
7163
+ }
7164
+ return void 0;
7165
+ }
7166
+ function rowScopeOf(scope, key) {
7167
+ const prop = propertyOf(scope, key);
7168
+ if (prop === void 0) return void 0;
7169
+ let node = peel(prop);
7170
+ for (let i = 0; i < 25; i++) {
7171
+ const d = defOf(node);
7172
+ if (d?.type === "array") node = peel(d.element);
7173
+ else if (d?.type === "record") node = peel(d.valueType);
7174
+ else return node;
7175
+ }
7176
+ return node;
7177
+ }
7178
+ function stepInto(scope, segment) {
7179
+ const u = peel(scope);
7180
+ const d = defOf(u);
7181
+ if (d?.type === "record") return { kind: "declared", next: d.valueType };
7182
+ const declared = keysOf(u);
7183
+ if (declared === null) return { kind: "opaque" };
7184
+ if (!declared.includes(segment)) return { kind: "undeclared", declared };
7185
+ return { kind: "declared", next: propertyOf(u, segment) };
7186
+ }
7187
+ function isNode4(v) {
7188
+ return !!v && typeof v === "object" && typeof v.op === "string";
7189
+ }
7190
+ function memberChain(node) {
7191
+ if (!isNode4(node)) return null;
7192
+ if (node.op === "id" && typeof node.args === "string") return [node.args];
7193
+ if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
7194
+ const head = memberChain(node.args[0]);
7195
+ return head ? [...head, node.args[1]] : null;
7196
+ }
7197
+ return null;
7198
+ }
7199
+ function rootedPaths(node, out) {
7200
+ if (Array.isArray(node)) {
7201
+ for (const child of node) rootedPaths(child, out);
7202
+ return;
7203
+ }
7204
+ if (!isNode4(node)) return;
7205
+ if (node.op === ".") {
7206
+ const chain = memberChain(node);
7207
+ if (chain && chain[0] === ROOT && chain.length > 1) {
7208
+ out.push(chain.slice(1));
7209
+ return;
7210
+ }
7211
+ }
7212
+ rootedPaths(node.args, out);
7213
+ }
7214
+ var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
7215
+ function equalitySites(node, out) {
7216
+ if (Array.isArray(node)) {
7217
+ for (const child of node) equalitySites(child, out);
7218
+ return;
7219
+ }
7220
+ if (!isNode4(node)) return;
7221
+ const args = node.args;
7222
+ if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
7223
+ equalitySites(args[1], out);
7224
+ return;
6215
7225
  }
6216
- return [];
7226
+ if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
7227
+ out.push({ op: node.op, right: args[1] });
7228
+ }
7229
+ equalitySites(args, out);
6217
7230
  }
6218
- function predicateSource(v) {
7231
+ function classifyIdentifiers(node, values, excluded) {
7232
+ if (Array.isArray(node)) {
7233
+ for (const child of node) classifyIdentifiers(child, values, excluded);
7234
+ return;
7235
+ }
7236
+ if (!isNode4(node)) return;
7237
+ const args = node.args;
7238
+ if (Array.isArray(args)) {
7239
+ const receiver = node.op === "rcall" ? args[1] : args[0];
7240
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
7241
+ excluded.add(receiver.args);
7242
+ }
7243
+ if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
7244
+ const macroArgs = args[2];
7245
+ if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
7246
+ const bound = macroArgs[0];
7247
+ if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
7248
+ excluded.add(bound.args);
7249
+ }
7250
+ }
7251
+ }
7252
+ }
7253
+ if (node.op === "id" && typeof node.args === "string") {
7254
+ values.add(node.args);
7255
+ return;
7256
+ }
7257
+ classifyIdentifiers(args, values, excluded);
7258
+ }
7259
+ function predicateSource2(v) {
6219
7260
  if (typeof v === "string") return v;
6220
7261
  if (v && typeof v === "object" && typeof v.source === "string") {
6221
7262
  return v.source;
6222
7263
  }
6223
7264
  return void 0;
6224
7265
  }
6225
- function usesRoot(source, root) {
6226
- return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
7266
+ function isRec22(v) {
7267
+ return !!v && typeof v === "object" && !Array.isArray(v);
6227
7268
  }
6228
- var MISLAYER_BY_LAYER = {
6229
- runtime: {
6230
- forbiddenRoot: "data",
6231
- 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).",
6232
- hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
6233
- },
6234
- metadata: {
6235
- forbiddenRoot: "record",
6236
- 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).",
6237
- hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
6238
- }
6239
- };
6240
- function checkElement(el, where, path, layer, findings) {
6241
- for (const alias of ALIASES) {
6242
- if (el[alias] !== void 0) {
7269
+ function checkPredicate(source, scope, where, path, findings) {
7270
+ const ast = (0, import_formula5.parseCelToAst)(source);
7271
+ if (!ast) return;
7272
+ const paths = [];
7273
+ rootedPaths(ast, paths);
7274
+ for (const segments of paths) {
7275
+ let cursor = scope;
7276
+ const walked = [];
7277
+ for (const segment of segments) {
7278
+ const step = stepInto(cursor, segment);
7279
+ if (step.kind === "opaque") break;
7280
+ if (step.kind === "undeclared") {
7281
+ const full = [ROOT, ...walked, segment].join(".");
7282
+ const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
7283
+ findings.push({
7284
+ severity: "error",
7285
+ rule: PREDICATE_PATH_UNRESOLVED,
7286
+ where,
7287
+ path,
7288
+ 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).`,
7289
+ hint: `${(0, import_spec4.formatSuggestion)((0, import_spec4.findClosestMatches)(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
7290
+ });
7291
+ break;
7292
+ }
7293
+ walked.push(segment);
7294
+ cursor = step.next;
7295
+ }
7296
+ }
7297
+ const declaredHere = keysOf(scope);
7298
+ const rhsOnly = bareRhsOnlyIdentifiers(ast);
7299
+ if (declaredHere) {
7300
+ const values = /* @__PURE__ */ new Set();
7301
+ const excluded = /* @__PURE__ */ new Set();
7302
+ classifyIdentifiers(ast, values, excluded);
7303
+ for (const id of values) {
7304
+ if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
6243
7305
  findings.push({
6244
- severity: "warning",
6245
- rule: VISIBILITY_ALIAS_DEPRECATED,
7306
+ severity: "error",
7307
+ rule: PREDICATE_PATH_UNROOTED,
6246
7308
  where,
6247
- path: `${path}.${alias}`,
6248
- 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\`.`,
6249
- hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
7309
+ path,
7310
+ 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).`,
7311
+ 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).`
6250
7312
  });
6251
7313
  }
6252
7314
  }
6253
- const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
6254
- const source = predicateSource(raw);
6255
- const rule = MISLAYER_BY_LAYER[layer];
6256
- if (source && usesRoot(source, rule.forbiddenRoot)) {
7315
+ const sites = [];
7316
+ equalitySites(ast, sites);
7317
+ for (const { op, right } of sites) {
7318
+ const chain = memberChain(right);
7319
+ if (!chain) continue;
7320
+ const text = chain.join(".");
7321
+ if (!PATH_SHAPED_RHS.test(text)) continue;
7322
+ const dotted = chain.length > 1;
6257
7323
  findings.push({
6258
- severity: "warning",
6259
- rule: VISIBILITY_ROOT_MISLAYERED,
7324
+ severity: dotted ? "error" : "warning",
7325
+ rule: PREDICATE_RHS_PATH_SHAPED,
6260
7326
  where,
6261
7327
  path,
6262
- message: rule.message,
6263
- hint: rule.hint
7328
+ 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).`,
7329
+ 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.`
6264
7330
  });
6265
7331
  }
6266
7332
  }
6267
- function isFieldObject(entry) {
6268
- return !!entry && typeof entry === "object" && !Array.isArray(entry);
6269
- }
6270
- function validateVisibilityPredicates(stack, opts = {}) {
6271
- const layer = opts.layer ?? "runtime";
6272
- const findings = [];
6273
- const views = asArray35(stack.views);
6274
- for (let i = 0; i < views.length; i++) {
6275
- const view = views[i];
6276
- if (!view || typeof view !== "object") continue;
6277
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6278
- const where = `view "${viewName}"`;
6279
- for (const bucket of ["sections", "groups"]) {
6280
- const sections = Array.isArray(view[bucket]) ? view[bucket] : [];
6281
- for (let s = 0; s < sections.length; s++) {
6282
- const sec = sections[s];
6283
- if (!sec || typeof sec !== "object") continue;
6284
- const secPath = `views[${i}].${bucket}[${s}]`;
6285
- checkElement(sec, where, secPath, layer, findings);
6286
- const secFields = Array.isArray(sec.fields) ? sec.fields : [];
6287
- for (let f = 0; f < secFields.length; f++) {
6288
- const entry = secFields[f];
6289
- if (isFieldObject(entry)) {
6290
- checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
6291
- }
6292
- }
7333
+ function walkFields(entries, scope, where, base, findings, depth) {
7334
+ if (!Array.isArray(entries) || depth > 12) return;
7335
+ for (let i = 0; i < entries.length; i++) {
7336
+ const entry = entries[i];
7337
+ if (!isRec22(entry)) continue;
7338
+ const path = `${base}[${i}]`;
7339
+ for (const key of PREDICATE_KEYS) {
7340
+ const source = predicateSource2(entry[key]);
7341
+ if (source !== void 0 && source.trim()) {
7342
+ checkPredicate(source, scope, where, `${path}.${key}`, findings);
7343
+ break;
6293
7344
  }
6294
7345
  }
7346
+ if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
7347
+ const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
7348
+ walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
7349
+ }
6295
7350
  }
6296
- const pages = asArray35(stack.pages);
6297
- for (let i = 0; i < pages.length; i++) {
6298
- const page = pages[i];
6299
- if (!page || typeof page !== "object") continue;
6300
- const pageName = typeof page.name === "string" ? page.name : `(page ${i})`;
6301
- const where = `page "${pageName}"`;
6302
- const regions = Array.isArray(page.regions) ? page.regions : [];
6303
- for (let r = 0; r < regions.length; r++) {
6304
- const region = regions[r];
6305
- const components = region && typeof region === "object" && Array.isArray(region.components) ? region.components : [];
6306
- for (let c = 0; c < components.length; c++) {
6307
- const comp = components[c];
6308
- if (comp && typeof comp === "object") {
6309
- checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
7351
+ }
7352
+ function validatePredicatePathRefs(stack, opts = {}) {
7353
+ const resolveSchema = opts.resolveSchema ?? ((schemaId) => (0, import_kernel2.getMetadataTypeSchema)(schemaId));
7354
+ const findings = [];
7355
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
7356
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
7357
+ for (const site of formViewSites(view, viewPath)) {
7358
+ const schemaId = schemaIdOf(site.view);
7359
+ if (!schemaId) continue;
7360
+ let root;
7361
+ try {
7362
+ root = resolveSchema(schemaId);
7363
+ } catch {
7364
+ root = void 0;
7365
+ }
7366
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
7367
+ for (const bucket of ["sections", "groups"]) {
7368
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7369
+ for (let s = 0; s < sections.length; s++) {
7370
+ const section = sections[s];
7371
+ if (!isRec22(section)) continue;
7372
+ const sectionPath = `${site.path}.${bucket}[${s}]`;
7373
+ for (const key of PREDICATE_KEYS) {
7374
+ const source = predicateSource2(section[key]);
7375
+ if (source !== void 0 && source.trim()) {
7376
+ checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
7377
+ break;
7378
+ }
7379
+ }
7380
+ walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
6310
7381
  }
6311
7382
  }
6312
7383
  }
@@ -6328,6 +7399,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
6328
7399
  var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
6329
7400
  var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
6330
7401
  var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
7402
+ var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
6331
7403
  var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
6332
7404
  var OWD_ALIAS_FIX = {
6333
7405
  read: "public_read",
@@ -6373,6 +7445,13 @@ function firstMasterDetailField(obj) {
6373
7445
  }
6374
7446
  return void 0;
6375
7447
  }
7448
+ function resolveCbpRelation(obj) {
7449
+ const entries = asArray36(obj.fields);
7450
+ const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
7451
+ const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
7452
+ if (!found) return void 0;
7453
+ return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
7454
+ }
6376
7455
  function grantsObjectAccess(p) {
6377
7456
  return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
6378
7457
  }
@@ -6418,6 +7497,16 @@ function validateSecurityPosture(stack, opts) {
6418
7497
  });
6419
7498
  }
6420
7499
  }
7500
+ if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
7501
+ findings.push({
7502
+ severity: "error",
7503
+ rule: SECURITY_CBP_NO_RELATION,
7504
+ where: `object "${objName}"`,
7505
+ path: `${objPath}.sharingModel`,
7506
+ 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.`,
7507
+ 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'.`
7508
+ });
7509
+ }
6421
7510
  if (typeof external === "string") {
6422
7511
  if (OWD_ALIAS_FIX[external]) {
6423
7512
  findings.push({
@@ -6483,53 +7572,6 @@ function validateSecurityPosture(stack, opts) {
6483
7572
  }
6484
7573
  }
6485
7574
  }
6486
- const flagRole = (kind, name, label2, where, path) => {
6487
- if (identifierHasRoleToken(name)) {
6488
- findings.push({
6489
- severity: "error",
6490
- rule: SECURITY_ROLE_WORD,
6491
- where,
6492
- path,
6493
- 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).`,
6494
- hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
6495
- });
6496
- } else if (labelHasRoleWord(label2)) {
6497
- findings.push({
6498
- severity: "error",
6499
- rule: SECURITY_ROLE_WORD,
6500
- where,
6501
- path: `${path.replace(/\.name$/, "")}.label`,
6502
- message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
6503
- hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
6504
- });
6505
- }
6506
- };
6507
- for (let i = 0; i < objects.length; i++) {
6508
- const obj = objects[i];
6509
- if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
6510
- const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
6511
- flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
6512
- for (const f of asArray36(obj.fields)) {
6513
- flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
6514
- }
6515
- for (const [ai, action] of asArray36(obj.actions).entries()) {
6516
- flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
6517
- }
6518
- }
6519
- for (let i = 0; i < permissionSets.length; i++) {
6520
- const ps = permissionSets[i];
6521
- if (!ps || typeof ps !== "object") continue;
6522
- flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
6523
- }
6524
- for (const [i, pos] of asArray36(stack.positions).entries()) {
6525
- flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
6526
- }
6527
- for (const [i, app] of asArray36(stack.apps).entries()) {
6528
- flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
6529
- }
6530
- for (const [i, book] of asArray36(stack.books).entries()) {
6531
- flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
6532
- }
6533
7575
  const stackSetNames = new Set(
6534
7576
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
6535
7577
  );
@@ -6650,6 +7692,60 @@ function validateSecurityPosture(stack, opts) {
6650
7692
  }
6651
7693
  return findings;
6652
7694
  }
7695
+ function validateSecurityRoleWord(stack) {
7696
+ const findings = [];
7697
+ if (!stack || typeof stack !== "object") return findings;
7698
+ const objects = asArray36(stack.objects);
7699
+ const permissionSets = asArray36(stack.permissions);
7700
+ const flagRole = (kind, name, label2, where, path) => {
7701
+ if (identifierHasRoleToken(name)) {
7702
+ findings.push({
7703
+ severity: "error",
7704
+ rule: SECURITY_ROLE_WORD,
7705
+ where,
7706
+ path,
7707
+ 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).`,
7708
+ hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
7709
+ });
7710
+ } else if (labelHasRoleWord(label2)) {
7711
+ findings.push({
7712
+ severity: "error",
7713
+ rule: SECURITY_ROLE_WORD,
7714
+ where,
7715
+ path: `${path.replace(/\.name$/, "")}.label`,
7716
+ message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
7717
+ hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
7718
+ });
7719
+ }
7720
+ };
7721
+ for (let i = 0; i < objects.length; i++) {
7722
+ const obj = objects[i];
7723
+ if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
7724
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
7725
+ flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
7726
+ for (const f of asArray36(obj.fields)) {
7727
+ flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
7728
+ }
7729
+ for (const [ai, action] of asArray36(obj.actions).entries()) {
7730
+ flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
7731
+ }
7732
+ }
7733
+ for (let i = 0; i < permissionSets.length; i++) {
7734
+ const ps = permissionSets[i];
7735
+ if (!ps || typeof ps !== "object") continue;
7736
+ flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
7737
+ }
7738
+ for (const [i, pos] of asArray36(stack.positions).entries()) {
7739
+ flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
7740
+ }
7741
+ for (const [i, app] of asArray36(stack.apps).entries()) {
7742
+ flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
7743
+ }
7744
+ for (const [i, book] of asArray36(stack.books).entries()) {
7745
+ flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
7746
+ }
7747
+ return findings;
7748
+ }
6653
7749
 
6654
7750
  // src/validate-org-axis-red-lines.ts
6655
7751
  var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
@@ -6742,7 +7838,7 @@ function validateOrgAxisRedLines(stack) {
6742
7838
  }
6743
7839
 
6744
7840
  // src/validate-sharing-rule-enforceability.ts
6745
- var import_formula4 = require("@objectstack/formula");
7841
+ var import_formula6 = require("@objectstack/formula");
6746
7842
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
6747
7843
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
6748
7844
  function asArray38(v) {
@@ -6775,7 +7871,7 @@ function validateSharingRuleEnforceability(stack) {
6775
7871
  asArray38(cfg.sharingRules).forEach((rule, index) => {
6776
7872
  const input = toCompilerInput(rule.condition);
6777
7873
  if (input === null) return;
6778
- const result = (0, import_formula4.compileCelToFilter)(input, { variables: {} });
7874
+ const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
6779
7875
  if (result.ok) return;
6780
7876
  if (result.reason === "parse-error") return;
6781
7877
  const name = str2(rule.name) || String(index);
@@ -6808,9 +7904,10 @@ function validateSharingRuleEnforceability(stack) {
6808
7904
  }
6809
7905
 
6810
7906
  // src/validate-rls-predicate-enforceability.ts
6811
- var import_formula5 = require("@objectstack/formula");
7907
+ var import_formula7 = require("@objectstack/formula");
6812
7908
  var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
6813
7909
  var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
7910
+ var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
6814
7911
  function asArray39(v) {
6815
7912
  if (Array.isArray(v)) return v;
6816
7913
  if (v && typeof v === "object") {
@@ -6822,6 +7919,13 @@ function str3(v) {
6822
7919
  return typeof v === "string" ? v : "";
6823
7920
  }
6824
7921
  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.";
7922
+ function boundsOverrunOf(bridged) {
7923
+ const parsed = (0, import_formula7.parseCelToAstWithReason)(bridged);
7924
+ return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
7925
+ }
7926
+ function quote(source) {
7927
+ return source.length > 200 ? `${source.slice(0, 197)}...` : source;
7928
+ }
6825
7929
  function consequence(clause) {
6826
7930
  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). ';
6827
7931
  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.";
@@ -6834,15 +7938,31 @@ function validateRlsPredicateEnforceability(stack) {
6834
7938
  for (const clause of ["using", "check"]) {
6835
7939
  const source = str3(policy[clause]);
6836
7940
  if (!source.trim()) continue;
6837
- if ((0, import_formula5.isSupportedRlsExpression)(source)) continue;
6838
- const why = (0, import_formula5.isPushdownableCel)((0, import_formula5.sqlPredicateToCel)(source));
7941
+ if ((0, import_formula7.isSupportedRlsExpression)(source)) continue;
7942
+ const bridged = (0, import_formula7.sqlPredicateToCel)(source);
7943
+ const why = (0, import_formula7.isPushdownableCel)(bridged);
6839
7944
  const detail = why.ok ? "" : why.detail;
6840
7945
  const parseError = !why.ok && why.reason === "parse-error";
7946
+ const overrun = parseError ? boundsOverrunOf(bridged) : null;
6841
7947
  const psName = str3(ps.name) || String(psIndex);
6842
7948
  const policyName = str3(policy.name) || String(pIndex);
6843
7949
  const object = str3(policy.object);
6844
7950
  const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
6845
7951
  const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
7952
+ if (overrun) {
7953
+ const bound = overrun.limit ?? "an unnamed platform CEL bound";
7954
+ const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
7955
+ const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
7956
+ findings.push({
7957
+ severity: "error",
7958
+ rule: RLS_PREDICATE_OVER_BUDGET,
7959
+ where,
7960
+ path,
7961
+ message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
7962
+ 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).`
7963
+ });
7964
+ continue;
7965
+ }
6846
7966
  if (parseError) {
6847
7967
  findings.push({
6848
7968
  severity: "error",
@@ -6874,11 +7994,11 @@ var import_meta4 = {};
6874
7994
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
6875
7995
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
6876
7996
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
6877
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7997
+ var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6878
7998
  function asArray40(v) {
6879
- if (Array.isArray(v)) return v.filter(isRec16);
6880
- if (isRec16(v)) {
6881
- return Object.entries(v).filter(([, def]) => isRec16(def)).map(([name, def]) => ({ name, ...def }));
7999
+ if (Array.isArray(v)) return v.filter(isRec23);
8000
+ if (isRec23(v)) {
8001
+ return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
6882
8002
  }
6883
8003
  return [];
6884
8004
  }
@@ -6895,7 +8015,7 @@ function loadAjv() {
6895
8015
  `@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.`
6896
8016
  );
6897
8017
  }
6898
- const ctor = isRec16(mod) && "default" in mod ? mod.default : mod;
8018
+ const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
6899
8019
  cachedAjv = ctor;
6900
8020
  return ctor;
6901
8021
  }
@@ -6910,7 +8030,7 @@ function loadAddFormats() {
6910
8030
  `@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.`
6911
8031
  );
6912
8032
  }
6913
- const plugin = isRec16(mod) && "default" in mod ? mod.default : mod;
8033
+ const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
6914
8034
  cachedAddFormats = plugin;
6915
8035
  return plugin;
6916
8036
  }
@@ -6940,13 +8060,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
6940
8060
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
6941
8061
  for (const branch of ["then", "otherwise"]) {
6942
8062
  const nested = rule[branch];
6943
- if (isRec16(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8063
+ if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6944
8064
  }
6945
8065
  return out;
6946
8066
  }
6947
8067
  function walkObjectValidationRules(stack) {
6948
8068
  const walked = [];
6949
- if (!isRec16(stack)) return walked;
8069
+ if (!isRec23(stack)) return walked;
6950
8070
  for (const obj of asArray40(stack.objects)) {
6951
8071
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6952
8072
  const validations = obj.validations;
@@ -6981,7 +8101,7 @@ function validateRuleCompilability(stack) {
6981
8101
  });
6982
8102
  }
6983
8103
  }
6984
- if (rule.type === "json_schema" && isRec16(rule.schema)) {
8104
+ if (rule.type === "json_schema" && isRec23(rule.schema)) {
6985
8105
  try {
6986
8106
  createRuntimeAjv().compile(rule.schema);
6987
8107
  } catch (err) {
@@ -7001,7 +8121,7 @@ function validateRuleCompilability(stack) {
7001
8121
 
7002
8122
  // src/validate-rule-schema-formats.ts
7003
8123
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7004
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8124
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7005
8125
  var SUBSCHEMA_KEYS = [
7006
8126
  "additionalItems",
7007
8127
  "additionalProperties",
@@ -7025,7 +8145,7 @@ var SUBSCHEMA_MAP_KEYS = [
7025
8145
  var MAX_SCHEMA_WALK_DEPTH = 32;
7026
8146
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7027
8147
  function collectFormatUses(schema, pointer, out, depth) {
7028
- if (!isRec17(schema)) return;
8148
+ if (!isRec24(schema)) return;
7029
8149
  if (typeof schema.format === "string") {
7030
8150
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7031
8151
  }
@@ -7044,7 +8164,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7044
8164
  }
7045
8165
  for (const key of SUBSCHEMA_MAP_KEYS) {
7046
8166
  const value = schema[key];
7047
- if (!isRec17(value)) continue;
8167
+ if (!isRec24(value)) continue;
7048
8168
  for (const [name, entry] of Object.entries(value)) {
7049
8169
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7050
8170
  }
@@ -7052,13 +8172,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7052
8172
  const items = schema.items;
7053
8173
  if (Array.isArray(items)) {
7054
8174
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7055
- } else if (isRec17(items)) {
8175
+ } else if (isRec24(items)) {
7056
8176
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7057
8177
  }
7058
8178
  const dependencies = schema.dependencies;
7059
- if (isRec17(dependencies)) {
8179
+ if (isRec24(dependencies)) {
7060
8180
  for (const [name, entry] of Object.entries(dependencies)) {
7061
- if (!isRec17(entry)) continue;
8181
+ if (!isRec24(entry)) continue;
7062
8182
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7063
8183
  }
7064
8184
  }
@@ -7097,7 +8217,7 @@ function validateRuleSchemaFormats(stack) {
7097
8217
  const findings = [];
7098
8218
  const pending = [];
7099
8219
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7100
- if (rule.type !== "json_schema" || !isRec17(rule.schema)) continue;
8220
+ if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
7101
8221
  const uses = [];
7102
8222
  collectFormatUses(rule.schema, "", uses, 0);
7103
8223
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7130,7 +8250,7 @@ function asArray41(v) {
7130
8250
  }
7131
8251
  return [];
7132
8252
  }
7133
- function strName17(v) {
8253
+ function strName20(v) {
7134
8254
  return typeof v === "string" && v.length > 0 ? v : void 0;
7135
8255
  }
7136
8256
  function strList3(v) {
@@ -7145,7 +8265,7 @@ function collectNamePlacedActions(stack) {
7145
8265
  for (const n of strList3(list3[key])) placed.add(n);
7146
8266
  }
7147
8267
  for (const def of asArray41(list3.bulkActionDefs)) {
7148
- const n = strName17(def?.name);
8268
+ const n = strName20(def?.name);
7149
8269
  if (n) placed.add(n);
7150
8270
  }
7151
8271
  };
@@ -7171,7 +8291,7 @@ function validateActionLocations(stack) {
7171
8291
  const check = (action, path) => {
7172
8292
  if (!action || typeof action !== "object") return;
7173
8293
  if ("locations" in action) return;
7174
- const name = strName17(action.name);
8294
+ const name = strName20(action.name);
7175
8295
  if (!name) return;
7176
8296
  if (namePlaced.has(name)) return;
7177
8297
  findings.push({
@@ -7180,7 +8300,7 @@ function validateActionLocations(stack) {
7180
8300
  where: `action "${name}"`,
7181
8301
  path,
7182
8302
  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.`,
7183
- 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."
8303
+ 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."
7184
8304
  });
7185
8305
  };
7186
8306
  const actions = asArray41(stack.actions);
@@ -7197,6 +8317,7 @@ function validateActionLocations(stack) {
7197
8317
 
7198
8318
  // src/lint-flow-patterns.ts
7199
8319
  var import_automation5 = require("@objectstack/spec/automation");
8320
+ var import_data10 = require("@objectstack/spec/data");
7200
8321
  function asArray42(v) {
7201
8322
  if (Array.isArray(v)) return v;
7202
8323
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7471,7 +8592,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
7471
8592
  function filterCarriesNoCondition(filter) {
7472
8593
  if (filter === void 0 || filter === null) return true;
7473
8594
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
7474
- return Object.keys(filter).length === 0;
8595
+ return (0, import_data10.reduceFilterVerdict)(filter) === "true";
8596
+ }
8597
+ function describeUnboundedFilter(filter) {
8598
+ if (filter === void 0 || filter === null) return "no `filter` key";
8599
+ if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
8600
+ return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
8601
+ }
8602
+ function previewFilter(filter) {
8603
+ try {
8604
+ const json = JSON.stringify(filter);
8605
+ if (typeof json !== "string") return typeof filter;
8606
+ return json.length > 80 ? `${json.slice(0, 77)}...` : json;
8607
+ } catch {
8608
+ return typeof filter;
8609
+ }
7475
8610
  }
7476
8611
  function scanUnboundedBulkWrites(at, nodes, findings) {
7477
8612
  for (const node of nodes) {
@@ -7482,10 +8617,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
7482
8617
  if (cfg.multi !== true) continue;
7483
8618
  if (!filterCarriesNoCondition(cfg.filter)) continue;
7484
8619
  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`";
8620
+ const filterState = describeUnboundedFilter(cfg.filter);
7486
8621
  findings.push({
7487
8622
  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.`,
8623
+ 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
8624
  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
8625
  // Warning, not `error`: see the severity policy at the top of this file.
7491
8626
  // The shape has a legitimate reading the engine grants on purpose, so it is
@@ -7787,6 +8922,13 @@ var TYPE_COLLECTIONS = [
7787
8922
  // checks every widget on the dashboard. Registering it here is not optional
7788
8923
  // bookkeeping: without it the ledger would be newly correct and newly
7789
8924
  // silent, which is the shape this lint exists to prevent.
8925
+ //
8926
+ // As of #6774 the dashboard ledger warns on NOTHING — four of those five were
8927
+ // retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
8928
+ // gave it a renderer. The type STAYS listed, the resolved state `webhook` and
8929
+ // `email_template` already sit in: a zero-warn entry costs one empty map
8930
+ // lookup, and it means a future regression that re-deadens a widget key warns
8931
+ // on its own instead of waiting for someone to notice this list again.
7790
8932
  { type: "dashboard", key: "dashboards" }
7791
8933
  ];
7792
8934
  function lintLivenessProperties(stack) {
@@ -7817,7 +8959,7 @@ function lintLivenessProperties(stack) {
7817
8959
  }
7818
8960
 
7819
8961
  // src/lint-autonumber-formats.ts
7820
- var import_data7 = require("@objectstack/spec/data");
8962
+ var import_data11 = require("@objectstack/spec/data");
7821
8963
  var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7822
8964
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7823
8965
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
@@ -7843,8 +8985,8 @@ function lintAutonumberFormats(stack) {
7843
8985
  const name = typeof f.name === "string" ? f.name : "(unnamed field)";
7844
8986
  const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
7845
8987
  if (!fmt) continue;
7846
- const tokens = (0, import_data7.parseAutonumberFormat)(fmt);
7847
- const refs = (0, import_data7.referencedFields)(tokens);
8988
+ const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
8989
+ const refs = (0, import_data11.referencedFields)(tokens);
7848
8990
  const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
7849
8991
  for (const t of tokens) {
7850
8992
  if (t.kind !== "literal") continue;
@@ -7898,7 +9040,7 @@ function lintAutonumberFormats(stack) {
7898
9040
  }
7899
9041
 
7900
9042
  // src/lint-view-refs.ts
7901
- var import_spec3 = require("@objectstack/spec");
9043
+ var import_spec5 = require("@objectstack/spec");
7902
9044
  function asArray45(v) {
7903
9045
  if (Array.isArray(v)) return v;
7904
9046
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7932,7 +9074,7 @@ function lintViewRefs(stack) {
7932
9074
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7933
9075
  continue;
7934
9076
  }
7935
- if (!(0, import_spec3.isAggregatedViewContainer)(v)) continue;
9077
+ if (!(0, import_spec5.isAggregatedViewContainer)(v)) continue;
7936
9078
  const object = viewContainerObjectName(v);
7937
9079
  if (object) containers.push({ object, container: v });
7938
9080
  }
@@ -7944,7 +9086,7 @@ function lintViewRefs(stack) {
7944
9086
  }
7945
9087
  }
7946
9088
  for (const { object, container } of containers) {
7947
- const { items, collisions } = (0, import_spec3.expandViewContainerWithDiagnostics)(object, container);
9089
+ const { items, collisions } = (0, import_spec5.expandViewContainerWithDiagnostics)(object, container);
7948
9090
  for (const it of items) indexKind(it.name, it.viewKind);
7949
9091
  for (const col of collisions) {
7950
9092
  findings.push({
@@ -8158,8 +9300,12 @@ var AUTHORING_RULES = [
8158
9300
  }))
8159
9301
  },
8160
9302
  // ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
8161
- // mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
8162
- // read the pre-parse tier or the evidence is already gone.
9303
+ // mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
9304
+ // is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
9305
+ // refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
9306
+ // THROWS on both. `normalized` here therefore means "needs no parsed stack"
9307
+ // (so `os lint`, which never parses, can run it), not "sees evidence the
9308
+ // parse would have eaten".
8163
9309
  {
8164
9310
  name: "validateListViewMode",
8165
9311
  tier: "gating",
@@ -8190,9 +9336,38 @@ var AUTHORING_RULES = [
8190
9336
  surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8191
9337
  run: (stack) => validateFunctionalCompleteness(stack)
8192
9338
  },
8193
- // A flat list-view object in `views: []` parses to an EMPTY container
8194
- // (ViewSchema strips unknown keys): the schema step passes, zero views
8195
- // register, and the Console renders nothing. Pre-parse for the same reason.
9339
+ // [#7521, via cloud#1225] A managed object advertising a generic write verb
9340
+ // in `enable.apiMethods` that its own resolved affordances refuse. Every key
9341
+ // is one we know and each is individually valid, so #4001's unknown-key
9342
+ // rejection and the Zod parse both pass it; the contradiction is only visible
9343
+ // when the two keys are read TOGETHER, which nothing did at authoring time.
9344
+ //
9345
+ // `gating` because the declaration is already false when it ships: objectql's
9346
+ // registry strips the verb at registration, so the metadata advertises an API
9347
+ // the product does not serve. That strip has been correct and silent — a
9348
+ // `console.warn` on every control-plane boot that went unread for the life of
9349
+ // a real divergence (`sys_environment`/`sys_package`). This entry is the
9350
+ // ruling's "close it where the author is"; boot stays warn-and-strip.
9351
+ //
9352
+ // Pre-parse: the predicate reads only authored keys, and the finding must
9353
+ // survive an unrelated schema error elsewhere in the stack.
9354
+ {
9355
+ name: "validateManagedApiMethods",
9356
+ tier: "gating",
9357
+ input: "normalized",
9358
+ commands: ALL,
9359
+ source: "packages/lint/src/validate-managed-api-methods.ts",
9360
+ surfaces: CLI_ONLY,
9361
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9362
+ run: (stack) => validateManagedApiMethods(stack)
9363
+ },
9364
+ // A view container in `views: []` that registers zero views: nothing appears
9365
+ // in the Console, and the schema step cannot tell it from an intentionally
9366
+ // empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
9367
+ // went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
9368
+ // data }` by name with the wrap-it hint (measured under #6073); the arm that
9369
+ // still needs a rule is the all-slots-empty container, whose keys are all
9370
+ // declared and which survives the parse untouched.
8196
9371
  {
8197
9372
  name: "validateViewContainers",
8198
9373
  tier: "gating",
@@ -8242,6 +9417,31 @@ var AUTHORING_RULES = [
8242
9417
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8243
9418
  run: (stack) => validateFilterTokens(stack)
8244
9419
  },
9420
+ // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
9421
+ // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
9422
+ // this rule does not touch it: it refuses the literal SPELLINGS at authoring
9423
+ // time with a per-shape prescription, which is Prime Directive #12's standard
9424
+ // shape (reject at the producer, never tolerate at the consumer) and #5240's
9425
+ // same-direction precedent one shape over.
9426
+ {
9427
+ name: "validateEmptyCombinators",
9428
+ tier: "gating",
9429
+ input: "parsed",
9430
+ commands: ALL,
9431
+ source: "packages/lint/src/validate-empty-combinators.ts",
9432
+ // The one type #4463's P1 slice opened, and the one this rule most needs:
9433
+ // a flow CRUD node's `config.filter` is where an empty combinator has the
9434
+ // largest blast radius, and the write path is the only door an AI author
9435
+ // uses. This rule needs NO resolution context at all — it judges the filter
9436
+ // literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
9437
+ // it, and widening to the other filter-carrying types (`object`, `view`,
9438
+ // `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
9439
+ // opens them at the gate. Making that call here would widen the gate's
9440
+ // dispatch surface on this rule's authority, which is P2's decision.
9441
+ surfaces: CLI_AND_RUNTIME,
9442
+ runtimeTypes: ["flow"],
9443
+ run: (stack) => validateEmptyCombinators(stack)
9444
+ },
8245
9445
  // The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
8246
9446
  // rules that answer "does this name resolve to anything?". It reached all
8247
9447
  // three commands before this file existed; it is an entry here so the two
@@ -8290,6 +9490,16 @@ var AUTHORING_RULES = [
8290
9490
  // `displayField` (#5775) — so gating today would fail the platform's own pages
8291
9491
  // to enforce declarations the platform does not keep. The error upgrade is a
8292
9492
  // separate step, once the warning-period inventory is empty.
9493
+ //
9494
+ // #5775 settled the record picker's half: `displayField` is retired in favour
9495
+ // of the `labelField` the renderer actually reads. Its claim that "the rest of
9496
+ // the keys the renderers honour are declared" did NOT hold — #6776 found five
9497
+ // more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
9498
+ // `page:accordion.variant`, and the tab strip's visual style, whose declared
9499
+ // spelling `page:tabs.type` collided with the component node's own dispatch
9500
+ // key and so was unauthorable in the flat and JSX carriers). All five are
9501
+ // declared as of #6776, the last as the renamed `tabStyle`. What remains
9502
+ // before the error upgrade is #5728 and two page rewrites.
8293
9503
  {
8294
9504
  name: "validateComponentProps",
8295
9505
  tier: "advisory",
@@ -8368,10 +9578,12 @@ var AUTHORING_RULES = [
8368
9578
  //
8369
9579
  // `gating` since #5762, which reviewed the file's rules as one family and
8370
9580
  // split them on a single question: is THIS STACK enough to know the flow is
8371
- // dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
9581
+ // dead? Four rules answer yes and emit `error` — a `config.timeRelative`
8372
9582
  // the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
8373
- // predicate cannot route at all, and a `record-*` triggerType outside the
8374
- // closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
9583
+ // predicate cannot route at all, a `record-*` triggerType outside the
9584
+ // closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
9585
+ // `type: 'record_change'` flow whose triggerType the engine's binding resolver
9586
+ // routes nowhere, silently demoting it to a manual flow. None of those verdicts
8375
9587
  // can be changed by installing a package, so there is no reading under which
8376
9588
  // the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
8377
9589
  // (the object may come from another installed package — a hedge this rule
@@ -8498,19 +9710,117 @@ var AUTHORING_RULES = [
8498
9710
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8499
9711
  run: (stack) => validateSeedStateMachine(stack)
8500
9712
  },
8501
- // ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
8502
- // Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
8503
- // during parse, so the alias the author wrote is gone from `result.data`.
9713
+ // ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
9714
+ // gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
9715
+ // schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
9716
+ // the alias the author wrote is gone from `result.data`". Measured false at
9717
+ // #6073: the ADR-0087 D2 conversions do that fold INSIDE
9718
+ // `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
9719
+ // alias site the alias-KEY rule reported zero here too.
9720
+ //
9721
+ // #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
9722
+ // re-anchored. Re-anchoring would have had to move this entry's input to a
9723
+ // pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
9724
+ // a change to this package's external input contract, and the maintainer's
9725
+ // call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
9726
+ // costs no author a signal: the same D2 conversion already shouts through
9727
+ // `warnConversionNotice` in `defineStack`, naming the site, the conversion and
9728
+ // the protocol-16 retirement window — better wording than the rule ever had.
9729
+ //
9730
+ // Every rule left in the family judges the predicate's VALUE, and the value
9731
+ // moves into `visibleWhen` intact, so all three report normally on this tier.
9732
+ // The tier therefore stays `normalized` on its SURVIVING justification (a
9733
+ // finding still reaches the author when an unrelated schema error would stop
9734
+ // the parse — see `AuthoringRuleInputTier`), never on the retired
9735
+ // "pre-parse evidence" one.
9736
+ //
9737
+ // `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
9738
+ // ADR-0089 rules stay advisory findings within it — the tier is a property of
9739
+ // the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
9740
+ // what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
9741
+ // has worked since #3760. The promotion follows the #5762 precedent: a family
9742
+ // that gains an `error` finding moves its registry tier in the same edit.
9743
+ //
9744
+ // ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
9745
+ //
9746
+ // This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
9747
+ // in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
9748
+ // `advisories` channel landed (PR #7435). Before that move a `view` written
9749
+ // through Studio / REST `/meta` / MCP — the only door most tenants have, and
9750
+ // the door AI authors use — was judged by NONE of the family's rule ids (six
9751
+ // at the time of the move; seven since #7659 added
9752
+ // `predicate-rhs-path-shaped` inside the second entry).
9753
+ //
9754
+ // They move together on purpose, and the two entries carry one comment because
9755
+ // they are one wall: #7214's implementer wired its own rule here alone and then
9756
+ // REVERTED it, because a `view` refused for an unresolvable predicate PATH
9757
+ // while a predicate that does not parse at all walks through the same door is
9758
+ // less predictable than refusing neither. A half-wired wall is worse than an
9759
+ // unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
9760
+ // directly: every id on this surface is gated at the runtime door, or none is.
9761
+ //
9762
+ // The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
9763
+ // and re-measuring it at move time found it false: both rule functions read
9764
+ // `stack.views` and `stack.pages` and NO other collection — never `objects` —
9765
+ // so the per-write snapshot the gate builds is not partial for them, it is
9766
+ // complete. (`pages` is simply absent on a `view` write, so the page half
9767
+ // contributes zero findings to both differential passes rather than inventing
9768
+ // any.) The reason was not describing this rule; it was the default a rule got
9769
+ // when nobody measured, which is the #4409/#4463 defect one layer in.
9770
+ //
9771
+ // Runtime input tier: the gate hands the rules the body as persisted, without
9772
+ // `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
9773
+ // door. That costs the family nothing — `validateVisibilityPredicates` reads
9774
+ // `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
9775
+ // so a caller handing it a raw authored object still gets a verdict.
8504
9776
  {
8505
9777
  name: "validateVisibilityPredicates",
8506
- tier: "advisory",
9778
+ tier: "gating",
8507
9779
  input: "normalized",
8508
9780
  commands: ALL,
8509
9781
  source: "packages/lint/src/validate-visibility-predicates.ts",
8510
- surfaces: CLI_ONLY,
8511
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9782
+ surfaces: CLI_AND_RUNTIME,
9783
+ runtimeTypes: ["view"],
8512
9784
  run: (stack) => validateVisibilityPredicates(stack)
8513
9785
  },
9786
+ // #7010 — the same predicate surface, one question further in. The three
9787
+ // ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
9788
+ // rooted, is the root right for the layer) and never open the target schema,
9789
+ // so `data.tpye == 'formula'` passes all three and still resolves to nothing.
9790
+ // This rule resolves the PATH against the schema the form edits — the closed
9791
+ // `getMetadataTypeSchema` key set — and is therefore immune to the CEL
9792
+ // type-name blind spot that made #6248's gate structurally unable to catch
9793
+ // #6254's 16 bare `type ==` predicates.
9794
+ //
9795
+ // Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
9796
+ // the `record.*` layer is deliberately out of scope because an ObjectQL
9797
+ // object's addressable path set is NOT closed (lookup traversal, system
9798
+ // columns, formula outputs), and an `error` gate over an open set generates
9799
+ // false build errors. See the rule's module note.
9800
+ //
9801
+ // #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
9802
+ // resolution question at all: the metadata-admin renderer resolves paths only
9803
+ // on the LEFT of `==` / `!=` and hands the right side to its literal parser,
9804
+ // so `data.a == data.b` resolves both sides cleanly, passes the two rules
9805
+ // above, and still compares against the string "data.b" — a constant verdict.
9806
+ // It carries `error` on a dotted chain (no reading under which it worked) and
9807
+ // `warning` on a bare word (`status == active` compares as the text today, so
9808
+ // refusing it would fail a build over metadata that renders correctly). The
9809
+ // per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
9810
+ // since #3760; the entry's `gating` tier is unchanged because it already was.
9811
+ {
9812
+ name: "validatePredicatePathRefs",
9813
+ tier: "gating",
9814
+ input: "normalized",
9815
+ commands: ALL,
9816
+ source: "packages/lint/src/validate-predicate-path-refs.ts",
9817
+ // The second half of the #7220 family move — see the block above the
9818
+ // `validateVisibilityPredicates` entry. This is the rule whose solo wiring
9819
+ // was reverted; it is wired now because its siblings are.
9820
+ surfaces: CLI_AND_RUNTIME,
9821
+ runtimeTypes: ["view"],
9822
+ run: (stack) => validatePredicatePathRefs(stack)
9823
+ },
8514
9824
  // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
8515
9825
  // `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
8516
9826
  // runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
@@ -8668,16 +9978,110 @@ var AUTHORING_RULES = [
8668
9978
  // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
8669
9979
  // binding gate, vocabulary freeze), moving the failure from a runtime deny to
8670
9980
  // an author-time fix-it. Per ADR-0049 this is not advisory security.
9981
+ //
9982
+ // [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
9983
+ // was false in both halves — it read: "Already gated at this surface by a
9984
+ // DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
9985
+ // `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
9986
+ // on every runtime write. Running the linter here as well would double-report
9987
+ // one refusal in two vocabularies."
9988
+ //
9989
+ // - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
9990
+ // `externalSharingModel` through a local `OWD_WIDTH`, and never touches
9991
+ // `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
9992
+ // block carries it covers ONE — `security-external-wider-than-internal`
9993
+ // (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
9994
+ // corresponds to no lint rule at all, so it is not coverage in the other
9995
+ // direction either. Twelve rules were enforced at no runtime door while
9996
+ // this field said they were.
9997
+ // - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
9998
+ // `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
9999
+ // `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
10000
+ // both refuse by THROWING. The first to fire ends the write, so an author
10001
+ // sees one refusal, never two. The stated cost of moving was imaginary; the
10002
+ // reason it has not moved is the measured one below.
10003
+ //
10004
+ // The move IS taken now — the #7891 programme's three slices, in order:
10005
+ //
10006
+ // - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
10007
+ // the isolation proof that the differential cancels every finding this
10008
+ // function derives from the sibling collections.
10009
+ // - #8309: the snapshot repair. The gate used to carry `objects` and
10010
+ // nothing else, so the three cross-collection rules judged a universe
10011
+ // missing the collection they compare against (measured: 38 phantom
10012
+ // `security-master-detail-ungranted` per-write vs 4 whole-stack,
10013
+ // PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
10014
+ // BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
10015
+ // - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
10016
+ // `object` measured DIRTY on that tree and was escalated, not forced.
10017
+ // - #8310 slice 2 (this state): `object` crosses under the maintainer
10018
+ // ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
10019
+ // authored OWD is REQUIRED at the runtime object door — an object
10020
+ // publish with no authored `sharingModel` is refused with the 422 lint
10021
+ // envelope (`security-owd-unset`); absence is not a decision. The ~16
10022
+ // objectql/rest suite files that relied on OWD-less publishes were
10023
+ // repaired honestly (fixtures author their posture), and
10024
+ // `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
10025
+ // answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
10026
+ // ADR-0094-seam 403 doors answer for what passes lint. The same ruling
10027
+ // retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
10028
+ // of this door (R1 env-tighten-only STAYS — no lint rule covers it);
10029
+ // see `object-posture-gate.ts` and the ADR-0094 amendment.
10030
+ //
10031
+ // `security-role-word` is NOT in this entry any more — that is what the
10032
+ // `validateSecurityRoleWord` entry below records. It judges six collections
10033
+ // (objects, fields, actions, permission sets, positions, apps — plus books),
10034
+ // and `positions`/`apps` are neither carried by the per-write snapshot nor
10035
+ // mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
10036
+ // function that still contained it would have enforced ONE rule id for a
10037
+ // strict subset of its collections: a door where a permission set named
10038
+ // `role_manager` is refused and a position named `sales_role` walks through
10039
+ // — the #7220 failure this table refuses to build, in either direction. The
10040
+ // rule therefore stays behind WHOLE (#8310's explicit call), as its own
10041
+ // entry.
10042
+ //
10043
+ // This entry remains the rest of the D7 block (12 rule ids) as ONE
10044
+ // registration, not a per-rule split: the baseline/candidate differential is
10045
+ // what keeps a write of one declared type from leaking the other rules'
10046
+ // whole-stack findings — every finding derived from a sibling collection is
10047
+ // produced byte-identically in both passes and cancels in the diff. Only
10048
+ // findings the written item itself adds are attributed to the write.
8671
10049
  {
8672
10050
  name: "validateSecurityPosture",
8673
10051
  tier: "gating",
8674
10052
  input: "parsed",
8675
10053
  commands: ALL,
8676
10054
  source: "packages/lint/src/validate-security-posture.ts",
8677
- surfaces: CLI_ONLY,
8678
- 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.",
10055
+ surfaces: CLI_AND_RUNTIME,
10056
+ runtimeTypes: ["seed", "permission", "book", "object"],
8679
10057
  run: (stack) => validateSecurityPosture(stack)
8680
10058
  },
10059
+ // [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
10060
+ // `validateSecurityPosture` the day the rest of that block crossed the
10061
+ // runtime wall — so that it could stay behind WHOLE rather than cross for
10062
+ // three of the six collections it judges (#7220: one rule id must sit on ONE
10063
+ // side of the wall). The split is a surface boundary, not taste: the rule's
10064
+ // verdict and findings are byte-identical to before on every CLI command
10065
+ // (both entries run on all three), and the runtime door does not run it for
10066
+ // ANY type.
10067
+ //
10068
+ // The road to crossing is concrete and short, recorded here so the next
10069
+ // seat prices it correctly: carry `positions`/`apps` in
10070
+ // `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
10071
+ // `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
10072
+ // are real), then declare `runtimeTypes: ['object', 'permission', 'book',
10073
+ // 'position', 'app']` on THIS entry — all six collections in one edit, the
10074
+ // #7220 discipline satisfied.
10075
+ {
10076
+ name: "validateSecurityRoleWord",
10077
+ tier: "gating",
10078
+ input: "parsed",
10079
+ commands: ALL,
10080
+ source: "packages/lint/src/validate-security-posture.ts",
10081
+ surfaces: CLI_ONLY,
10082
+ 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.",
10083
+ run: (stack) => validateSecurityRoleWord(stack)
10084
+ },
8681
10085
  // ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
8682
10086
  // sharing rule that walks it builds a second permission hierarchy (the
8683
10087
  // dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
@@ -8728,7 +10132,7 @@ var AUTHORING_RULES = [
8728
10132
  commands: ALL,
8729
10133
  source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
8730
10134
  surfaces: CLI_ONLY,
8731
- 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.",
10135
+ 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.",
8732
10136
  run: (stack) => validateRlsPredicateEnforceability(stack)
8733
10137
  },
8734
10138
  // #4762 — the same "declared but enforces nothing" question, for the two
@@ -8786,8 +10190,34 @@ var TYPE_TO_STACK_KEY = {
8786
10190
  dashboard: "dashboards",
8787
10191
  agent: "agents",
8788
10192
  hook: "hooks",
8789
- seed: "seeds"
10193
+ // [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
10194
+ // holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
10195
+ // — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
10196
+ // mapping onto one either.
10197
+ //
10198
+ // The wrong spelling was INERT rather than harmless, and it is the #4449 shape
10199
+ // one surface over: the wiring guard asks only that a declared type HAS a
10200
+ // mapping, never that the mapping names a key some rule reads. So it would
10201
+ // have stayed green while the gate built `{ objects, seeds: [item] }` for
10202
+ // every seed write and every rule reading `stack.data` saw nothing — wired,
10203
+ // and running on nothing, with `rulesRun` reporting the rules as having run.
10204
+ // Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
10205
+ // changed no behaviour then; it was corrected here, with the measurement
10206
+ // that found it (#7576), rather than left for the rollout card to trip
10207
+ // over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
10208
+ // mapping is load-bearing today, not merely inert-and-correct.
10209
+ seed: "data",
10210
+ // [#8309] `permission`/`book` map ahead of their registration (#8310), the
10211
+ // same order `seed` arrived in: the mapping plus the enriched snapshot below
10212
+ // are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
10213
+ // a mapping without a declaring rule is inert by construction (the gate
10214
+ // filters by `runtimeTypes` before it ever consults this table), while a
10215
+ // declaration without the mapping is the wired-onto-nothing state the wiring
10216
+ // guard refuses. Landing the mapping first keeps #8310 a registry data edit.
10217
+ permission: "permissions",
10218
+ book: "books"
8790
10219
  };
10220
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
8791
10221
  function runtimeAuthoringRulesFor(type) {
8792
10222
  return AUTHORING_RULES.filter(
8793
10223
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -8805,6 +10235,23 @@ function stackKeyForType(type) {
8805
10235
  return TYPE_TO_STACK_KEY[type] ?? null;
8806
10236
  }
8807
10237
  var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
10238
+ function buildRuntimeWriteSnapshots(args) {
10239
+ const stackKey = stackKeyForType(args.type);
10240
+ if (!stackKey) return null;
10241
+ if (!args.item || typeof args.item !== "object") return null;
10242
+ const item = args.item;
10243
+ const itemName = typeof item.name === "string" ? item.name : void 0;
10244
+ const baseline = {};
10245
+ for (const key of CONTEXT_STACK_KEYS) {
10246
+ const collection = args.context?.[key] ?? [];
10247
+ baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10248
+ }
10249
+ const candidate = {
10250
+ ...baseline,
10251
+ [stackKey]: [...baseline[stackKey] ?? [], item]
10252
+ };
10253
+ return { baseline, candidate };
10254
+ }
8808
10255
  function runRules(rules, stack, ctx) {
8809
10256
  const findings = [];
8810
10257
  for (const rule of rules) {
@@ -8827,19 +10274,15 @@ function runRuntimeAuthoringRules(args) {
8827
10274
  const rules = runtimeAuthoringRulesFor(args.type);
8828
10275
  const empty = { errors: [], advisories: [], rulesRun: [] };
8829
10276
  if (rules.length === 0) return empty;
8830
- const stackKey = stackKeyForType(args.type);
8831
- if (!stackKey) return empty;
8832
- if (!args.item || typeof args.item !== "object") return empty;
8833
- const item = args.item;
8834
- const itemName = typeof item.name === "string" ? item.name : void 0;
8835
- const contextObjects = args.context?.objects ?? [];
10277
+ const snapshots = buildRuntimeWriteSnapshots({
10278
+ type: args.type,
10279
+ item: args.item,
10280
+ ...args.context !== void 0 ? { context: args.context } : {}
10281
+ });
10282
+ if (!snapshots) return empty;
8836
10283
  const ctx = { sduiManifest: args.sduiManifest };
8837
- const writesIntoContext = stackKey === "objects";
8838
- const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
8839
- const baseline = { objects: baselineObjects };
8840
- const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
8841
- const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
8842
- const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
10284
+ const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10285
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
8843
10286
  return {
8844
10287
  errors: added.filter((f) => f.severity === "error"),
8845
10288
  advisories: added.filter((f) => f.severity !== "error"),
@@ -8848,6 +10291,7 @@ function runRuntimeAuthoringRules(args) {
8848
10291
  }
8849
10292
  // Annotate the CommonJS export names for ESM import in node:
8850
10293
  0 && (module.exports = {
10294
+ buildRuntimeWriteSnapshots,
8851
10295
  runRuntimeAuthoringRules,
8852
10296
  runtimeAuthoringRulesFor,
8853
10297
  runtimeGatedTypes,