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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/validate-expressions.ts
2
- import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
2
+ import { validateExpression, collectCelRootIdentifiers, SCOPE_ROOTS } from "@objectstack/formula";
3
3
  import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
4
4
 
5
5
  // src/system-fields.ts
@@ -371,6 +371,38 @@ function validateStackExpressions(stack) {
371
371
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
372
372
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
373
373
  };
374
+ const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
375
+ const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
376
+ 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";
377
+ const FIELD_RULE_SLOT_CONSEQUENCE = {
378
+ 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)",
379
+ 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",
380
+ 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",
381
+ // Listed rather than left to the `??` below, so the map covers every slot
382
+ // the field walk passes and the default stays unreachable. `FieldSchema`
383
+ // declares this key only as a `retiredKey`, which rejects it by name, so
384
+ // there is no fourth runtime to measure — the honest clause is the generic
385
+ // one, not a fabricated fourth cell (#6716).
386
+ conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
387
+ };
388
+ const checkFieldRuleRoot = (where, slot, raw) => {
389
+ const source = celSourceOf(raw);
390
+ if (!source) return;
391
+ const roots = collectCelRootIdentifiers(source);
392
+ if (!roots.ok) return;
393
+ const kept = SCOPE_ROOTS.filter(
394
+ (r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
395
+ );
396
+ if (kept.length === 0) return;
397
+ const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
398
+ 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}\`.`;
399
+ issues.push({
400
+ where,
401
+ 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,
402
+ source,
403
+ severity: "error"
404
+ });
405
+ };
374
406
  const checkDeclaredPredicate = (where, raw) => {
375
407
  if (raw == null) return;
376
408
  const res = validateExpression("predicate", raw);
@@ -403,7 +435,14 @@ function validateStackExpressions(stack) {
403
435
  if (retired.length > 0) {
404
436
  issues.push({
405
437
  where: `${at} \xB7 node '${node.id}' (script) callable`,
406
- message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + `Run \`os migrate meta --from 16\` to rewrite it automatically.`,
438
+ 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
439
+ // behaviour, never the retired key's fate — "rewrite it" reads two ways
440
+ // over a branch that DELETES the key (template/recipients/variables/script),
441
+ // "rewrite existing sources" only one. Plain-quoted (not a template literal)
442
+ // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
443
+ // widened scan (#7030) on the same textual shape as the spec corpus — no
444
+ // interpolation lives in this clause, so nothing is lost switching quote style.
445
+ "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
407
446
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
408
447
  });
409
448
  } else if (!fn) {
@@ -437,13 +476,27 @@ function validateStackExpressions(stack) {
437
476
  for (const [fname, f] of fieldList) {
438
477
  for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
439
478
  check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
479
+ checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
440
480
  }
441
- const roWhenSource = celSourceOf(f.readonlyWhen);
442
- if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
481
+ for (const [oi, opt] of asArray(f.options).entries()) {
482
+ const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
483
+ check(
484
+ `object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
485
+ opt.visibleWhen,
486
+ objectName,
487
+ "record"
488
+ );
489
+ }
490
+ for (const [slot, raw, consequence2] of [
491
+ ["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
492
+ ["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`]
493
+ ]) {
494
+ const source = celSourceOf(raw);
495
+ if (masters === 1 || !source || !readsParentRoot(source)) continue;
443
496
  issues.push({
444
- where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
445
- message: `\`readonlyWhen\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and the field would be locked on every write. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
446
- source: roWhenSource,
497
+ where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
498
+ 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\`.`),
499
+ source,
447
500
  severity: "error"
448
501
  });
449
502
  }
@@ -1171,7 +1224,8 @@ function validateDashboardActionRefs(stack) {
1171
1224
 
1172
1225
  // src/validate-filter-tokens.ts
1173
1226
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
1174
- var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1227
+
1228
+ // src/filter-walk.ts
1175
1229
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
1176
1230
  function asArray5(v) {
1177
1231
  if (Array.isArray(v)) return v;
@@ -1183,7 +1237,62 @@ function asArray5(v) {
1183
1237
  function label(v, fallback) {
1184
1238
  return typeof v === "string" && v.length > 0 ? v : fallback;
1185
1239
  }
1240
+ function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
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, visit, seen));
1246
+ return;
1247
+ }
1248
+ for (const [k, v] of Object.entries(node)) {
1249
+ const childPath = `${path}.${k}`;
1250
+ if (FILTER_KEYS.has(k)) {
1251
+ visit({ value: v, path: childPath, where });
1252
+ continue;
1253
+ }
1254
+ scanForFilters(v, childPath, where, visit, seen);
1255
+ }
1256
+ }
1257
+ function walkAuthoredFilters(stack, surfaces, visit) {
1258
+ if (!stack || typeof stack !== "object") return;
1259
+ for (const { key, kind } of surfaces) {
1260
+ const items = asArray5(stack[key]);
1261
+ items.forEach((item, i) => {
1262
+ const name = label(item.name ?? item.id, `#${i}`);
1263
+ if (kind === "dashboard") {
1264
+ const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1265
+ widgets.forEach((w, wi) => {
1266
+ const wName = label(w.id ?? w.title, `#${wi}`);
1267
+ scanForFilters(
1268
+ w,
1269
+ `${key}[${i}].widgets[${wi}]`,
1270
+ `dashboard "${name}" \xB7 widget "${wName}"`,
1271
+ visit,
1272
+ /* @__PURE__ */ new Set()
1273
+ );
1274
+ });
1275
+ const { widgets: _skip, ...rest } = item;
1276
+ scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
1277
+ return;
1278
+ }
1279
+ scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
1280
+ });
1281
+ }
1282
+ }
1283
+
1284
+ // src/validate-filter-tokens.ts
1285
+ var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1186
1286
  var KNOWN_LIST = CONTEXT_TOKENS.join("}, {");
1287
+ var TOKEN_FILTER_SURFACES = [
1288
+ { key: "dashboards", kind: "dashboard" },
1289
+ { key: "objects", kind: "object" },
1290
+ { key: "views", kind: "view" },
1291
+ { key: "reports", kind: "report" },
1292
+ { key: "datasets", kind: "dataset" },
1293
+ { key: "pages", kind: "page" },
1294
+ { key: "apps", kind: "app" }
1295
+ ];
1187
1296
  function walkFilterValues(node, path, where, out, seen) {
1188
1297
  if (node === null || node === void 0) return;
1189
1298
  if (typeof node === "string") {
@@ -1212,58 +1321,132 @@ function walkFilterValues(node, path, where, out, seen) {
1212
1321
  walkFilterValues(v, `${path}.${k}`, where, out, seen);
1213
1322
  }
1214
1323
  }
1215
- function scanForFilters(node, path, where, out, seen) {
1216
- if (!node || typeof node !== "object") return;
1217
- if (seen.has(node)) return;
1218
- seen.add(node);
1219
- if (Array.isArray(node)) {
1220
- node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, out, seen));
1324
+ function validateFilterTokens(stack) {
1325
+ if (!stack || typeof stack !== "object") return [];
1326
+ const out = [];
1327
+ walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
1328
+ walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
1329
+ });
1330
+ return out;
1331
+ }
1332
+
1333
+ // src/validate-empty-combinators.ts
1334
+ import { reduceFilterVerdict } from "@objectstack/spec/data";
1335
+ var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
1336
+ var FILTER_EMPTY_NODE = "filter-empty-node";
1337
+ var EMPTY_COMBINATOR_SURFACES = [
1338
+ { key: "dashboards", kind: "dashboard" },
1339
+ { key: "objects", kind: "object" },
1340
+ { key: "views", kind: "view" },
1341
+ { key: "reports", kind: "report" },
1342
+ { key: "datasets", kind: "dataset" },
1343
+ { key: "pages", kind: "page" },
1344
+ { key: "apps", kind: "app" },
1345
+ { key: "flows", kind: "flow" }
1346
+ ];
1347
+ function isFilterNode(value) {
1348
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1349
+ const proto = Object.getPrototypeOf(value);
1350
+ return proto === Object.prototype || proto === null;
1351
+ }
1352
+ var VERDICT_OF = {
1353
+ $and: reduceFilterVerdict({ $and: [] }),
1354
+ $or: reduceFilterVerdict({ $or: [] }),
1355
+ $not: reduceFilterVerdict({ $not: {} }),
1356
+ node: reduceFilterVerdict({}),
1357
+ /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
1358
+ orWithEmptyBranch: reduceFilterVerdict({ $or: [{ status: "open" }, {}] })
1359
+ };
1360
+ function rows(verdict) {
1361
+ if (verdict === "true") return "matches EVERY row";
1362
+ if (verdict === "false") return "matches NO row";
1363
+ return "carries a real predicate";
1364
+ }
1365
+ 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.";
1366
+ 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).';
1367
+ function emitEmptyCombinator(key, path, ctx) {
1368
+ const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
1369
+ 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.`;
1370
+ 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}`;
1371
+ ctx.out.push({
1372
+ severity: "error",
1373
+ rule: FILTER_EMPTY_COMBINATOR,
1374
+ where: ctx.where,
1375
+ path,
1376
+ message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
1377
+ 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.`
1378
+ });
1379
+ }
1380
+ function emitEmptyNode(position, path, ctx) {
1381
+ if (position === "root") {
1382
+ ctx.out.push({
1383
+ severity: "error",
1384
+ rule: FILTER_EMPTY_NODE,
1385
+ where: ctx.where,
1386
+ path,
1387
+ 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.`,
1388
+ hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
1389
+ });
1221
1390
  return;
1222
1391
  }
1223
- for (const [k, v] of Object.entries(node)) {
1224
- const childPath = `${path}.${k}`;
1225
- if (FILTER_KEYS.has(k)) {
1226
- walkFilterValues(v, childPath, where, out, /* @__PURE__ */ new Set());
1392
+ if (position === "or-branch") {
1393
+ ctx.out.push({
1394
+ severity: "error",
1395
+ rule: FILTER_EMPTY_NODE,
1396
+ where: ctx.where,
1397
+ path,
1398
+ 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.`,
1399
+ 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.)"
1400
+ });
1401
+ return;
1402
+ }
1403
+ ctx.out.push({
1404
+ severity: "error",
1405
+ rule: FILTER_EMPTY_NODE,
1406
+ where: ctx.where,
1407
+ path,
1408
+ 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.",
1409
+ 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."
1410
+ });
1411
+ }
1412
+ function scanNodeKeys(node, path, ctx) {
1413
+ for (const [key, value] of Object.entries(node)) {
1414
+ if (key === "$and" || key === "$or") {
1415
+ if (!Array.isArray(value)) continue;
1416
+ if (value.length === 0) {
1417
+ emitEmptyCombinator(key, `${path}.${key}`, ctx);
1418
+ continue;
1419
+ }
1420
+ value.forEach((element, index) => {
1421
+ scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
1422
+ });
1423
+ continue;
1424
+ }
1425
+ if (key === "$not") {
1426
+ if (!isFilterNode(value)) continue;
1427
+ if (Object.keys(value).length === 0) {
1428
+ emitEmptyCombinator("$not", `${path}.$not`, ctx);
1429
+ continue;
1430
+ }
1431
+ scanNodeKeys(value, `${path}.$not`, ctx);
1227
1432
  continue;
1228
1433
  }
1229
- scanForFilters(v, childPath, where, out, seen);
1230
1434
  }
1231
1435
  }
1232
- function validateFilterTokens(stack) {
1436
+ function scanBranch(value, path, position, ctx) {
1437
+ if (!isFilterNode(value)) return;
1438
+ if (Object.keys(value).length === 0) {
1439
+ emitEmptyNode(position, path, ctx);
1440
+ return;
1441
+ }
1442
+ scanNodeKeys(value, path, ctx);
1443
+ }
1444
+ function validateEmptyCombinators(stack) {
1233
1445
  if (!stack || typeof stack !== "object") return [];
1234
1446
  const out = [];
1235
- const surfaces = [
1236
- ["dashboards", "dashboard"],
1237
- ["objects", "object"],
1238
- ["views", "view"],
1239
- ["reports", "report"],
1240
- ["datasets", "dataset"],
1241
- ["pages", "page"],
1242
- ["apps", "app"]
1243
- ];
1244
- for (const [key, kind] of surfaces) {
1245
- const items = asArray5(stack[key]);
1246
- items.forEach((item, i) => {
1247
- const name = label(item.name ?? item.id, `#${i}`);
1248
- if (kind === "dashboard") {
1249
- const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1250
- widgets.forEach((w, wi) => {
1251
- const wName = label(w.id ?? w.title, `#${wi}`);
1252
- scanForFilters(
1253
- w,
1254
- `${key}[${i}].widgets[${wi}]`,
1255
- `dashboard "${name}" \xB7 widget "${wName}"`,
1256
- out,
1257
- /* @__PURE__ */ new Set()
1258
- );
1259
- });
1260
- const { widgets: _skip, ...rest } = item;
1261
- scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
1262
- return;
1263
- }
1264
- scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
1265
- });
1266
- }
1447
+ walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
1448
+ scanBranch(value, path, "root", { where, out });
1449
+ });
1267
1450
  return out;
1268
1451
  }
1269
1452
 
@@ -1463,6 +1646,7 @@ function validateObjectReferences(stack) {
1463
1646
  // src/validate-searchable-fields.ts
1464
1647
  import {
1465
1648
  resolveSearchFieldResolution,
1649
+ isVirtualSearchField,
1466
1650
  SEARCHABLE_TEXTUAL_TYPES,
1467
1651
  SEARCHABLE_ENUM_TYPES,
1468
1652
  SEARCH_AUTO_EXCLUDED_FIELDS
@@ -1580,7 +1764,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1580
1764
  where,
1581
1765
  path: `${path}[${i}]`,
1582
1766
  message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest3(name, known)),
1583
- hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a formula field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1767
+ 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(", ")}.` : "")
1768
+ });
1769
+ continue;
1770
+ }
1771
+ if (isVirtualSearchField(target.fields[name])) {
1772
+ const vtype = target.fields[name]?.type;
1773
+ findings.push({
1774
+ severity: "error",
1775
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1776
+ where,
1777
+ path: `${path}[${i}]`,
1778
+ 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).`,
1779
+ hint: `Mirror the computed value onto a stored text field on "${objectName}" and declare that instead, or drop "${name}". At runtime the ingress gate now refuses this entry with 400 INVALID_FIELD, the same answer a stale entry gets (#4254).`
1584
1780
  });
1585
1781
  continue;
1586
1782
  }
@@ -1615,7 +1811,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1615
1811
  where,
1616
1812
  path: `${path}[${i}]`,
1617
1813
  message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
1618
- hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text/formula field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1814
+ hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1619
1815
  });
1620
1816
  }
1621
1817
  return findings;
@@ -1934,6 +2130,15 @@ function validateActionNameRefs(stack) {
1934
2130
  "Navigation action item"
1935
2131
  );
1936
2132
  }
2133
+ const runAction = strName5(nav.runAction);
2134
+ if (nav.type === "object" && runAction) {
2135
+ check(
2136
+ runAction,
2137
+ `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2138
+ `${navPath}.runAction`,
2139
+ "Navigation deep-link auto-run"
2140
+ );
2141
+ }
1937
2142
  if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
1938
2143
  }
1939
2144
  };
@@ -1999,8 +2204,21 @@ var COMPONENT_FIELD_SPECS = {
1999
2204
  "element:number": { props: ["field"] },
2000
2205
  "element:filter": { props: ["fields"] },
2001
2206
  "element:form": { props: ["fields"] },
2002
- // The schema says `displayField`; real pages author `labelField`. Accept both.
2003
- "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
2207
+ // `labelField` is the one field-bearing prop this element declares. Its former
2208
+ // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
2209
+ // `searchFields` (deleted, ADR-0049) were retired in #5775 and are
2210
+ // `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
2211
+ // spec-conformant page carries either, and this rule's job (resolve a field
2212
+ // NAME against the object) is not the question a retired key raises (#6629).
2213
+ //
2214
+ // A non-conformant page that writes one anyway is not left unattended: the
2215
+ // #5068 props gate reports the key with its rename/delete prescription. That
2216
+ // gate is advisory and CLI-only and lives in a different registry
2217
+ // (`authoring-rules`) from this suite, so it neither precedes nor suppresses
2218
+ // this rule — what these two entries actually added was a SECOND finding,
2219
+ // saying a field named by a key that no longer exists does not exist either.
2220
+ // The prescription is the useful half; this half was noise on top of it.
2221
+ "element:record_picker": { props: ["labelField"] }
2004
2222
  };
2005
2223
  var RELATED_LIST_TYPE = "record:related_list";
2006
2224
  function componentFieldRefs(type, props, basePath, sep = ".") {
@@ -2569,18 +2787,57 @@ function validateNavTargetRefs(stack) {
2569
2787
  }
2570
2788
 
2571
2789
  // src/validate-translation-references.ts
2790
+ import { expandViewContainer } from "@objectstack/spec";
2572
2791
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
2792
+
2793
+ // src/view-walk.ts
2794
+ function isRec7(v) {
2795
+ return !!v && typeof v === "object" && !Array.isArray(v);
2796
+ }
2797
+ function strName10(v) {
2798
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2799
+ }
2800
+ function viewObjectName(view) {
2801
+ return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
2802
+ }
2803
+ function viewContainerSites(view, basePath) {
2804
+ if (!isRec7(view)) return [];
2805
+ const sites = [{ view, path: basePath, surface: "", kind: "self" }];
2806
+ if (isRec7(view.form)) {
2807
+ sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
2808
+ }
2809
+ for (const key of ["listViews", "formViews"]) {
2810
+ const container = view[key];
2811
+ if (!isRec7(container)) continue;
2812
+ const kind = key === "listViews" ? "listView" : "formView";
2813
+ for (const [subKey, sub] of Object.entries(container)) {
2814
+ if (!isRec7(sub)) continue;
2815
+ sites.push({
2816
+ view: sub,
2817
+ path: `${basePath}.${key}.${subKey}`,
2818
+ surface: `${key}.${subKey}`,
2819
+ kind
2820
+ });
2821
+ }
2822
+ }
2823
+ return sites;
2824
+ }
2825
+ function formViewSites(view, basePath) {
2826
+ return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
2827
+ }
2828
+
2829
+ // src/validate-translation-references.ts
2573
2830
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
2574
2831
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
2575
- function isRec7(v) {
2832
+ function isRec8(v) {
2576
2833
  return !!v && typeof v === "object" && !Array.isArray(v);
2577
2834
  }
2578
2835
  function asArray14(v) {
2579
2836
  if (Array.isArray(v)) return v;
2580
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
2837
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec8(def) ? def : {} }));
2581
2838
  return [];
2582
2839
  }
2583
- function strName10(v) {
2840
+ function strName11(v) {
2584
2841
  return typeof v === "string" && v.length > 0 ? v : void 0;
2585
2842
  }
2586
2843
  function distance5(a, b) {
@@ -2641,29 +2898,51 @@ function collectViewRecord(view, factsFor) {
2641
2898
  const addSections = (container, binding) => {
2642
2899
  if (!binding) return;
2643
2900
  for (const section of asArray14(container.sections)) {
2644
- const sectionName = strName10(section.name);
2901
+ const sectionName = strName11(section.name);
2645
2902
  if (sectionName) factsFor(binding).sections.add(sectionName);
2646
2903
  }
2647
2904
  };
2648
- const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
2649
- if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
2650
- addView(recordObject ?? listBinding, strName10(view.name));
2651
- for (const key of ["listViews", "formViews"]) {
2652
- const container = view[key];
2653
- if (!isRec7(container)) continue;
2654
- for (const [subKey, sub] of Object.entries(container)) {
2655
- if (!isRec7(sub)) continue;
2905
+ const listBinding = isRec8(view.list) ? bindingOf(view.list) : void 0;
2906
+ if (isRec8(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
2907
+ addView(recordObject ?? listBinding, strName11(view.name));
2908
+ const named = namedViewKeys(view);
2909
+ for (const family of ["listViews", "formViews"]) {
2910
+ const container = view[family];
2911
+ if (!isRec8(container)) continue;
2912
+ const registryKeys = family === "listViews" ? named.list : named.form;
2913
+ let at = 0;
2914
+ for (const sub of Object.values(container)) {
2915
+ if (!sub || typeof sub !== "object") continue;
2916
+ const registryKey = registryKeys[at++];
2917
+ if (!isRec8(sub)) continue;
2656
2918
  const binding = bindingOf(sub) ?? listBinding;
2657
- addView(binding, subKey);
2658
- addView(binding, strName10(sub.name));
2919
+ addView(binding, registryKey);
2659
2920
  addSections(sub, binding);
2660
2921
  }
2661
2922
  }
2662
- if (isRec7(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
2923
+ if (isRec8(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
2663
2924
  addSections(view, recordObject ?? listBinding);
2664
2925
  }
2665
- function viewObjectName(view) {
2666
- return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
2926
+ function defaultListViewKey(object, container) {
2927
+ if (!object || !isRec8(container.list)) return void 0;
2928
+ const item = expandViewContainer(object, container).find(
2929
+ (i) => i.viewKind === "list" && i.isDefault
2930
+ );
2931
+ if (!item) return void 0;
2932
+ const prefix = `${object}.`;
2933
+ return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
2934
+ }
2935
+ function namedViewKeys(container) {
2936
+ const object = "probe";
2937
+ const prefix = `${object}.`;
2938
+ const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
2939
+ const countEntries = (v) => isRec8(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
2940
+ const listCount = countEntries(container.listViews);
2941
+ const formCount = countEntries(container.formViews);
2942
+ if (!listCount && !formCount) return { list: [], form: [] };
2943
+ const items = expandViewContainer(object, container);
2944
+ const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
2945
+ return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
2667
2946
  }
2668
2947
  function readOptions(field) {
2669
2948
  const raw = field.options;
@@ -2675,14 +2954,14 @@ function readOptions(field) {
2675
2954
  values.add(opt);
2676
2955
  continue;
2677
2956
  }
2678
- if (!isRec7(opt)) continue;
2679
- const value = strName10(opt.value);
2957
+ if (!isRec8(opt)) continue;
2958
+ const value = strName11(opt.value);
2680
2959
  if (!value) continue;
2681
2960
  values.add(value);
2682
- const label2 = strName10(opt.label);
2961
+ const label2 = strName11(opt.label);
2683
2962
  if (label2) byLabel.set(label2.toLowerCase(), value);
2684
2963
  }
2685
- } else if (isRec7(raw)) {
2964
+ } else if (isRec8(raw)) {
2686
2965
  for (const [value, label2] of Object.entries(raw)) {
2687
2966
  values.add(value);
2688
2967
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -2703,23 +2982,23 @@ function buildUniverse(stack) {
2703
2982
  return facts;
2704
2983
  };
2705
2984
  for (const obj of asArray14(stack.objects)) {
2706
- const objectName = strName10(obj.name);
2985
+ const objectName = strName11(obj.name);
2707
2986
  if (!objectName) continue;
2708
2987
  const facts = factsFor(objectName);
2709
2988
  for (const field of asArray14(obj.fields)) {
2710
- const fieldName = strName10(field.name);
2989
+ const fieldName = strName11(field.name);
2711
2990
  if (fieldName) facts.fields.set(fieldName, field);
2712
2991
  }
2713
2992
  for (const action of asArray14(obj.actions)) {
2714
- const actionName = strName10(action.name);
2993
+ const actionName = strName11(action.name);
2715
2994
  if (actionName) facts.actions.set(actionName, action);
2716
2995
  }
2717
2996
  for (const view of asArray14(obj.views)) {
2718
- collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
2997
+ collectViewRecord({ ...view, object: strName11(view.object) ?? objectName }, factsFor);
2719
2998
  }
2720
2999
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
2721
3000
  for (const group of asArray14(obj.fieldGroups)) {
2722
- const key = strName10(group.key) ?? strName10(group.name);
3001
+ const key = strName11(group.key) ?? strName11(group.name);
2723
3002
  if (key) facts.sections.add(key);
2724
3003
  }
2725
3004
  }
@@ -2730,10 +3009,10 @@ function buildUniverse(stack) {
2730
3009
  for (let pi = 0; pi < pages.length; pi++) {
2731
3010
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
2732
3011
  if (!walked.objectName) continue;
2733
- const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
3012
+ const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
2734
3013
  if (!props) continue;
2735
3014
  for (const section of asArray14(props.sections)) {
2736
- const sectionName = strName10(section.name);
3015
+ const sectionName = strName11(section.name);
2737
3016
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
2738
3017
  }
2739
3018
  }
@@ -2741,9 +3020,9 @@ function buildUniverse(stack) {
2741
3020
  const globalActions = /* @__PURE__ */ new Map();
2742
3021
  const actionOwners = /* @__PURE__ */ new Map();
2743
3022
  for (const action of asArray14(stack.actions)) {
2744
- const actionName = strName10(action.name);
3023
+ const actionName = strName11(action.name);
2745
3024
  if (!actionName) continue;
2746
- const owner = strName10(action.objectName) ?? strName10(action.object);
3025
+ const owner = strName11(action.objectName) ?? strName11(action.object);
2747
3026
  if (owner) {
2748
3027
  factsFor(owner).actions.set(actionName, action);
2749
3028
  actionOwners.set(actionName, owner);
@@ -2758,19 +3037,19 @@ function buildUniverse(stack) {
2758
3037
  }
2759
3038
  const apps = /* @__PURE__ */ new Map();
2760
3039
  for (const app of asArray14(stack.apps)) {
2761
- const appName = strName10(app.name);
3040
+ const appName = strName11(app.name);
2762
3041
  if (!appName) continue;
2763
3042
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
2764
3043
  const walkNav = (items) => {
2765
3044
  for (const item of asArray14(items)) {
2766
- const id = strName10(item.id);
3045
+ const id = strName11(item.id);
2767
3046
  if (id) navIds.add(id);
2768
3047
  if (item.children) walkNav(item.children);
2769
3048
  }
2770
3049
  };
2771
3050
  walkNav(app.navigation);
2772
3051
  for (const area of asArray14(app.areas)) {
2773
- const areaId = strName10(area.id);
3052
+ const areaId = strName11(area.id);
2774
3053
  if (areaId) navIds.add(areaId);
2775
3054
  walkNav(area.navigation);
2776
3055
  }
@@ -2778,20 +3057,20 @@ function buildUniverse(stack) {
2778
3057
  }
2779
3058
  const dashboards = /* @__PURE__ */ new Map();
2780
3059
  for (const dash of asArray14(stack.dashboards)) {
2781
- const dashName = strName10(dash.name);
3060
+ const dashName = strName11(dash.name);
2782
3061
  if (!dashName) continue;
2783
3062
  const widgets = /* @__PURE__ */ new Set();
2784
3063
  for (const widget of asArray14(dash.widgets)) {
2785
- const id = strName10(widget.id) ?? strName10(widget.name);
3064
+ const id = strName11(widget.id) ?? strName11(widget.name);
2786
3065
  if (id) widgets.add(id);
2787
3066
  }
2788
3067
  const actions = /* @__PURE__ */ new Set();
2789
3068
  const headerActions = [
2790
- ...asArray14(isRec7(dash.header) ? dash.header.actions : void 0),
3069
+ ...asArray14(isRec8(dash.header) ? dash.header.actions : void 0),
2791
3070
  ...asArray14(dash.actions)
2792
3071
  ];
2793
3072
  for (const action of headerActions) {
2794
- const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
3073
+ const key = strName11(action.actionUrl) ?? strName11(action.url) ?? strName11(action.name);
2795
3074
  if (key) actions.add(key);
2796
3075
  }
2797
3076
  dashboards.set(dashName, { widgets, actions });
@@ -2803,7 +3082,7 @@ function localePath(bundleIndex, locale) {
2803
3082
  }
2804
3083
  function validateTranslationReferences(stack) {
2805
3084
  const findings = [];
2806
- if (!isRec7(stack)) return findings;
3085
+ if (!isRec8(stack)) return findings;
2807
3086
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
2808
3087
  if (bundles.length === 0) return findings;
2809
3088
  const universe = buildUniverse(stack);
@@ -2812,13 +3091,13 @@ function validateTranslationReferences(stack) {
2812
3091
  };
2813
3092
  for (let bi = 0; bi < bundles.length; bi++) {
2814
3093
  const bundle = bundles[bi];
2815
- if (!isRec7(bundle)) continue;
3094
+ if (!isRec8(bundle)) continue;
2816
3095
  for (const [locale, rawData] of Object.entries(bundle)) {
2817
- if (!isRec7(rawData)) continue;
3096
+ if (!isRec8(rawData)) continue;
2818
3097
  const base = localePath(bi, locale);
2819
3098
  const inLocale = `locale "${locale}"`;
2820
3099
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
2821
- if (!isRec7(rawNode)) continue;
3100
+ if (!isRec8(rawNode)) continue;
2822
3101
  const objPath = `${base}.objects.${objectName}`;
2823
3102
  const facts = universe.objects.get(objectName);
2824
3103
  if (!facts) {
@@ -2844,7 +3123,7 @@ function validateTranslationReferences(stack) {
2844
3123
  );
2845
3124
  continue;
2846
3125
  }
2847
- if (!isRec7(rawField)) continue;
3126
+ if (!isRec8(rawField)) continue;
2848
3127
  checkOptionKeys(findings, {
2849
3128
  optionMap: rawField.options,
2850
3129
  field,
@@ -2926,7 +3205,7 @@ function validateTranslationReferences(stack) {
2926
3205
  );
2927
3206
  continue;
2928
3207
  }
2929
- if (!isRec7(rawApp)) continue;
3208
+ if (!isRec8(rawApp)) continue;
2930
3209
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
2931
3210
  if (navIds.has(navId)) continue;
2932
3211
  orphan(
@@ -2949,7 +3228,7 @@ function validateTranslationReferences(stack) {
2949
3228
  );
2950
3229
  continue;
2951
3230
  }
2952
- if (!isRec7(rawDash)) continue;
3231
+ if (!isRec8(rawDash)) continue;
2953
3232
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
2954
3233
  if (dash.widgets.has(widgetId)) continue;
2955
3234
  orphan(
@@ -2974,7 +3253,7 @@ function validateTranslationReferences(stack) {
2974
3253
  return findings;
2975
3254
  }
2976
3255
  function asRecord(v) {
2977
- return isRec7(v) ? v : {};
3256
+ return isRec8(v) ? v : {};
2978
3257
  }
2979
3258
  function checkOptionKeys(findings, ctx) {
2980
3259
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -2986,7 +3265,7 @@ function checkOptionKeys(findings, ctx) {
2986
3265
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
2987
3266
  where: ctx.where,
2988
3267
  path: ctx.path,
2989
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName10(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
3268
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName11(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
2990
3269
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
2991
3270
  });
2992
3271
  return;
@@ -3005,11 +3284,11 @@ function checkOptionKeys(findings, ctx) {
3005
3284
  }
3006
3285
  }
3007
3286
  function checkActionParams(findings, ctx) {
3008
- const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
3287
+ const rawParams = Object.keys(asRecord(isRec8(ctx.rawAction) ? ctx.rawAction.params : void 0));
3009
3288
  if (rawParams.length === 0) return;
3010
3289
  const declared = /* @__PURE__ */ new Set();
3011
3290
  for (const param of asArray14(ctx.action.params)) {
3012
- const name = strName10(param.name) ?? strName10(param.field);
3291
+ const name = strName11(param.name) ?? strName11(param.field);
3013
3292
  if (name) declared.add(name);
3014
3293
  }
3015
3294
  for (const paramName of rawParams) {
@@ -3025,78 +3304,60 @@ function checkActionParams(findings, ctx) {
3025
3304
  }
3026
3305
  }
3027
3306
 
3028
- // src/validate-translatable-sections.ts
3029
- var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3030
- function isRec8(v) {
3307
+ // src/collection-entries.ts
3308
+ function isRec9(v) {
3031
3309
  return !!v && typeof v === "object" && !Array.isArray(v);
3032
3310
  }
3033
- function strName11(v) {
3034
- return typeof v === "string" && v.length > 0 ? v : void 0;
3035
- }
3036
- function viewObjectName2(view) {
3037
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
3038
- }
3039
3311
  function collectionEntries(v, base) {
3040
3312
  if (Array.isArray(v)) {
3041
3313
  const out = [];
3042
3314
  for (let i = 0; i < v.length; i++) {
3043
- if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3315
+ if (isRec9(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3044
3316
  }
3045
3317
  return out;
3046
3318
  }
3047
- if (isRec8(v)) {
3048
- return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3319
+ if (isRec9(v)) {
3320
+ return Object.entries(v).filter(([, def]) => isRec9(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3049
3321
  }
3050
3322
  return [];
3051
3323
  }
3324
+
3325
+ // src/validate-translatable-sections.ts
3326
+ var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3327
+ function isRec10(v) {
3328
+ return !!v && typeof v === "object" && !Array.isArray(v);
3329
+ }
3330
+ function strName12(v) {
3331
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3332
+ }
3052
3333
  function viewLabel(view) {
3053
- const name = strName11(view.name);
3334
+ const name = strName12(view.name);
3054
3335
  return name ? `view "${name}"` : "";
3055
3336
  }
3056
3337
  function joinWhere(...parts) {
3057
3338
  return parts.filter((p) => p.length > 0).join(" \xB7 ");
3058
3339
  }
3059
3340
  function collectViewSites(view, basePath, label2, sites) {
3060
- const recordObject = viewObjectName2(view);
3061
- const listBinding = isRec8(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
3062
- const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
3063
- sites.push({
3064
- path: `${basePath}.sections`,
3065
- surface: label2,
3066
- objectName: recordObject ?? listBinding,
3067
- sections: view.sections
3068
- });
3069
- if (isRec8(view.form)) {
3341
+ const recordObject = viewObjectName(view);
3342
+ const listBinding = isRec10(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3343
+ for (const site of viewContainerSites(view, basePath)) {
3070
3344
  sites.push({
3071
- path: `${basePath}.form.sections`,
3072
- surface: joinWhere(label2, "form"),
3073
- objectName: bindingOf(view.form) ?? listBinding,
3074
- sections: view.form.sections
3345
+ path: `${site.path}.sections`,
3346
+ surface: joinWhere(label2, site.surface),
3347
+ objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
3348
+ sections: site.view.sections
3075
3349
  });
3076
3350
  }
3077
- for (const key of ["listViews", "formViews"]) {
3078
- const container = view[key];
3079
- if (!isRec8(container)) continue;
3080
- for (const [subKey, sub] of Object.entries(container)) {
3081
- if (!isRec8(sub)) continue;
3082
- sites.push({
3083
- path: `${basePath}.${key}.${subKey}.sections`,
3084
- surface: joinWhere(label2, `${key}.${subKey}`),
3085
- objectName: bindingOf(sub) ?? listBinding,
3086
- sections: sub.sections
3087
- });
3088
- }
3089
- }
3090
3351
  }
3091
3352
  function translatedObjectNames(stack) {
3092
3353
  const out = /* @__PURE__ */ new Set();
3093
3354
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3094
3355
  for (const bundle of bundles) {
3095
- if (!isRec8(bundle)) continue;
3356
+ if (!isRec10(bundle)) continue;
3096
3357
  for (const data of Object.values(bundle)) {
3097
- if (!isRec8(data) || !isRec8(data.objects)) continue;
3358
+ if (!isRec10(data) || !isRec10(data.objects)) continue;
3098
3359
  for (const [objectName, node] of Object.entries(data.objects)) {
3099
- if (isRec8(node)) out.add(objectName);
3360
+ if (isRec10(node)) out.add(objectName);
3100
3361
  }
3101
3362
  }
3102
3363
  }
@@ -3108,22 +3369,22 @@ function suggestedName(label2) {
3108
3369
  }
3109
3370
  function validateTranslatableSections(stack) {
3110
3371
  const findings = [];
3111
- if (!isRec8(stack)) return findings;
3372
+ if (!isRec10(stack)) return findings;
3112
3373
  const translated = translatedObjectNames(stack);
3113
3374
  if (translated.size === 0) return findings;
3114
3375
  const sites = [];
3115
3376
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3116
- const objectName = strName11(obj.name);
3377
+ const objectName = strName12(obj.name);
3117
3378
  if (!objectName) continue;
3118
3379
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3119
3380
  collectViewSites(
3120
- { ...view, object: strName11(view.object) ?? objectName },
3381
+ { ...view, object: strName12(view.object) ?? objectName },
3121
3382
  path,
3122
3383
  viewLabel(view),
3123
3384
  sites
3124
3385
  );
3125
3386
  }
3126
- if (isRec8(obj.listViews)) {
3387
+ if (isRec10(obj.listViews)) {
3127
3388
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3128
3389
  }
3129
3390
  }
@@ -3131,13 +3392,13 @@ function validateTranslatableSections(stack) {
3131
3392
  collectViewSites(view, path, viewLabel(view), sites);
3132
3393
  }
3133
3394
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3134
- const pageName = strName11(page.name);
3395
+ const pageName = strName12(page.name);
3135
3396
  const pageLabel = pageName ? `page "${pageName}"` : "";
3136
3397
  for (const walked of walkPageComponents(page, pagePath)) {
3137
3398
  if (!walked.objectName) continue;
3138
- const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
3399
+ const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3139
3400
  if (!props) continue;
3140
- const type = strName11(walked.component.type) ?? "component";
3401
+ const type = strName12(walked.component.type) ?? "component";
3141
3402
  sites.push({
3142
3403
  path: `${walked.path}.properties.sections`,
3143
3404
  surface: joinWhere(pageLabel, type),
@@ -3152,9 +3413,9 @@ function validateTranslatableSections(stack) {
3152
3413
  if (!Array.isArray(site.sections)) continue;
3153
3414
  for (let i = 0; i < site.sections.length; i++) {
3154
3415
  const section = site.sections[i];
3155
- if (!isRec8(section)) continue;
3156
- if (strName11(section.name)) continue;
3157
- const heading = strName11(section.label) ?? strName11(section.title);
3416
+ if (!isRec10(section)) continue;
3417
+ if (strName12(section.name)) continue;
3418
+ const heading = strName12(section.label);
3158
3419
  if (!heading) continue;
3159
3420
  const slug = suggestedName(heading);
3160
3421
  findings.push({
@@ -3172,10 +3433,10 @@ function validateTranslatableSections(stack) {
3172
3433
 
3173
3434
  // src/flow-walk.ts
3174
3435
  import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
3175
- function isRec9(v) {
3436
+ function isRec11(v) {
3176
3437
  return !!v && typeof v === "object" && !Array.isArray(v);
3177
3438
  }
3178
- function strName12(v) {
3439
+ function strName13(v) {
3179
3440
  return typeof v === "string" && v.length > 0 ? v : void 0;
3180
3441
  }
3181
3442
  var REGION_SLOTS = new Map(
@@ -3184,10 +3445,10 @@ var REGION_SLOTS = new Map(
3184
3445
  var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
3185
3446
  var MAX_REGION_DEPTH = 16;
3186
3447
  function flowNodeLabel(node, index) {
3187
- return strName12(node.label) ?? strName12(node.id) ?? `#${index}`;
3448
+ return strName13(node.label) ?? strName13(node.id) ?? `#${index}`;
3188
3449
  }
3189
3450
  function stripRegions(config) {
3190
- if (!isRec9(config)) return void 0;
3451
+ if (!isRec11(config)) return void 0;
3191
3452
  let out;
3192
3453
  for (const key of Object.keys(config)) {
3193
3454
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3198,11 +3459,11 @@ function stripRegions(config) {
3198
3459
  }
3199
3460
  function walkFlowNodes(flow, flowPath) {
3200
3461
  const out = [];
3201
- if (!isRec9(flow)) return out;
3462
+ if (!isRec11(flow)) return out;
3202
3463
  const visitList = (nodes, basePath, trail, depth) => {
3203
3464
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3204
3465
  nodes.forEach((raw, index) => {
3205
- if (!isRec9(raw)) return;
3466
+ if (!isRec11(raw)) return;
3206
3467
  const path = `${basePath}[${index}]`;
3207
3468
  out.push({
3208
3469
  node: raw,
@@ -3211,9 +3472,9 @@ function walkFlowNodes(flow, flowPath) {
3211
3472
  regionTrail: trail,
3212
3473
  depth
3213
3474
  });
3214
- const type = strName12(raw.type);
3475
+ const type = strName13(raw.type);
3215
3476
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3216
- if (!slots || !isRec9(raw.config)) return;
3477
+ if (!slots || !isRec11(raw.config)) return;
3217
3478
  const config = raw.config;
3218
3479
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3219
3480
  for (const slot of slots) {
@@ -3221,8 +3482,8 @@ function walkFlowNodes(flow, flowPath) {
3221
3482
  if (slot === "branches") {
3222
3483
  if (!Array.isArray(value)) continue;
3223
3484
  value.forEach((branch, b) => {
3224
- if (!isRec9(branch)) return;
3225
- const branchName = strName12(branch.name) ?? `#${b}`;
3485
+ if (!isRec11(branch)) return;
3486
+ const branchName = strName13(branch.name) ?? `#${b}`;
3226
3487
  visitList(
3227
3488
  branch.nodes,
3228
3489
  `${path}.config.branches[${b}].nodes`,
@@ -3232,7 +3493,7 @@ function walkFlowNodes(flow, flowPath) {
3232
3493
  });
3233
3494
  continue;
3234
3495
  }
3235
- if (!isRec9(value)) continue;
3496
+ if (!isRec11(value)) continue;
3236
3497
  visitList(
3237
3498
  value.nodes,
3238
3499
  `${path}.config.${slot}.nodes`,
@@ -3457,7 +3718,7 @@ function asArray16(v) {
3457
3718
  }
3458
3719
  return [];
3459
3720
  }
3460
- function strName13(v) {
3721
+ function strName14(v) {
3461
3722
  return typeof v === "string" && v.length > 0 ? v : void 0;
3462
3723
  }
3463
3724
  function surfaceOf(v) {
@@ -3468,17 +3729,17 @@ function validateAiSurfaceAffinity(stack) {
3468
3729
  if (!stack || typeof stack !== "object") return findings;
3469
3730
  const skillsByName = /* @__PURE__ */ new Map();
3470
3731
  for (const skill of asArray16(stack.skills)) {
3471
- const n = strName13(skill.name);
3732
+ const n = strName14(skill.name);
3472
3733
  if (n) skillsByName.set(n, skill);
3473
3734
  }
3474
3735
  const agents = asArray16(stack.agents);
3475
3736
  for (let ai = 0; ai < agents.length; ai++) {
3476
3737
  const agent = agents[ai];
3477
- const agentName = strName13(agent.name) ?? `#${ai}`;
3738
+ const agentName = strName14(agent.name) ?? `#${ai}`;
3478
3739
  const agentSurface = surfaceOf(agent.surface);
3479
3740
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
3480
3741
  for (let si = 0; si < skillRefs.length; si++) {
3481
- const ref = strName13(skillRefs[si]);
3742
+ const ref = strName14(skillRefs[si]);
3482
3743
  if (!ref) continue;
3483
3744
  const skill = skillsByName.get(ref);
3484
3745
  if (!skill) continue;
@@ -3507,7 +3768,7 @@ function asArray17(v) {
3507
3768
  }
3508
3769
  return [];
3509
3770
  }
3510
- function strName14(v) {
3771
+ function strName15(v) {
3511
3772
  return typeof v === "string" && v.length > 0 ? v : void 0;
3512
3773
  }
3513
3774
  function distance6(a, b) {
@@ -3548,8 +3809,8 @@ function materialisesAsTool(action) {
3548
3809
  if (!ai || typeof ai !== "object") return false;
3549
3810
  const aiRec = ai;
3550
3811
  if (aiRec.exposed !== true) return false;
3551
- if (!strName14(aiRec.description)) return false;
3552
- const type = strName14(action.type);
3812
+ if (!strName15(aiRec.description)) return false;
3813
+ const type = strName15(action.type);
3553
3814
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
3554
3815
  if (type === "script") return Boolean(action.target || action.body);
3555
3816
  return Boolean(action.target);
@@ -3557,12 +3818,12 @@ function materialisesAsTool(action) {
3557
3818
  function collectToolUniverse(stack) {
3558
3819
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
3559
3820
  for (const tool of asArray17(stack.tools)) {
3560
- const n = strName14(tool.name);
3821
+ const n = strName15(tool.name);
3561
3822
  if (n) universe.add(n);
3562
3823
  }
3563
3824
  const addActionFamily = (actions) => {
3564
3825
  for (const action of asArray17(actions)) {
3565
- const n = strName14(action.name);
3826
+ const n = strName15(action.name);
3566
3827
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
3567
3828
  }
3568
3829
  };
@@ -3576,7 +3837,7 @@ function collectUnexposedActionNames(stack) {
3576
3837
  const names = /* @__PURE__ */ new Set();
3577
3838
  const scan = (actions) => {
3578
3839
  for (const action of asArray17(actions)) {
3579
- const n = strName14(action.name);
3840
+ const n = strName15(action.name);
3580
3841
  if (n && !materialisesAsTool(action)) names.add(n);
3581
3842
  }
3582
3843
  };
@@ -3602,10 +3863,10 @@ function validateAiToolReferences(stack) {
3602
3863
  const skills = asArray17(stack.skills);
3603
3864
  for (let si = 0; si < skills.length; si++) {
3604
3865
  const skill = skills[si];
3605
- const skillName = strName14(skill.name) ?? `#${si}`;
3866
+ const skillName = strName15(skill.name) ?? `#${si}`;
3606
3867
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
3607
3868
  for (let ti = 0; ti < refs.length; ti++) {
3608
- const ref = strName14(refs[ti]);
3869
+ const ref = strName15(refs[ti]);
3609
3870
  if (!ref || resolves(ref)) continue;
3610
3871
  const isPattern = ref.endsWith("*");
3611
3872
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -3624,6 +3885,7 @@ function validateAiToolReferences(stack) {
3624
3885
 
3625
3886
  // src/validate-ai-agent-authoring.ts
3626
3887
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
3888
+ var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
3627
3889
  function asArray18(v) {
3628
3890
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3629
3891
  if (v && typeof v === "object") {
@@ -3631,7 +3893,7 @@ function asArray18(v) {
3631
3893
  }
3632
3894
  return [];
3633
3895
  }
3634
- function strName15(v) {
3896
+ function strName16(v) {
3635
3897
  return typeof v === "string" && v.length > 0 ? v : void 0;
3636
3898
  }
3637
3899
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -3641,7 +3903,7 @@ function validateAiAgentAuthoring(stack) {
3641
3903
  const agents = asArray18(stack.agents);
3642
3904
  for (let ai = 0; ai < agents.length; ai++) {
3643
3905
  const agent = agents[ai];
3644
- const name = strName15(agent.name) ?? `#${ai}`;
3906
+ const name = strName16(agent.name) ?? `#${ai}`;
3645
3907
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
3646
3908
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
3647
3909
  findings.push({
@@ -3653,6 +3915,22 @@ function validateAiAgentAuthoring(stack) {
3653
3915
  hint: isPlatformName ? `Remove the declaration; the platform owns "${name}". Extend it with skills instead.` : `Delete the agent and express its capability as skills. Everything an agent carried that a skill does not is persona text: move the useful parts of \`instructions\` into the skills' own instructions.` + (skillCount > 0 ? ` The ${skillCount} skill${skillCount === 1 ? "" : "s"} this agent references already carry the capability \u2014 they attach to the platform agent by \`surface\` affinity, so nothing is lost by dropping the persona.` : ``)
3654
3916
  });
3655
3917
  }
3918
+ const roster = [...PLATFORM_AGENT_NAMES].join(", ");
3919
+ const apps = asArray18(stack.apps);
3920
+ for (let appIdx = 0; appIdx < apps.length; appIdx++) {
3921
+ const app = apps[appIdx];
3922
+ const defaultAgent = strName16(app.defaultAgent);
3923
+ if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
3924
+ const appName = strName16(app.name) ?? `#${appIdx}`;
3925
+ findings.push({
3926
+ severity: "warning",
3927
+ rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
3928
+ where: `app "${appName}".defaultAgent`,
3929
+ path: `apps[${appIdx}].defaultAgent`,
3930
+ 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.`,
3931
+ 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.`
3932
+ });
3933
+ }
3656
3934
  return findings;
3657
3935
  }
3658
3936
 
@@ -3739,13 +4017,13 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
3739
4017
  "owner",
3740
4018
  "record_type"
3741
4019
  ]);
3742
- var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4020
+ var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3743
4021
  function asArray19(v) {
3744
- if (Array.isArray(v)) return v.filter((x) => isRec10(x));
3745
- if (isRec10(v)) {
4022
+ if (Array.isArray(v)) return v.filter((x) => isRec12(x));
4023
+ if (isRec12(v)) {
3746
4024
  return Object.entries(v).map(([name, def]) => ({
3747
4025
  name,
3748
- ...isRec10(def) ? def : {}
4026
+ ...isRec12(def) ? def : {}
3749
4027
  }));
3750
4028
  }
3751
4029
  return [];
@@ -3891,7 +4169,7 @@ function validateHookBodyWrites(stack) {
3891
4169
  let objectFields = null;
3892
4170
  hooks.forEach((hook, hookIndex) => {
3893
4171
  const body = hook.body;
3894
- if (!isRec10(body) || body.language !== "js") return;
4172
+ if (!isRec12(body) || body.language !== "js") return;
3895
4173
  const source = body.source;
3896
4174
  if (typeof source !== "string" || source.trim() === "") return;
3897
4175
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -3962,13 +4240,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
3962
4240
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
3963
4241
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
3964
4242
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
3965
- var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4243
+ var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3966
4244
  function asArray20(v) {
3967
- if (Array.isArray(v)) return v.filter((x) => isRec11(x));
3968
- if (isRec11(v)) {
4245
+ if (Array.isArray(v)) return v.filter((x) => isRec13(x));
4246
+ if (isRec13(v)) {
3969
4247
  return Object.entries(v).map(([name, def]) => ({
3970
4248
  name,
3971
- ...isRec11(def) ? def : {}
4249
+ ...isRec13(def) ? def : {}
3972
4250
  }));
3973
4251
  }
3974
4252
  return [];
@@ -3986,7 +4264,7 @@ function collectActionBodies(stack) {
3986
4264
  const type = typeof action.type === "string" ? action.type : "script";
3987
4265
  if (type !== "script") return;
3988
4266
  const body = action.body;
3989
- if (!isRec11(body) || body.language !== "js") return;
4267
+ if (!isRec13(body) || body.language !== "js") return;
3990
4268
  const source = body.source;
3991
4269
  if (typeof source !== "string" || source.trim() === "") return;
3992
4270
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4005,7 +4283,7 @@ function collectActionBodies(stack) {
4005
4283
  }
4006
4284
  function validateActionBodyWrites(stack) {
4007
4285
  const findings = [];
4008
- if (!isRec11(stack)) return findings;
4286
+ if (!isRec13(stack)) return findings;
4009
4287
  const sites = collectActionBodies(stack);
4010
4288
  if (sites.length === 0) return findings;
4011
4289
  let objectFields = null;
@@ -4063,13 +4341,13 @@ function fixHint2(field, declared) {
4063
4341
  import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
4064
4342
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
4065
4343
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4066
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4344
+ var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4067
4345
  function asArray21(v) {
4068
- if (Array.isArray(v)) return v.filter((x) => isRec12(x));
4069
- if (isRec12(v)) {
4346
+ if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4347
+ if (isRec14(v)) {
4070
4348
  return Object.entries(v).map(([name, def]) => ({
4071
4349
  name,
4072
- ...isRec12(def) ? def : {}
4350
+ ...isRec14(def) ? def : {}
4073
4351
  }));
4074
4352
  }
4075
4353
  return [];
@@ -4082,7 +4360,7 @@ function readLiteralObjectName(config) {
4082
4360
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4083
4361
  function validateFlowNodeWrites(stack) {
4084
4362
  const findings = [];
4085
- if (!isRec12(stack)) return findings;
4363
+ if (!isRec14(stack)) return findings;
4086
4364
  const flows = asArray21(stack.flows);
4087
4365
  if (flows.length === 0) return findings;
4088
4366
  let objectFields = null;
@@ -4091,10 +4369,10 @@ function validateFlowNodeWrites(stack) {
4091
4369
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4092
4370
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4093
4371
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4094
- const config = isRec12(node.config) ? node.config : void 0;
4372
+ const config = isRec14(node.config) ? node.config : void 0;
4095
4373
  if (!config) return;
4096
4374
  const fields = config.fields;
4097
- if (!isRec12(fields)) return;
4375
+ if (!isRec14(fields)) return;
4098
4376
  const written = Object.keys(fields);
4099
4377
  if (written.length === 0) return;
4100
4378
  const objectName = readLiteralObjectName(config);
@@ -4230,11 +4508,11 @@ import {
4230
4508
  import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
4231
4509
 
4232
4510
  // src/zod-issue-format.ts
4233
- var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4511
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4234
4512
  var valueAtPath = (root, path) => {
4235
4513
  let cur = root;
4236
4514
  for (const key of path) {
4237
- if (!isRec13(cur) && !Array.isArray(cur)) return void 0;
4515
+ if (!isRec15(cur) && !Array.isArray(cur)) return void 0;
4238
4516
  cur = cur[key];
4239
4517
  }
4240
4518
  return cur;
@@ -4372,7 +4650,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4372
4650
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4373
4651
  function checkChartDrillDown(raw, push2) {
4374
4652
  if (raw === void 0 || raw === NOT_STATIC) return;
4375
- if (!isRec14(raw)) {
4653
+ if (!isRec16(raw)) {
4376
4654
  push2(
4377
4655
  "error",
4378
4656
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4395,7 +4673,7 @@ function checkChartDrillDown(raw, push2) {
4395
4673
  }
4396
4674
  function checkChartAggregate(raw, push2) {
4397
4675
  if (raw === void 0 || raw === NOT_STATIC) return;
4398
- if (!isRec14(raw)) {
4676
+ if (!isRec16(raw)) {
4399
4677
  push2(
4400
4678
  "error",
4401
4679
  REACT_CHART_AGGREGATE_INVALID,
@@ -4410,7 +4688,7 @@ function checkChartAggregate(raw, push2) {
4410
4688
  "warning",
4411
4689
  REACT_CHART_AGGREGATE_INVALID,
4412
4690
  "aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
4413
- "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. Deliberate single-value charts are tolerated at warning level for now: ChartAggregateSchema declares groupBy required while ObjectChart honours its absence by falling back to xAxisKey \u2014 objectstack#5583 decides which of the two moves."
4691
+ "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
4414
4692
  );
4415
4693
  }
4416
4694
  const parsed = ChartAggregateSchema.safeParse(raw);
@@ -4426,7 +4704,7 @@ function checkChartAggregate(raw, push2) {
4426
4704
  );
4427
4705
  }
4428
4706
  }
4429
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4707
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4430
4708
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4431
4709
  function checkObjectChart(attrs, objectFields, findings) {
4432
4710
  const { values, where, path } = attrs;
@@ -4436,11 +4714,11 @@ function checkObjectChart(attrs, objectFields, findings) {
4436
4714
  const aggregate = values.get("aggregate");
4437
4715
  checkChartAggregate(aggregate, push2);
4438
4716
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4439
- if (!isRec14(aggregate)) return;
4717
+ if (!isRec16(aggregate)) return;
4440
4718
  const fn = strOf(aggregate.function);
4441
4719
  const field = strOf(aggregate.field);
4442
4720
  const groupBy = aggregate.groupBy;
4443
- const groupByField = strOf(groupBy) ?? (isRec14(groupBy) ? strOf(groupBy.field) : void 0);
4721
+ const groupByField = strOf(groupBy) ?? (isRec16(groupBy) ? strOf(groupBy.field) : void 0);
4444
4722
  const objectName = strOf(values.get("objectName"));
4445
4723
  const known = objectName ? objectFields.get(objectName) : void 0;
4446
4724
  if (objectName && known) {
@@ -4473,18 +4751,18 @@ function checkObjectChart(attrs, objectFields, findings) {
4473
4751
  );
4474
4752
  };
4475
4753
  const xAxisRaw = values.get("xAxis");
4476
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec14(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
4754
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec16(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
4477
4755
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
4478
4756
  axisRef(categoryAxis, categoryProp);
4479
4757
  const yAxisRaw = values.get("yAxis");
4480
4758
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
4481
4759
  for (const a of yAxisList) {
4482
- axisRef(strOf(a) ?? (isRec14(a) ? strOf(a.field) : void 0), "yAxis[].field");
4760
+ axisRef(strOf(a) ?? (isRec16(a) ? strOf(a.field) : void 0), "yAxis[].field");
4483
4761
  }
4484
4762
  const series = values.get("series");
4485
4763
  if (Array.isArray(series)) {
4486
4764
  for (const s of series) {
4487
- if (!isRec14(s)) continue;
4765
+ if (!isRec16(s)) continue;
4488
4766
  const dataKey = strOf(s.dataKey);
4489
4767
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
4490
4768
  }
@@ -4540,7 +4818,7 @@ function subformFieldRefs(value, basePath) {
4540
4818
  if (!Array.isArray(value)) return { child, parent };
4541
4819
  for (let i = 0; i < value.length; i++) {
4542
4820
  const sub = value[i];
4543
- if (!isRec14(sub)) continue;
4821
+ if (!isRec16(sub)) continue;
4544
4822
  const at = (key) => `${basePath}[${i}].${key}`;
4545
4823
  child.push({
4546
4824
  objectName: strOf(sub.childObject),
@@ -4585,20 +4863,20 @@ function reactFieldRefs(spec, values, basePath) {
4585
4863
  }
4586
4864
  for (const key of spec.nestedFields ?? []) {
4587
4865
  const v = readable(key);
4588
- if (isRec14(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
4866
+ if (isRec16(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
4589
4867
  }
4590
4868
  for (const key of spec.sections ?? []) {
4591
4869
  const v = readable(key);
4592
4870
  if (!Array.isArray(v)) continue;
4593
4871
  for (let i = 0; i < v.length; i++) {
4594
4872
  const section = v[i];
4595
- if (!isRec14(section)) continue;
4873
+ if (!isRec16(section)) continue;
4596
4874
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
4597
4875
  }
4598
4876
  }
4599
4877
  for (const key of spec.keyedByField ?? []) {
4600
4878
  const v = readable(key);
4601
- if (!isRec14(v)) continue;
4879
+ if (!isRec16(v)) continue;
4602
4880
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
4603
4881
  }
4604
4882
  for (const key of spec.filterArrays ?? []) {
@@ -4885,10 +5163,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
4885
5163
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
4886
5164
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
4887
5165
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
4888
- function isRec15(v) {
5166
+ function isRec17(v) {
4889
5167
  return !!v && typeof v === "object" && !Array.isArray(v);
4890
5168
  }
4891
- function strName16(v) {
5169
+ function strName17(v) {
4892
5170
  return typeof v === "string" && v.length > 0 ? v : void 0;
4893
5171
  }
4894
5172
  function asArray24(v) {
@@ -4902,23 +5180,23 @@ var PROPS_SCHEMAS = ComponentPropsMap;
4902
5180
  var DATASOURCE_SUPPLIED_PROP = "object";
4903
5181
  function suppliedByDataSource(issue, component) {
4904
5182
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
4905
- const dataSource = isRec15(component.dataSource) ? component.dataSource : void 0;
4906
- return strName16(dataSource?.object) !== void 0;
5183
+ const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
5184
+ return strName17(dataSource?.object) !== void 0;
4907
5185
  }
4908
5186
  function validateComponentProps(stack) {
4909
5187
  const findings = [];
4910
- if (!isRec15(stack)) return findings;
5188
+ if (!isRec17(stack)) return findings;
4911
5189
  const pages = asArray24(stack.pages);
4912
5190
  for (let pi = 0; pi < pages.length; pi++) {
4913
5191
  const page = pages[pi];
4914
- if (!isRec15(page)) continue;
4915
- const pageName = strName16(page.name) ?? `#${pi}`;
5192
+ if (!isRec17(page)) continue;
5193
+ const pageName = strName17(page.name) ?? `#${pi}`;
4916
5194
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4917
- const type = strName16(component.type);
5195
+ const type = strName17(component.type);
4918
5196
  if (!type) continue;
4919
5197
  const schema = PROPS_SCHEMAS[type];
4920
5198
  if (!schema) continue;
4921
- const props = isRec15(component.properties) ? component.properties : void 0;
5199
+ const props = isRec17(component.properties) ? component.properties : void 0;
4922
5200
  if (!props) continue;
4923
5201
  const where = `page "${pageName}" \xB7 ${type}`;
4924
5202
  const base = `${path}.properties`;
@@ -5511,6 +5789,7 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
5511
5789
  var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
5512
5790
  var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
5513
5791
  var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
5792
+ var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
5514
5793
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
5515
5794
  function asArray30(v) {
5516
5795
  if (Array.isArray(v)) return v;
@@ -5528,6 +5807,11 @@ function renderNonObject(v) {
5528
5807
  if (t === "bigint") return `${String(v)}n (a bigint)`;
5529
5808
  return `a ${t}`;
5530
5809
  }
5810
+ function renderTriggerToken(v) {
5811
+ if (typeof v === "string") return `'${v}'`;
5812
+ const json = JSON.stringify(v);
5813
+ return json === void 0 ? `a ${typeof v}` : json;
5814
+ }
5531
5815
  function startNodeOf(flow) {
5532
5816
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
5533
5817
  const index = nodes.findIndex((n) => n?.type === "start");
@@ -5647,6 +5931,25 @@ function validateFlowTriggerReadiness(stack) {
5647
5931
  hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
5648
5932
  });
5649
5933
  }
5934
+ const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
5935
+ if (start && flow.type === "record_change" && !routesToSomeTrigger) {
5936
+ const hasTriggerType = config.triggerType != null;
5937
+ findings.push({
5938
+ // `error` (#5762's criterion, applied to a fourth id). The verdict is
5939
+ // the engine's own routing chain — literal `startsWith`/`typeof` tests
5940
+ // with no registry lookup in them — so no installed package can make
5941
+ // this token resolve. `registerTrigger` is keyed by the RESOLVED type,
5942
+ // which is the near-miss worth stating: a plugin can supply the
5943
+ // record-change trigger itself, and it still would not help, because
5944
+ // the flow never reaches the point of asking for one.
5945
+ severity: "error",
5946
+ rule: FLOW_TRIGGER_UNROUTABLE,
5947
+ where: `flow "${flowName}" \u203A start node`,
5948
+ path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
5949
+ 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.`,
5950
+ 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.`
5951
+ });
5952
+ }
5650
5953
  if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
5651
5954
  findings.push({
5652
5955
  severity: "warning",
@@ -6006,7 +6309,7 @@ function validateSemanticRoles(stack) {
6006
6309
  (h) => typeof h === "string" && h.length > 0
6007
6310
  );
6008
6311
  if (declaredStrings.length > 0 && declaredGroups.size > 0) {
6009
- const declaredTitle = [obj.nameField, obj.primaryField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6312
+ const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6010
6313
  const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
6011
6314
  const stripSet = new Set(
6012
6315
  declaredStrings.filter((h) => h !== titleField).slice(0, 4)
@@ -6041,6 +6344,12 @@ function asArray34(v) {
6041
6344
  }
6042
6345
  return [];
6043
6346
  }
6347
+ function isRec18(v) {
6348
+ return !!v && typeof v === "object" && !Array.isArray(v);
6349
+ }
6350
+ function strName18(v) {
6351
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6352
+ }
6044
6353
  function fieldNameOf(entry) {
6045
6354
  if (typeof entry === "string") return entry.length > 0 ? entry : null;
6046
6355
  if (entry && typeof entry === "object" && !Array.isArray(entry)) {
@@ -6049,13 +6358,6 @@ function fieldNameOf(entry) {
6049
6358
  }
6050
6359
  return null;
6051
6360
  }
6052
- function boundObject(view) {
6053
- const data = view.data;
6054
- if (data && typeof data === "object" && typeof data.object === "string") {
6055
- return data.object;
6056
- }
6057
- return typeof view.objectName === "string" ? view.objectName : void 0;
6058
- }
6059
6361
  function validateFormLayout(stack) {
6060
6362
  const findings = [];
6061
6363
  const objectFields = /* @__PURE__ */ new Map();
@@ -6065,44 +6367,44 @@ function validateFormLayout(stack) {
6065
6367
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
6066
6368
  objectFields.set(name, new Set(fields));
6067
6369
  }
6068
- const views = asArray34(stack.views);
6069
- for (let i = 0; i < views.length; i++) {
6070
- const view = views[i];
6071
- if (!view || typeof view !== "object") continue;
6072
- const sections = Array.isArray(view.sections) ? view.sections : null;
6073
- if (!sections) continue;
6074
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6075
- const objName = boundObject(view);
6076
- const known = objName ? objectFields.get(objName) : void 0;
6077
- const where = `view "${viewName}"`;
6078
- const base = `views[${i}]`;
6079
- for (let s = 0; s < sections.length; s++) {
6080
- const sec = sections[s];
6081
- const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
6082
- for (let f = 0; f < secFields.length; f++) {
6083
- const entry = secFields[f];
6084
- const fname = fieldNameOf(entry);
6085
- const fpath = `${base}.sections[${s}].fields[${f}]`;
6086
- if (fname && known && !known.has(fname)) {
6087
- findings.push({
6088
- severity: "warning",
6089
- rule: FORM_FIELD_UNKNOWN,
6090
- where,
6091
- path: fpath,
6092
- message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6093
- hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6094
- });
6095
- }
6096
- const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
6097
- if (colSpan != null) {
6098
- findings.push({
6099
- severity: "warning",
6100
- rule: FORM_COLSPAN_ABSOLUTE,
6101
- where,
6102
- path: `${fpath}.colSpan`,
6103
- message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
6104
- hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6105
- });
6370
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6371
+ const viewName = strName18(view.name) ?? strName18(view.object) ?? viewPath;
6372
+ const containerObject = viewObjectName(view);
6373
+ for (const site of formViewSites(view, viewPath)) {
6374
+ const objName = viewObjectName(site.view) ?? containerObject;
6375
+ const known = objName ? objectFields.get(objName) : void 0;
6376
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
6377
+ for (const bucket of ["sections", "groups"]) {
6378
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6379
+ for (let s = 0; s < sections.length; s++) {
6380
+ const sec = sections[s];
6381
+ const secFields = isRec18(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6382
+ for (let f = 0; f < secFields.length; f++) {
6383
+ const entry = secFields[f];
6384
+ const fname = fieldNameOf(entry);
6385
+ const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
6386
+ if (fname && known && !known.has(fname)) {
6387
+ findings.push({
6388
+ severity: "warning",
6389
+ rule: FORM_FIELD_UNKNOWN,
6390
+ where,
6391
+ path: fpath,
6392
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6393
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6394
+ });
6395
+ }
6396
+ const colSpan = isRec18(entry) ? entry.colSpan : void 0;
6397
+ if (colSpan != null) {
6398
+ findings.push({
6399
+ severity: "warning",
6400
+ rule: FORM_COLSPAN_ABSOLUTE,
6401
+ where,
6402
+ path: `${fpath}.colSpan`,
6403
+ 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`,
6404
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6405
+ });
6406
+ }
6407
+ }
6106
6408
  }
6107
6409
  }
6108
6410
  }
@@ -6201,17 +6503,17 @@ function validateSeedStateMachine(stack) {
6201
6503
  }
6202
6504
 
6203
6505
  // src/validate-visibility-predicates.ts
6204
- var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
6506
+ import {
6507
+ collectCelRootIdentifiers as collectCelRootIdentifiers3,
6508
+ firstUndeclaredReference,
6509
+ parseCelToAst as parseCelToAst2,
6510
+ parseCelToAstWithReason
6511
+ } from "@objectstack/formula";
6205
6512
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
6513
+ var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
6514
+ var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
6515
+ var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
6206
6516
  var CANONICAL = "visibleWhen";
6207
- var ALIASES = ["visibleOn", "visibility"];
6208
- function asArray35(v) {
6209
- if (Array.isArray(v)) return v;
6210
- if (v && typeof v === "object") {
6211
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6212
- }
6213
- return [];
6214
- }
6215
6517
  function predicateSource(v) {
6216
6518
  if (typeof v === "string") return v;
6217
6519
  if (v && typeof v === "object" && typeof v.source === "string") {
@@ -6222,6 +6524,67 @@ function predicateSource(v) {
6222
6524
  function usesRoot(source, root) {
6223
6525
  return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
6224
6526
  }
6527
+ function withoutStringLiterals(source) {
6528
+ return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
6529
+ }
6530
+ var NON_CEL_SPELLINGS = [
6531
+ { wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
6532
+ { wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
6533
+ { wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
6534
+ { wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
6535
+ { wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
6536
+ { wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
6537
+ // Assignment where a comparison was meant. Last, and fenced off from every
6538
+ // operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
6539
+ { wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
6540
+ ];
6541
+ function quoteSource(source) {
6542
+ const flat = source.replace(/\s+/g, " ").trim();
6543
+ return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
6544
+ }
6545
+ function celRefusal(source) {
6546
+ if (!source.trim()) return null;
6547
+ const parsed = parseCelToAstWithReason(source);
6548
+ if (parsed.ok || parsed.kind === "empty") return null;
6549
+ if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
6550
+ const identifiers = collectCelRootIdentifiers3(source);
6551
+ const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
6552
+ const scannable = withoutStringLiterals(source);
6553
+ return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
6554
+ }
6555
+ function boundName(overrun) {
6556
+ return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
6557
+ }
6558
+ var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
6559
+ function isNode2(v) {
6560
+ return !!v && typeof v === "object" && typeof v.op === "string";
6561
+ }
6562
+ function namespaceRoots(node, out) {
6563
+ if (Array.isArray(node)) {
6564
+ for (const child of node) namespaceRoots(child, out);
6565
+ return;
6566
+ }
6567
+ if (!isNode2(node)) return;
6568
+ const args = node.args;
6569
+ if (Array.isArray(args)) {
6570
+ const receiver = node.op === "rcall" ? args[1] : args[0];
6571
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6572
+ out.add(receiver.args);
6573
+ }
6574
+ }
6575
+ namespaceRoots(args, out);
6576
+ }
6577
+ function firstBareIdentifier(source) {
6578
+ const ast = parseCelToAst2(source);
6579
+ if (!ast) return null;
6580
+ const rooted = /* @__PURE__ */ new Set();
6581
+ namespaceRoots(ast, rooted);
6582
+ return firstUndeclaredReference(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
6583
+ }
6584
+ var CANONICAL_ROOT_BY_LAYER = {
6585
+ runtime: "record",
6586
+ metadata: "data"
6587
+ };
6225
6588
  var MISLAYER_BY_LAYER = {
6226
6589
  runtime: {
6227
6590
  forbiddenRoot: "data",
@@ -6235,18 +6598,6 @@ var MISLAYER_BY_LAYER = {
6235
6598
  }
6236
6599
  };
6237
6600
  function checkElement(el, where, path, layer, findings) {
6238
- for (const alias of ALIASES) {
6239
- if (el[alias] !== void 0) {
6240
- findings.push({
6241
- severity: "warning",
6242
- rule: VISIBILITY_ALIAS_DEPRECATED,
6243
- where,
6244
- path: `${path}.${alias}`,
6245
- message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
6246
- hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
6247
- });
6248
- }
6249
- }
6250
6601
  const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
6251
6602
  const source = predicateSource(raw);
6252
6603
  const rule = MISLAYER_BY_LAYER[layer];
@@ -6260,6 +6611,43 @@ function checkElement(el, where, path, layer, findings) {
6260
6611
  hint: rule.hint
6261
6612
  });
6262
6613
  }
6614
+ const refusal = source ? celRefusal(source) : null;
6615
+ if (source && refusal?.kind === "bounds") {
6616
+ const bound = boundName(refusal.overrun);
6617
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
6618
+ findings.push({
6619
+ severity: "error",
6620
+ rule: VISIBILITY_PREDICATE_OVER_BUDGET,
6621
+ where,
6622
+ path,
6623
+ 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).`,
6624
+ 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.`
6625
+ });
6626
+ }
6627
+ if (source && refusal?.kind === "syntax") {
6628
+ findings.push({
6629
+ severity: "error",
6630
+ rule: VISIBILITY_PREDICATE_SYNTAX,
6631
+ where,
6632
+ path,
6633
+ 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).`,
6634
+ 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\`).`
6635
+ });
6636
+ }
6637
+ if (source && !refusal) {
6638
+ const bare = firstBareIdentifier(source);
6639
+ if (bare) {
6640
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
6641
+ findings.push({
6642
+ severity: "error",
6643
+ rule: VISIBILITY_BARE_IDENTIFIER,
6644
+ where,
6645
+ path,
6646
+ 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).`,
6647
+ hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a `*.form.ts` metadata-editing form binds the row under edit as `data`).")
6648
+ });
6649
+ }
6650
+ }
6263
6651
  }
6264
6652
  function isFieldObject(entry) {
6265
6653
  return !!entry && typeof entry === "object" && !Array.isArray(entry);
@@ -6267,43 +6655,304 @@ function isFieldObject(entry) {
6267
6655
  function validateVisibilityPredicates(stack, opts = {}) {
6268
6656
  const layer = opts.layer ?? "runtime";
6269
6657
  const findings = [];
6270
- const views = asArray35(stack.views);
6271
- for (let i = 0; i < views.length; i++) {
6272
- const view = views[i];
6273
- if (!view || typeof view !== "object") continue;
6274
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6275
- const where = `view "${viewName}"`;
6276
- for (const bucket of ["sections", "groups"]) {
6277
- const sections = Array.isArray(view[bucket]) ? view[bucket] : [];
6278
- for (let s = 0; s < sections.length; s++) {
6279
- const sec = sections[s];
6280
- if (!sec || typeof sec !== "object") continue;
6281
- const secPath = `views[${i}].${bucket}[${s}]`;
6282
- checkElement(sec, where, secPath, layer, findings);
6283
- const secFields = Array.isArray(sec.fields) ? sec.fields : [];
6284
- for (let f = 0; f < secFields.length; f++) {
6285
- const entry = secFields[f];
6286
- if (isFieldObject(entry)) {
6287
- checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
6658
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6659
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
6660
+ for (const site of formViewSites(view, viewPath)) {
6661
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
6662
+ for (const bucket of ["sections", "groups"]) {
6663
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6664
+ for (let s = 0; s < sections.length; s++) {
6665
+ const sec = sections[s];
6666
+ if (!sec || typeof sec !== "object") continue;
6667
+ const secPath = `${site.path}.${bucket}[${s}]`;
6668
+ checkElement(sec, where, secPath, layer, findings);
6669
+ const secFields = Array.isArray(sec.fields) ? sec.fields : [];
6670
+ for (let f = 0; f < secFields.length; f++) {
6671
+ const entry = secFields[f];
6672
+ if (isFieldObject(entry)) {
6673
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
6674
+ }
6288
6675
  }
6289
6676
  }
6290
6677
  }
6291
6678
  }
6292
6679
  }
6293
- const pages = asArray35(stack.pages);
6294
- for (let i = 0; i < pages.length; i++) {
6295
- const page = pages[i];
6296
- if (!page || typeof page !== "object") continue;
6297
- const pageName = typeof page.name === "string" ? page.name : `(page ${i})`;
6298
- const where = `page "${pageName}"`;
6299
- const regions = Array.isArray(page.regions) ? page.regions : [];
6300
- for (let r = 0; r < regions.length; r++) {
6301
- const region = regions[r];
6302
- const components = region && typeof region === "object" && Array.isArray(region.components) ? region.components : [];
6303
- for (let c = 0; c < components.length; c++) {
6304
- const comp = components[c];
6305
- if (comp && typeof comp === "object") {
6306
- checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
6680
+ for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
6681
+ const pageName = typeof page.name === "string" ? page.name : void 0;
6682
+ const where = `page "${pageName ?? pagePath}"`;
6683
+ for (const walked of walkPageComponents(page, pagePath)) {
6684
+ checkElement(walked.component, where, walked.path, layer, findings);
6685
+ }
6686
+ }
6687
+ return findings;
6688
+ }
6689
+
6690
+ // src/validate-predicate-path-refs.ts
6691
+ import { parseCelToAst as parseCelToAst3 } from "@objectstack/formula";
6692
+ import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
6693
+ import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec";
6694
+ var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
6695
+ var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
6696
+ var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
6697
+ var ROOT = "data";
6698
+ var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
6699
+ function defOf(schema) {
6700
+ if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
6701
+ const s = schema;
6702
+ return s.def ?? s._def;
6703
+ }
6704
+ function peel(schema, depth = 0) {
6705
+ if (!schema || depth > 25) return schema;
6706
+ const d = defOf(schema);
6707
+ if (!d) return schema;
6708
+ switch (d.type) {
6709
+ case "optional":
6710
+ case "nullable":
6711
+ case "default":
6712
+ case "prefault":
6713
+ case "readonly":
6714
+ case "catch":
6715
+ case "nonoptional":
6716
+ return peel(d.innerType, depth + 1);
6717
+ case "lazy":
6718
+ return peel(d.getter(), depth + 1);
6719
+ case "pipe": {
6720
+ const inner = peel(d.in, depth + 1);
6721
+ return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
6722
+ }
6723
+ default:
6724
+ return schema;
6725
+ }
6726
+ }
6727
+ function optionsOf(d) {
6728
+ return Array.isArray(d?.options) ? d.options : [];
6729
+ }
6730
+ function keysOf(schema, depth = 0) {
6731
+ if (depth > 25) return null;
6732
+ const u = peel(schema);
6733
+ const d = defOf(u);
6734
+ if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
6735
+ if (d?.type === "union" || d?.type === "discriminated_union") {
6736
+ const all = /* @__PURE__ */ new Set();
6737
+ let keyBearing = false;
6738
+ for (const option of optionsOf(d)) {
6739
+ const k = keysOf(option, depth + 1);
6740
+ if (!k) continue;
6741
+ keyBearing = true;
6742
+ for (const key of k) all.add(key);
6743
+ }
6744
+ return keyBearing ? [...all] : null;
6745
+ }
6746
+ if (d?.type === "intersection") {
6747
+ const left = keysOf(d.left, depth + 1);
6748
+ const right = keysOf(d.right, depth + 1);
6749
+ if (!left && !right) return null;
6750
+ return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
6751
+ }
6752
+ return null;
6753
+ }
6754
+ function propertyOf(schema, key, depth = 0) {
6755
+ if (depth > 25) return void 0;
6756
+ const u = peel(schema);
6757
+ const d = defOf(u);
6758
+ if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
6759
+ if (d?.type === "union" || d?.type === "discriminated_union") {
6760
+ for (const option of optionsOf(d)) {
6761
+ const found = propertyOf(option, key, depth + 1);
6762
+ if (found !== void 0) return found;
6763
+ }
6764
+ }
6765
+ if (d?.type === "intersection") {
6766
+ return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
6767
+ }
6768
+ return void 0;
6769
+ }
6770
+ function rowScopeOf(scope, key) {
6771
+ const prop = propertyOf(scope, key);
6772
+ if (prop === void 0) return void 0;
6773
+ let node = peel(prop);
6774
+ for (let i = 0; i < 25; i++) {
6775
+ const d = defOf(node);
6776
+ if (d?.type === "array") node = peel(d.element);
6777
+ else if (d?.type === "record") node = peel(d.valueType);
6778
+ else return node;
6779
+ }
6780
+ return node;
6781
+ }
6782
+ function stepInto(scope, segment) {
6783
+ const u = peel(scope);
6784
+ const d = defOf(u);
6785
+ if (d?.type === "record") return { kind: "declared", next: d.valueType };
6786
+ const declared = keysOf(u);
6787
+ if (declared === null) return { kind: "opaque" };
6788
+ if (!declared.includes(segment)) return { kind: "undeclared", declared };
6789
+ return { kind: "declared", next: propertyOf(u, segment) };
6790
+ }
6791
+ function isNode3(v) {
6792
+ return !!v && typeof v === "object" && typeof v.op === "string";
6793
+ }
6794
+ function memberChain(node) {
6795
+ if (!isNode3(node)) return null;
6796
+ if (node.op === "id" && typeof node.args === "string") return [node.args];
6797
+ if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
6798
+ const head = memberChain(node.args[0]);
6799
+ return head ? [...head, node.args[1]] : null;
6800
+ }
6801
+ return null;
6802
+ }
6803
+ function rootedPaths(node, out) {
6804
+ if (Array.isArray(node)) {
6805
+ for (const child of node) rootedPaths(child, out);
6806
+ return;
6807
+ }
6808
+ if (!isNode3(node)) return;
6809
+ if (node.op === ".") {
6810
+ const chain = memberChain(node);
6811
+ if (chain && chain[0] === ROOT && chain.length > 1) {
6812
+ out.push(chain.slice(1));
6813
+ return;
6814
+ }
6815
+ }
6816
+ rootedPaths(node.args, out);
6817
+ }
6818
+ function classifyIdentifiers(node, values, excluded) {
6819
+ if (Array.isArray(node)) {
6820
+ for (const child of node) classifyIdentifiers(child, values, excluded);
6821
+ return;
6822
+ }
6823
+ if (!isNode3(node)) return;
6824
+ const args = node.args;
6825
+ if (Array.isArray(args)) {
6826
+ const receiver = node.op === "rcall" ? args[1] : args[0];
6827
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6828
+ excluded.add(receiver.args);
6829
+ }
6830
+ if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
6831
+ const macroArgs = args[2];
6832
+ if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
6833
+ const bound = macroArgs[0];
6834
+ if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
6835
+ excluded.add(bound.args);
6836
+ }
6837
+ }
6838
+ }
6839
+ }
6840
+ if (node.op === "id" && typeof node.args === "string") {
6841
+ values.add(node.args);
6842
+ return;
6843
+ }
6844
+ classifyIdentifiers(args, values, excluded);
6845
+ }
6846
+ function predicateSource2(v) {
6847
+ if (typeof v === "string") return v;
6848
+ if (v && typeof v === "object" && typeof v.source === "string") {
6849
+ return v.source;
6850
+ }
6851
+ return void 0;
6852
+ }
6853
+ function isRec19(v) {
6854
+ return !!v && typeof v === "object" && !Array.isArray(v);
6855
+ }
6856
+ function schemaIdOf(view) {
6857
+ const data = view.data;
6858
+ if (!isRec19(data)) return void 0;
6859
+ if (data.provider !== "schema") return void 0;
6860
+ return typeof data.schemaId === "string" ? data.schemaId : void 0;
6861
+ }
6862
+ function checkPredicate(source, scope, where, path, findings) {
6863
+ const ast = parseCelToAst3(source);
6864
+ if (!ast) return;
6865
+ const paths = [];
6866
+ rootedPaths(ast, paths);
6867
+ for (const segments of paths) {
6868
+ let cursor = scope;
6869
+ const walked = [];
6870
+ for (const segment of segments) {
6871
+ const step = stepInto(cursor, segment);
6872
+ if (step.kind === "opaque") break;
6873
+ if (step.kind === "undeclared") {
6874
+ const full = [ROOT, ...walked, segment].join(".");
6875
+ const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
6876
+ findings.push({
6877
+ severity: "error",
6878
+ rule: PREDICATE_PATH_UNRESOLVED,
6879
+ where,
6880
+ path,
6881
+ 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).`,
6882
+ hint: `${formatSuggestion4(findClosestMatches4(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
6883
+ });
6884
+ break;
6885
+ }
6886
+ walked.push(segment);
6887
+ cursor = step.next;
6888
+ }
6889
+ }
6890
+ const declaredHere = keysOf(scope);
6891
+ if (!declaredHere) return;
6892
+ const values = /* @__PURE__ */ new Set();
6893
+ const excluded = /* @__PURE__ */ new Set();
6894
+ classifyIdentifiers(ast, values, excluded);
6895
+ for (const id of values) {
6896
+ if (excluded.has(id) || !declaredHere.includes(id)) continue;
6897
+ findings.push({
6898
+ severity: "error",
6899
+ rule: PREDICATE_PATH_UNROOTED,
6900
+ where,
6901
+ path,
6902
+ 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).`,
6903
+ 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).`
6904
+ });
6905
+ }
6906
+ }
6907
+ function walkFields(entries, scope, where, base, findings, depth) {
6908
+ if (!Array.isArray(entries) || depth > 12) return;
6909
+ for (let i = 0; i < entries.length; i++) {
6910
+ const entry = entries[i];
6911
+ if (!isRec19(entry)) continue;
6912
+ const path = `${base}[${i}]`;
6913
+ for (const key of PREDICATE_KEYS) {
6914
+ const source = predicateSource2(entry[key]);
6915
+ if (source !== void 0 && source.trim()) {
6916
+ checkPredicate(source, scope, where, `${path}.${key}`, findings);
6917
+ break;
6918
+ }
6919
+ }
6920
+ if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
6921
+ const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
6922
+ walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
6923
+ }
6924
+ }
6925
+ }
6926
+ function validatePredicatePathRefs(stack, opts = {}) {
6927
+ const resolveSchema = opts.resolveSchema ?? ((schemaId) => getMetadataTypeSchema(schemaId));
6928
+ const findings = [];
6929
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6930
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
6931
+ for (const site of formViewSites(view, viewPath)) {
6932
+ const schemaId = schemaIdOf(site.view);
6933
+ if (!schemaId) continue;
6934
+ let root;
6935
+ try {
6936
+ root = resolveSchema(schemaId);
6937
+ } catch {
6938
+ continue;
6939
+ }
6940
+ if (!root) continue;
6941
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
6942
+ for (const bucket of ["sections", "groups"]) {
6943
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6944
+ for (let s = 0; s < sections.length; s++) {
6945
+ const section = sections[s];
6946
+ if (!isRec19(section)) continue;
6947
+ const sectionPath = `${site.path}.${bucket}[${s}]`;
6948
+ for (const key of PREDICATE_KEYS) {
6949
+ const source = predicateSource2(section[key]);
6950
+ if (source !== void 0 && source.trim()) {
6951
+ checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
6952
+ break;
6953
+ }
6954
+ }
6955
+ walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
6307
6956
  }
6308
6957
  }
6309
6958
  }
@@ -6337,7 +6986,7 @@ var OWD_WIDTH = {
6337
6986
  public_read: 1,
6338
6987
  public_read_write: 2
6339
6988
  };
6340
- function asArray36(v) {
6989
+ function asArray35(v) {
6341
6990
  if (Array.isArray(v)) return v;
6342
6991
  if (v && typeof v === "object") {
6343
6992
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6363,7 +7012,7 @@ function refOf(def) {
6363
7012
  return typeof r === "string" && r ? r : void 0;
6364
7013
  }
6365
7014
  function firstMasterDetailField(obj) {
6366
- for (const f of asArray36(obj.fields)) {
7015
+ for (const f of asArray35(obj.fields)) {
6367
7016
  if (f.type === "master_detail") {
6368
7017
  return { name: String(f.name ?? "?"), parent: refOf(f) };
6369
7018
  }
@@ -6376,8 +7025,8 @@ function grantsObjectAccess(p) {
6376
7025
  function validateSecurityPosture(stack, opts) {
6377
7026
  const findings = [];
6378
7027
  if (!stack || typeof stack !== "object") return findings;
6379
- const objects = asArray36(stack.objects);
6380
- const permissionSets = asArray36(stack.permissions);
7028
+ const objects = asArray35(stack.objects);
7029
+ const permissionSets = asArray35(stack.permissions);
6381
7030
  for (let i = 0; i < objects.length; i++) {
6382
7031
  const obj = objects[i];
6383
7032
  if (!obj || typeof obj !== "object") continue;
@@ -6506,10 +7155,10 @@ function validateSecurityPosture(stack, opts) {
6506
7155
  if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
6507
7156
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
6508
7157
  flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
6509
- for (const f of asArray36(obj.fields)) {
7158
+ for (const f of asArray35(obj.fields)) {
6510
7159
  flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
6511
7160
  }
6512
- for (const [ai, action] of asArray36(obj.actions).entries()) {
7161
+ for (const [ai, action] of asArray35(obj.actions).entries()) {
6513
7162
  flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
6514
7163
  }
6515
7164
  }
@@ -6518,19 +7167,19 @@ function validateSecurityPosture(stack, opts) {
6518
7167
  if (!ps || typeof ps !== "object") continue;
6519
7168
  flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
6520
7169
  }
6521
- for (const [i, pos] of asArray36(stack.positions).entries()) {
7170
+ for (const [i, pos] of asArray35(stack.positions).entries()) {
6522
7171
  flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
6523
7172
  }
6524
- for (const [i, app] of asArray36(stack.apps).entries()) {
7173
+ for (const [i, app] of asArray35(stack.apps).entries()) {
6525
7174
  flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
6526
7175
  }
6527
- for (const [i, book] of asArray36(stack.books).entries()) {
7176
+ for (const [i, book] of asArray35(stack.books).entries()) {
6528
7177
  flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
6529
7178
  }
6530
7179
  const stackSetNames = new Set(
6531
7180
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
6532
7181
  );
6533
- for (const [i, book] of asArray36(stack.books).entries()) {
7182
+ for (const [i, book] of asArray35(stack.books).entries()) {
6534
7183
  const audience = book.audience;
6535
7184
  if (!audience || typeof audience !== "object") continue;
6536
7185
  const setName = audience.permissionSet;
@@ -6608,7 +7257,7 @@ function validateSecurityPosture(stack, opts) {
6608
7257
  }
6609
7258
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
6610
7259
  const nowMs = opts?.nowMs ?? Date.now();
6611
- for (const [i, seed] of asArray36(stack.data).entries()) {
7260
+ for (const [i, seed] of asArray35(stack.data).entries()) {
6612
7261
  const seedObject = typeof seed.object === "string" ? seed.object : "";
6613
7262
  if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
6614
7263
  const records = Array.isArray(seed.records) ? seed.records : [];
@@ -6653,7 +7302,7 @@ var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
6653
7302
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
6654
7303
  var ORG_PARENT_FIELD = "parent_organization_id";
6655
7304
  var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
6656
- function asArray37(v) {
7305
+ function asArray36(v) {
6657
7306
  if (Array.isArray(v)) return v;
6658
7307
  if (v && typeof v === "object") {
6659
7308
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6683,9 +7332,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
6683
7332
  function validateOrgAxisRedLines(stack) {
6684
7333
  const findings = [];
6685
7334
  const cfg = stack ?? {};
6686
- const permissionSets = asArray37(cfg.permissions);
7335
+ const permissionSets = asArray36(cfg.permissions);
6687
7336
  permissionSets.forEach((ps, psIndex) => {
6688
- asArray37(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7337
+ asArray36(ps.rowLevelSecurity).forEach((policy, pIndex) => {
6689
7338
  for (const clause of ["using", "check"]) {
6690
7339
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
6691
7340
  findings.push({
@@ -6699,7 +7348,7 @@ function validateOrgAxisRedLines(stack) {
6699
7348
  }
6700
7349
  });
6701
7350
  });
6702
- asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
7351
+ asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
6703
7352
  const slots = [
6704
7353
  { key: "condition", text: expressionText(rule.condition) },
6705
7354
  { key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
@@ -6717,9 +7366,9 @@ function validateOrgAxisRedLines(stack) {
6717
7366
  }
6718
7367
  });
6719
7368
  const tenancyDisabledObjects = new Set(
6720
- asArray37(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
7369
+ asArray36(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
6721
7370
  );
6722
- asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
7371
+ asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
6723
7372
  const target = str(rule.object);
6724
7373
  if (!target || !tenancyDisabledObjects.has(target)) return;
6725
7374
  const sharedWith = rule.sharedWith;
@@ -6742,7 +7391,7 @@ function validateOrgAxisRedLines(stack) {
6742
7391
  import { compileCelToFilter } from "@objectstack/formula";
6743
7392
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
6744
7393
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
6745
- function asArray38(v) {
7394
+ function asArray37(v) {
6746
7395
  if (Array.isArray(v)) return v;
6747
7396
  if (v && typeof v === "object") {
6748
7397
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6769,7 +7418,7 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
6769
7418
  function validateSharingRuleEnforceability(stack) {
6770
7419
  const findings = [];
6771
7420
  const cfg = stack ?? {};
6772
- asArray38(cfg.sharingRules).forEach((rule, index) => {
7421
+ asArray37(cfg.sharingRules).forEach((rule, index) => {
6773
7422
  const input = toCompilerInput(rule.condition);
6774
7423
  if (input === null) return;
6775
7424
  const result = compileCelToFilter(input, { variables: {} });
@@ -6805,10 +7454,16 @@ function validateSharingRuleEnforceability(stack) {
6805
7454
  }
6806
7455
 
6807
7456
  // src/validate-rls-predicate-enforceability.ts
6808
- import { isPushdownableCel, isSupportedRlsExpression, sqlPredicateToCel } from "@objectstack/formula";
7457
+ import {
7458
+ isPushdownableCel,
7459
+ isSupportedRlsExpression,
7460
+ parseCelToAstWithReason as parseCelToAstWithReason2,
7461
+ sqlPredicateToCel
7462
+ } from "@objectstack/formula";
6809
7463
  var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
6810
7464
  var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
6811
- function asArray39(v) {
7465
+ var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
7466
+ function asArray38(v) {
6812
7467
  if (Array.isArray(v)) return v;
6813
7468
  if (v && typeof v === "object") {
6814
7469
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6819,6 +7474,13 @@ function str3(v) {
6819
7474
  return typeof v === "string" ? v : "";
6820
7475
  }
6821
7476
  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.";
7477
+ function boundsOverrunOf(bridged) {
7478
+ const parsed = parseCelToAstWithReason2(bridged);
7479
+ return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
7480
+ }
7481
+ function quote(source) {
7482
+ return source.length > 200 ? `${source.slice(0, 197)}...` : source;
7483
+ }
6822
7484
  function consequence(clause) {
6823
7485
  const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
6824
7486
  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.";
@@ -6826,20 +7488,36 @@ function consequence(clause) {
6826
7488
  function validateRlsPredicateEnforceability(stack) {
6827
7489
  const findings = [];
6828
7490
  const cfg = stack ?? {};
6829
- asArray39(cfg.permissions).forEach((ps, psIndex) => {
6830
- asArray39(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7491
+ asArray38(cfg.permissions).forEach((ps, psIndex) => {
7492
+ asArray38(ps.rowLevelSecurity).forEach((policy, pIndex) => {
6831
7493
  for (const clause of ["using", "check"]) {
6832
7494
  const source = str3(policy[clause]);
6833
7495
  if (!source.trim()) continue;
6834
7496
  if (isSupportedRlsExpression(source)) continue;
6835
- const why = isPushdownableCel(sqlPredicateToCel(source));
7497
+ const bridged = sqlPredicateToCel(source);
7498
+ const why = isPushdownableCel(bridged);
6836
7499
  const detail = why.ok ? "" : why.detail;
6837
7500
  const parseError = !why.ok && why.reason === "parse-error";
7501
+ const overrun = parseError ? boundsOverrunOf(bridged) : null;
6838
7502
  const psName = str3(ps.name) || String(psIndex);
6839
7503
  const policyName = str3(policy.name) || String(pIndex);
6840
7504
  const object = str3(policy.object);
6841
7505
  const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
6842
7506
  const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
7507
+ if (overrun) {
7508
+ const bound = overrun.limit ?? "an unnamed platform CEL bound";
7509
+ const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
7510
+ const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
7511
+ findings.push({
7512
+ severity: "error",
7513
+ rule: RLS_PREDICATE_OVER_BUDGET,
7514
+ where,
7515
+ path,
7516
+ message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
7517
+ 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).`
7518
+ });
7519
+ continue;
7520
+ }
6843
7521
  if (parseError) {
6844
7522
  findings.push({
6845
7523
  severity: "error",
@@ -6870,11 +7548,11 @@ import { createRequire as createRequire4 } from "module";
6870
7548
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
6871
7549
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
6872
7550
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
6873
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6874
- function asArray40(v) {
6875
- if (Array.isArray(v)) return v.filter(isRec16);
6876
- if (isRec16(v)) {
6877
- return Object.entries(v).filter(([, def]) => isRec16(def)).map(([name, def]) => ({ name, ...def }));
7551
+ var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7552
+ function asArray39(v) {
7553
+ if (Array.isArray(v)) return v.filter(isRec20);
7554
+ if (isRec20(v)) {
7555
+ return Object.entries(v).filter(([, def]) => isRec20(def)).map(([name, def]) => ({ name, ...def }));
6878
7556
  }
6879
7557
  return [];
6880
7558
  }
@@ -6891,7 +7569,7 @@ function loadAjv() {
6891
7569
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
6892
7570
  );
6893
7571
  }
6894
- const ctor = isRec16(mod) && "default" in mod ? mod.default : mod;
7572
+ const ctor = isRec20(mod) && "default" in mod ? mod.default : mod;
6895
7573
  cachedAjv = ctor;
6896
7574
  return ctor;
6897
7575
  }
@@ -6906,7 +7584,7 @@ function loadAddFormats() {
6906
7584
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
6907
7585
  );
6908
7586
  }
6909
- const plugin = isRec16(mod) && "default" in mod ? mod.default : mod;
7587
+ const plugin = isRec20(mod) && "default" in mod ? mod.default : mod;
6910
7588
  cachedAddFormats = plugin;
6911
7589
  return plugin;
6912
7590
  }
@@ -6936,17 +7614,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
6936
7614
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
6937
7615
  for (const branch of ["then", "otherwise"]) {
6938
7616
  const nested = rule[branch];
6939
- if (isRec16(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
7617
+ if (isRec20(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6940
7618
  }
6941
7619
  return out;
6942
7620
  }
6943
7621
  function walkObjectValidationRules(stack) {
6944
7622
  const walked = [];
6945
- if (!isRec16(stack)) return walked;
6946
- for (const obj of asArray40(stack.objects)) {
7623
+ if (!isRec20(stack)) return walked;
7624
+ for (const obj of asArray39(stack.objects)) {
6947
7625
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6948
7626
  const validations = obj.validations;
6949
- for (const authored of asArray40(validations)) {
7627
+ for (const authored of asArray39(validations)) {
6950
7628
  for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
6951
7629
  walked.push({
6952
7630
  rule,
@@ -6977,7 +7655,7 @@ function validateRuleCompilability(stack) {
6977
7655
  });
6978
7656
  }
6979
7657
  }
6980
- if (rule.type === "json_schema" && isRec16(rule.schema)) {
7658
+ if (rule.type === "json_schema" && isRec20(rule.schema)) {
6981
7659
  try {
6982
7660
  createRuntimeAjv().compile(rule.schema);
6983
7661
  } catch (err) {
@@ -6997,7 +7675,7 @@ function validateRuleCompilability(stack) {
6997
7675
 
6998
7676
  // src/validate-rule-schema-formats.ts
6999
7677
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7000
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7678
+ var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7001
7679
  var SUBSCHEMA_KEYS = [
7002
7680
  "additionalItems",
7003
7681
  "additionalProperties",
@@ -7021,7 +7699,7 @@ var SUBSCHEMA_MAP_KEYS = [
7021
7699
  var MAX_SCHEMA_WALK_DEPTH = 32;
7022
7700
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7023
7701
  function collectFormatUses(schema, pointer, out, depth) {
7024
- if (!isRec17(schema)) return;
7702
+ if (!isRec21(schema)) return;
7025
7703
  if (typeof schema.format === "string") {
7026
7704
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7027
7705
  }
@@ -7040,7 +7718,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7040
7718
  }
7041
7719
  for (const key of SUBSCHEMA_MAP_KEYS) {
7042
7720
  const value = schema[key];
7043
- if (!isRec17(value)) continue;
7721
+ if (!isRec21(value)) continue;
7044
7722
  for (const [name, entry] of Object.entries(value)) {
7045
7723
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7046
7724
  }
@@ -7048,13 +7726,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7048
7726
  const items = schema.items;
7049
7727
  if (Array.isArray(items)) {
7050
7728
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7051
- } else if (isRec17(items)) {
7729
+ } else if (isRec21(items)) {
7052
7730
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7053
7731
  }
7054
7732
  const dependencies = schema.dependencies;
7055
- if (isRec17(dependencies)) {
7733
+ if (isRec21(dependencies)) {
7056
7734
  for (const [name, entry] of Object.entries(dependencies)) {
7057
- if (!isRec17(entry)) continue;
7735
+ if (!isRec21(entry)) continue;
7058
7736
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7059
7737
  }
7060
7738
  }
@@ -7093,7 +7771,7 @@ function validateRuleSchemaFormats(stack) {
7093
7771
  const findings = [];
7094
7772
  const pending = [];
7095
7773
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7096
- if (rule.type !== "json_schema" || !isRec17(rule.schema)) continue;
7774
+ if (rule.type !== "json_schema" || !isRec21(rule.schema)) continue;
7097
7775
  const uses = [];
7098
7776
  collectFormatUses(rule.schema, "", uses, 0);
7099
7777
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7119,14 +7797,14 @@ function validateRuleSchemaFormats(stack) {
7119
7797
 
7120
7798
  // src/validate-action-locations.ts
7121
7799
  var ACTION_NO_PLACEMENT = "action-no-placement";
7122
- function asArray41(v) {
7800
+ function asArray40(v) {
7123
7801
  if (Array.isArray(v)) return v;
7124
7802
  if (v && typeof v === "object") {
7125
7803
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7126
7804
  }
7127
7805
  return [];
7128
7806
  }
7129
- function strName17(v) {
7807
+ function strName19(v) {
7130
7808
  return typeof v === "string" && v.length > 0 ? v : void 0;
7131
7809
  }
7132
7810
  function strList3(v) {
@@ -7140,8 +7818,8 @@ function collectNamePlacedActions(stack) {
7140
7818
  for (const key of ["rowActions", "bulkActions"]) {
7141
7819
  for (const n of strList3(list3[key])) placed.add(n);
7142
7820
  }
7143
- for (const def of asArray41(list3.bulkActionDefs)) {
7144
- const n = strName17(def?.name);
7821
+ for (const def of asArray40(list3.bulkActionDefs)) {
7822
+ const n = strName19(def?.name);
7145
7823
  if (n) placed.add(n);
7146
7824
  }
7147
7825
  };
@@ -7149,12 +7827,12 @@ function collectNamePlacedActions(stack) {
7149
7827
  if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
7150
7828
  for (const lv of Object.values(listViews)) harvest(lv);
7151
7829
  };
7152
- for (const view of asArray41(stack.views)) {
7830
+ for (const view of asArray40(stack.views)) {
7153
7831
  if (!view || typeof view !== "object") continue;
7154
7832
  harvest(view.list);
7155
7833
  harvestListViews(view.listViews);
7156
7834
  }
7157
- for (const obj of asArray41(stack.objects)) {
7835
+ for (const obj of asArray40(stack.objects)) {
7158
7836
  if (!obj || typeof obj !== "object") continue;
7159
7837
  harvestListViews(obj.listViews);
7160
7838
  }
@@ -7167,7 +7845,7 @@ function validateActionLocations(stack) {
7167
7845
  const check = (action, path) => {
7168
7846
  if (!action || typeof action !== "object") return;
7169
7847
  if ("locations" in action) return;
7170
- const name = strName17(action.name);
7848
+ const name = strName19(action.name);
7171
7849
  if (!name) return;
7172
7850
  if (namePlaced.has(name)) return;
7173
7851
  findings.push({
@@ -7176,16 +7854,16 @@ function validateActionLocations(stack) {
7176
7854
  where: `action "${name}"`,
7177
7855
  path,
7178
7856
  message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
7179
- hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`, `global_nav`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
7857
+ hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
7180
7858
  });
7181
7859
  };
7182
- const actions = asArray41(stack.actions);
7860
+ const actions = asArray40(stack.actions);
7183
7861
  for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
7184
- const objects = asArray41(stack.objects);
7862
+ const objects = asArray40(stack.objects);
7185
7863
  for (let oi = 0; oi < objects.length; oi++) {
7186
7864
  const obj = objects[oi];
7187
7865
  if (!obj || typeof obj !== "object") continue;
7188
- const own = asArray41(obj.actions);
7866
+ const own = asArray40(obj.actions);
7189
7867
  for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
7190
7868
  }
7191
7869
  return findings;
@@ -7197,7 +7875,8 @@ import {
7197
7875
  APPROVAL_REVISE_NODE_TYPE,
7198
7876
  collectFlowGraphs as collectFlowGraphs2
7199
7877
  } from "@objectstack/spec/automation";
7200
- function asArray42(v) {
7878
+ import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
7879
+ function asArray41(v) {
7201
7880
  if (Array.isArray(v)) return v;
7202
7881
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7203
7882
  return [];
@@ -7471,7 +8150,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
7471
8150
  function filterCarriesNoCondition(filter) {
7472
8151
  if (filter === void 0 || filter === null) return true;
7473
8152
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
7474
- return Object.keys(filter).length === 0;
8153
+ return reduceFilterVerdict2(filter) === "true";
8154
+ }
8155
+ function describeUnboundedFilter(filter) {
8156
+ if (filter === void 0 || filter === null) return "no `filter` key";
8157
+ if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
8158
+ return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
8159
+ }
8160
+ function previewFilter(filter) {
8161
+ try {
8162
+ const json = JSON.stringify(filter);
8163
+ if (typeof json !== "string") return typeof filter;
8164
+ return json.length > 80 ? `${json.slice(0, 77)}...` : json;
8165
+ } catch {
8166
+ return typeof filter;
8167
+ }
7475
8168
  }
7476
8169
  function scanUnboundedBulkWrites(at, nodes, findings) {
7477
8170
  for (const node of nodes) {
@@ -7482,10 +8175,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
7482
8175
  if (cfg.multi !== true) continue;
7483
8176
  if (!filterCarriesNoCondition(cfg.filter)) continue;
7484
8177
  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`";
8178
+ const filterState = describeUnboundedFilter(cfg.filter);
7486
8179
  findings.push({
7487
8180
  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.`,
8181
+ 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
8182
  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
8183
  // Warning, not `error`: see the severity policy at the top of this file.
7491
8184
  // The shape has a legitimate reading the engine grants on purpose, so it is
@@ -7567,7 +8260,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
7567
8260
  }
7568
8261
  function lintFlowPatterns(stack) {
7569
8262
  const findings = [];
7570
- for (const flow of asArray42(stack.flows)) {
8263
+ for (const flow of asArray41(stack.flows)) {
7571
8264
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
7572
8265
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
7573
8266
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
@@ -7664,7 +8357,7 @@ import { dirname, join } from "path";
7664
8357
  import { existsSync, readFileSync } from "fs";
7665
8358
  var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
7666
8359
  var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
7667
- function asArray43(v) {
8360
+ function asArray42(v) {
7668
8361
  if (Array.isArray(v)) return v;
7669
8362
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7670
8363
  return [];
@@ -7786,6 +8479,13 @@ var TYPE_COLLECTIONS = [
7786
8479
  // checks every widget on the dashboard. Registering it here is not optional
7787
8480
  // bookkeeping: without it the ledger would be newly correct and newly
7788
8481
  // silent, which is the shape this lint exists to prevent.
8482
+ //
8483
+ // As of #6774 the dashboard ledger warns on NOTHING — four of those five were
8484
+ // retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
8485
+ // gave it a renderer. The type STAYS listed, the resolved state `webhook` and
8486
+ // `email_template` already sit in: a zero-warn entry costs one empty map
8487
+ // lookup, and it means a future regression that re-deadens a widget key warns
8488
+ // on its own instead of waiting for someone to notice this list again.
7789
8489
  { type: "dashboard", key: "dashboards" }
7790
8490
  ];
7791
8491
  function lintLivenessProperties(stack) {
@@ -7794,11 +8494,11 @@ function lintLivenessProperties(stack) {
7794
8494
  const findings = [];
7795
8495
  const objectWarn = loadWarnMap(dir, "object");
7796
8496
  const fieldWarn = loadWarnMap(dir, "field");
7797
- for (const obj of asArray43(stack.objects)) {
8497
+ for (const obj of asArray42(stack.objects)) {
7798
8498
  const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7799
8499
  if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
7800
8500
  if (fieldWarn.size > 0) {
7801
- for (const field of asArray43(obj.fields)) {
8501
+ for (const field of asArray42(obj.fields)) {
7802
8502
  const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
7803
8503
  checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
7804
8504
  }
@@ -7807,7 +8507,7 @@ function lintLivenessProperties(stack) {
7807
8507
  for (const { type, key } of TYPE_COLLECTIONS) {
7808
8508
  const warnMap = loadWarnMap(dir, type);
7809
8509
  if (warnMap.size === 0) continue;
7810
- for (const item of asArray43(stack[key])) {
8510
+ for (const item of asArray42(stack[key])) {
7811
8511
  const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
7812
8512
  checkItem(type, item, `${type} '${name}'`, warnMap, findings);
7813
8513
  }
@@ -7821,7 +8521,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7821
8521
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7822
8522
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
7823
8523
  var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
7824
- function asArray44(v) {
8524
+ function asArray43(v) {
7825
8525
  if (Array.isArray(v)) return v;
7826
8526
  if (v && typeof v === "object") {
7827
8527
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7830,9 +8530,9 @@ function asArray44(v) {
7830
8530
  }
7831
8531
  function lintAutonumberFormats(stack) {
7832
8532
  const findings = [];
7833
- for (const obj of asArray44(stack.objects)) {
8533
+ for (const obj of asArray43(stack.objects)) {
7834
8534
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7835
- const fields = asArray44(obj.fields);
8535
+ const fields = asArray43(obj.fields);
7836
8536
  const fieldMeta = /* @__PURE__ */ new Map();
7837
8537
  for (const f of fields) {
7838
8538
  if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
@@ -7898,7 +8598,7 @@ function lintAutonumberFormats(stack) {
7898
8598
 
7899
8599
  // src/lint-view-refs.ts
7900
8600
  import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
7901
- function asArray45(v) {
8601
+ function asArray44(v) {
7902
8602
  if (Array.isArray(v)) return v;
7903
8603
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7904
8604
  return [];
@@ -7926,7 +8626,7 @@ function lintViewRefs(stack) {
7926
8626
  s.add(kind);
7927
8627
  };
7928
8628
  const containers = [];
7929
- for (const v of asArray45(stack.views)) {
8629
+ for (const v of asArray44(stack.views)) {
7930
8630
  if (v.viewKind) {
7931
8631
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7932
8632
  continue;
@@ -7935,7 +8635,7 @@ function lintViewRefs(stack) {
7935
8635
  const object = viewContainerObjectName(v);
7936
8636
  if (object) containers.push({ object, container: v });
7937
8637
  }
7938
- for (const obj of asArray45(stack.objects)) {
8638
+ for (const obj of asArray44(stack.objects)) {
7939
8639
  const object = typeof obj.name === "string" ? obj.name : void 0;
7940
8640
  if (!object) continue;
7941
8641
  if (obj.list || obj.form || obj.listViews || obj.formViews) {
@@ -7989,11 +8689,11 @@ function lintViewRefs(stack) {
7989
8689
  });
7990
8690
  }
7991
8691
  };
7992
- for (const obj of asArray45(stack.objects)) {
8692
+ for (const obj of asArray44(stack.objects)) {
7993
8693
  const object = typeof obj.name === "string" ? obj.name : void 0;
7994
- for (const action of asArray45(obj.actions)) checkAction(action, object);
8694
+ for (const action of asArray44(obj.actions)) checkAction(action, object);
7995
8695
  }
7996
- for (const action of asArray45(stack.actions)) checkAction(action);
8696
+ for (const action of asArray44(stack.actions)) checkAction(action);
7997
8697
  return findings;
7998
8698
  }
7999
8699
 
@@ -8128,6 +8828,7 @@ var CLI_ONLY = ["cli"];
8128
8828
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
8129
8829
  var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
8130
8830
  var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
8831
+ var RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY = "Deliberate, and not a snapshot limitation: this rule needs only the written item, but every other rule on the `views[]` visibility-predicate surface (validate-visibility-predicates.ts) is CLI-only. Gating one of three sibling verdicts about the same predicate at the Studio door is less predictable than gating none; move the family together, as one measured edit.";
8131
8832
  var RUNTIME_OBJECT_WRITES_P2 = "P2 (#4463): judges an object/field declaration. Object writes are the hottest metadata path in the product, so P1 gates `flow` first and widens once the gate has real traffic behind it.";
8132
8833
  var EXPRESSION_INVALID = "expression-invalid";
8133
8834
  var AUTHORING_RULES = [
@@ -8157,8 +8858,12 @@ var AUTHORING_RULES = [
8157
8858
  }))
8158
8859
  },
8159
8860
  // ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
8160
- // mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
8161
- // read the pre-parse tier or the evidence is already gone.
8861
+ // mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
8862
+ // is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
8863
+ // refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
8864
+ // THROWS on both. `normalized` here therefore means "needs no parsed stack"
8865
+ // (so `os lint`, which never parses, can run it), not "sees evidence the
8866
+ // parse would have eaten".
8162
8867
  {
8163
8868
  name: "validateListViewMode",
8164
8869
  tier: "gating",
@@ -8189,9 +8894,13 @@ var AUTHORING_RULES = [
8189
8894
  surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8190
8895
  run: (stack) => validateFunctionalCompleteness(stack)
8191
8896
  },
8192
- // A flat list-view object in `views: []` parses to an EMPTY container
8193
- // (ViewSchema strips unknown keys): the schema step passes, zero views
8194
- // register, and the Console renders nothing. Pre-parse for the same reason.
8897
+ // A view container in `views: []` that registers zero views: nothing appears
8898
+ // in the Console, and the schema step cannot tell it from an intentionally
8899
+ // empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
8900
+ // went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
8901
+ // data }` by name with the wrap-it hint (measured under #6073); the arm that
8902
+ // still needs a rule is the all-slots-empty container, whose keys are all
8903
+ // declared and which survives the parse untouched.
8195
8904
  {
8196
8905
  name: "validateViewContainers",
8197
8906
  tier: "gating",
@@ -8241,6 +8950,31 @@ var AUTHORING_RULES = [
8241
8950
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8242
8951
  run: (stack) => validateFilterTokens(stack)
8243
8952
  },
8953
+ // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
8954
+ // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
8955
+ // this rule does not touch it: it refuses the literal SPELLINGS at authoring
8956
+ // time with a per-shape prescription, which is Prime Directive #12's standard
8957
+ // shape (reject at the producer, never tolerate at the consumer) and #5240's
8958
+ // same-direction precedent one shape over.
8959
+ {
8960
+ name: "validateEmptyCombinators",
8961
+ tier: "gating",
8962
+ input: "parsed",
8963
+ commands: ALL,
8964
+ source: "packages/lint/src/validate-empty-combinators.ts",
8965
+ // The one type #4463's P1 slice opened, and the one this rule most needs:
8966
+ // a flow CRUD node's `config.filter` is where an empty combinator has the
8967
+ // largest blast radius, and the write path is the only door an AI author
8968
+ // uses. This rule needs NO resolution context at all — it judges the filter
8969
+ // literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
8970
+ // it, and widening to the other filter-carrying types (`object`, `view`,
8971
+ // `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
8972
+ // opens them at the gate. Making that call here would widen the gate's
8973
+ // dispatch surface on this rule's authority, which is P2's decision.
8974
+ surfaces: CLI_AND_RUNTIME,
8975
+ runtimeTypes: ["flow"],
8976
+ run: (stack) => validateEmptyCombinators(stack)
8977
+ },
8244
8978
  // The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
8245
8979
  // rules that answer "does this name resolve to anything?". It reached all
8246
8980
  // three commands before this file existed; it is an entry here so the two
@@ -8289,6 +9023,16 @@ var AUTHORING_RULES = [
8289
9023
  // `displayField` (#5775) — so gating today would fail the platform's own pages
8290
9024
  // to enforce declarations the platform does not keep. The error upgrade is a
8291
9025
  // separate step, once the warning-period inventory is empty.
9026
+ //
9027
+ // #5775 settled the record picker's half: `displayField` is retired in favour
9028
+ // of the `labelField` the renderer actually reads. Its claim that "the rest of
9029
+ // the keys the renderers honour are declared" did NOT hold — #6776 found five
9030
+ // more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
9031
+ // `page:accordion.variant`, and the tab strip's visual style, whose declared
9032
+ // spelling `page:tabs.type` collided with the component node's own dispatch
9033
+ // key and so was unauthorable in the flat and JSX carriers). All five are
9034
+ // declared as of #6776, the last as the renamed `tabStyle`. What remains
9035
+ // before the error upgrade is #5728 and two page rewrites.
8292
9036
  {
8293
9037
  name: "validateComponentProps",
8294
9038
  tier: "advisory",
@@ -8367,10 +9111,12 @@ var AUTHORING_RULES = [
8367
9111
  //
8368
9112
  // `gating` since #5762, which reviewed the file's rules as one family and
8369
9113
  // split them on a single question: is THIS STACK enough to know the flow is
8370
- // dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
9114
+ // dead? Four rules answer yes and emit `error` — a `config.timeRelative`
8371
9115
  // the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
8372
- // predicate cannot route at all, and a `record-*` triggerType outside the
8373
- // closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
9116
+ // predicate cannot route at all, a `record-*` triggerType outside the
9117
+ // closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
9118
+ // `type: 'record_change'` flow whose triggerType the engine's binding resolver
9119
+ // routes nowhere, silently demoting it to a manual flow. None of those verdicts
8374
9120
  // can be changed by installing a package, so there is no reading under which
8375
9121
  // the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
8376
9122
  // (the object may come from another installed package — a hedge this rule
@@ -8497,12 +9243,39 @@ var AUTHORING_RULES = [
8497
9243
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8498
9244
  run: (stack) => validateSeedStateMachine(stack)
8499
9245
  },
8500
- // ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
8501
- // Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
8502
- // during parse, so the alias the author wrote is gone from `result.data`.
9246
+ // ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
9247
+ // gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
9248
+ // schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
9249
+ // the alias the author wrote is gone from `result.data`". Measured false at
9250
+ // #6073: the ADR-0087 D2 conversions do that fold INSIDE
9251
+ // `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
9252
+ // alias site the alias-KEY rule reported zero here too.
9253
+ //
9254
+ // #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
9255
+ // re-anchored. Re-anchoring would have had to move this entry's input to a
9256
+ // pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
9257
+ // a change to this package's external input contract, and the maintainer's
9258
+ // call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
9259
+ // costs no author a signal: the same D2 conversion already shouts through
9260
+ // `warnConversionNotice` in `defineStack`, naming the site, the conversion and
9261
+ // the protocol-16 retirement window — better wording than the rule ever had.
9262
+ //
9263
+ // Every rule left in the family judges the predicate's VALUE, and the value
9264
+ // moves into `visibleWhen` intact, so all three report normally on this tier.
9265
+ // The tier therefore stays `normalized` on its SURVIVING justification (a
9266
+ // finding still reaches the author when an unrelated schema error would stop
9267
+ // the parse — see `AuthoringRuleInputTier`), never on the retired
9268
+ // "pre-parse evidence" one.
9269
+ //
9270
+ // `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
9271
+ // ADR-0089 rules stay advisory findings within it — the tier is a property of
9272
+ // the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
9273
+ // what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
9274
+ // has worked since #3760. The promotion follows the #5762 precedent: a family
9275
+ // that gains an `error` finding moves its registry tier in the same edit.
8503
9276
  {
8504
9277
  name: "validateVisibilityPredicates",
8505
- tier: "advisory",
9278
+ tier: "gating",
8506
9279
  input: "normalized",
8507
9280
  commands: ALL,
8508
9281
  source: "packages/lint/src/validate-visibility-predicates.ts",
@@ -8510,6 +9283,30 @@ var AUTHORING_RULES = [
8510
9283
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8511
9284
  run: (stack) => validateVisibilityPredicates(stack)
8512
9285
  },
9286
+ // #7010 — the same predicate surface, one question further in. The three
9287
+ // ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
9288
+ // rooted, is the root right for the layer) and never open the target schema,
9289
+ // so `data.tpye == 'formula'` passes all three and still resolves to nothing.
9290
+ // This rule resolves the PATH against the schema the form edits — the closed
9291
+ // `getMetadataTypeSchema` key set — and is therefore immune to the CEL
9292
+ // type-name blind spot that made #6248's gate structurally unable to catch
9293
+ // #6254's 16 bare `type ==` predicates.
9294
+ //
9295
+ // Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
9296
+ // the `record.*` layer is deliberately out of scope because an ObjectQL
9297
+ // object's addressable path set is NOT closed (lookup traversal, system
9298
+ // columns, formula outputs), and an `error` gate over an open set generates
9299
+ // false build errors. See the rule's module note.
9300
+ {
9301
+ name: "validatePredicatePathRefs",
9302
+ tier: "gating",
9303
+ input: "normalized",
9304
+ commands: ALL,
9305
+ source: "packages/lint/src/validate-predicate-path-refs.ts",
9306
+ surfaces: CLI_ONLY,
9307
+ surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
9308
+ run: (stack) => validatePredicatePathRefs(stack)
9309
+ },
8513
9310
  // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
8514
9311
  // `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
8515
9312
  // runtime REFUSES to execute), plus `flow-branch-label-unmatched` and