@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.cjs CHANGED
@@ -400,6 +400,38 @@ function validateStackExpressions(stack) {
400
400
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
401
401
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
402
402
  };
403
+ const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
404
+ const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
405
+ 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";
406
+ const FIELD_RULE_SLOT_CONSEQUENCE = {
407
+ 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)",
408
+ 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",
409
+ 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",
410
+ // Listed rather than left to the `??` below, so the map covers every slot
411
+ // the field walk passes and the default stays unreachable. `FieldSchema`
412
+ // declares this key only as a `retiredKey`, which rejects it by name, so
413
+ // there is no fourth runtime to measure — the honest clause is the generic
414
+ // one, not a fabricated fourth cell (#6716).
415
+ conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
416
+ };
417
+ const checkFieldRuleRoot = (where, slot, raw) => {
418
+ const source = celSourceOf(raw);
419
+ if (!source) return;
420
+ const roots = (0, import_formula2.collectCelRootIdentifiers)(source);
421
+ if (!roots.ok) return;
422
+ const kept = import_formula2.SCOPE_ROOTS.filter(
423
+ (r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
424
+ );
425
+ if (kept.length === 0) return;
426
+ const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
427
+ 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}\`.`;
428
+ issues.push({
429
+ where,
430
+ 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,
431
+ source,
432
+ severity: "error"
433
+ });
434
+ };
403
435
  const checkDeclaredPredicate = (where, raw) => {
404
436
  if (raw == null) return;
405
437
  const res = (0, import_formula2.validateExpression)("predicate", raw);
@@ -432,7 +464,14 @@ function validateStackExpressions(stack) {
432
464
  if (retired.length > 0) {
433
465
  issues.push({
434
466
  where: `${at} \xB7 node '${node.id}' (script) callable`,
435
- message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + `Run \`os migrate meta --from 16\` to rewrite it automatically.`,
467
+ 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
468
+ // behaviour, never the retired key's fate — "rewrite it" reads two ways
469
+ // over a branch that DELETES the key (template/recipients/variables/script),
470
+ // "rewrite existing sources" only one. Plain-quoted (not a template literal)
471
+ // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
472
+ // widened scan (#7030) on the same textual shape as the spec corpus — no
473
+ // interpolation lives in this clause, so nothing is lost switching quote style.
474
+ "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
436
475
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
437
476
  });
438
477
  } else if (!fn) {
@@ -466,13 +505,27 @@ function validateStackExpressions(stack) {
466
505
  for (const [fname, f] of fieldList) {
467
506
  for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
468
507
  check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
508
+ checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
469
509
  }
470
- const roWhenSource = celSourceOf(f.readonlyWhen);
471
- if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
510
+ for (const [oi, opt] of asArray(f.options).entries()) {
511
+ const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
512
+ check(
513
+ `object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
514
+ opt.visibleWhen,
515
+ objectName,
516
+ "record"
517
+ );
518
+ }
519
+ for (const [slot, raw, consequence2] of [
520
+ ["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
521
+ ["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`]
522
+ ]) {
523
+ const source = celSourceOf(raw);
524
+ if (masters === 1 || !source || !readsParentRoot(source)) continue;
472
525
  issues.push({
473
- where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
474
- message: `\`readonlyWhen\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and the field would be locked on every write. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
475
- source: roWhenSource,
526
+ where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
527
+ 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\`.`),
528
+ source,
476
529
  severity: "error"
477
530
  });
478
531
  }
@@ -1196,7 +1249,8 @@ function validateDashboardActionRefs(stack) {
1196
1249
 
1197
1250
  // src/validate-filter-tokens.ts
1198
1251
  var import_data3 = require("@objectstack/spec/data");
1199
- var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1252
+
1253
+ // src/filter-walk.ts
1200
1254
  var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
1201
1255
  function asArray5(v) {
1202
1256
  if (Array.isArray(v)) return v;
@@ -1208,7 +1262,62 @@ function asArray5(v) {
1208
1262
  function label(v, fallback) {
1209
1263
  return typeof v === "string" && v.length > 0 ? v : fallback;
1210
1264
  }
1265
+ function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
1266
+ if (!node || typeof node !== "object") return;
1267
+ if (seen.has(node)) return;
1268
+ seen.add(node);
1269
+ if (Array.isArray(node)) {
1270
+ node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
1271
+ return;
1272
+ }
1273
+ for (const [k, v] of Object.entries(node)) {
1274
+ const childPath = `${path}.${k}`;
1275
+ if (FILTER_KEYS.has(k)) {
1276
+ visit({ value: v, path: childPath, where });
1277
+ continue;
1278
+ }
1279
+ scanForFilters(v, childPath, where, visit, seen);
1280
+ }
1281
+ }
1282
+ function walkAuthoredFilters(stack, surfaces, visit) {
1283
+ if (!stack || typeof stack !== "object") return;
1284
+ for (const { key, kind } of surfaces) {
1285
+ const items = asArray5(stack[key]);
1286
+ items.forEach((item, i) => {
1287
+ const name = label(item.name ?? item.id, `#${i}`);
1288
+ if (kind === "dashboard") {
1289
+ const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1290
+ widgets.forEach((w, wi) => {
1291
+ const wName = label(w.id ?? w.title, `#${wi}`);
1292
+ scanForFilters(
1293
+ w,
1294
+ `${key}[${i}].widgets[${wi}]`,
1295
+ `dashboard "${name}" \xB7 widget "${wName}"`,
1296
+ visit,
1297
+ /* @__PURE__ */ new Set()
1298
+ );
1299
+ });
1300
+ const { widgets: _skip, ...rest } = item;
1301
+ scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
1302
+ return;
1303
+ }
1304
+ scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
1305
+ });
1306
+ }
1307
+ }
1308
+
1309
+ // src/validate-filter-tokens.ts
1310
+ var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
1211
1311
  var KNOWN_LIST = import_data3.CONTEXT_TOKENS.join("}, {");
1312
+ var TOKEN_FILTER_SURFACES = [
1313
+ { key: "dashboards", kind: "dashboard" },
1314
+ { key: "objects", kind: "object" },
1315
+ { key: "views", kind: "view" },
1316
+ { key: "reports", kind: "report" },
1317
+ { key: "datasets", kind: "dataset" },
1318
+ { key: "pages", kind: "page" },
1319
+ { key: "apps", kind: "app" }
1320
+ ];
1212
1321
  function walkFilterValues(node, path, where, out, seen) {
1213
1322
  if (node === null || node === void 0) return;
1214
1323
  if (typeof node === "string") {
@@ -1237,58 +1346,132 @@ function walkFilterValues(node, path, where, out, seen) {
1237
1346
  walkFilterValues(v, `${path}.${k}`, where, out, seen);
1238
1347
  }
1239
1348
  }
1240
- function scanForFilters(node, path, where, out, seen) {
1241
- if (!node || typeof node !== "object") return;
1242
- if (seen.has(node)) return;
1243
- seen.add(node);
1244
- if (Array.isArray(node)) {
1245
- node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, out, seen));
1349
+ function validateFilterTokens(stack) {
1350
+ if (!stack || typeof stack !== "object") return [];
1351
+ const out = [];
1352
+ walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
1353
+ walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
1354
+ });
1355
+ return out;
1356
+ }
1357
+
1358
+ // src/validate-empty-combinators.ts
1359
+ var import_data4 = require("@objectstack/spec/data");
1360
+ var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
1361
+ var FILTER_EMPTY_NODE = "filter-empty-node";
1362
+ var EMPTY_COMBINATOR_SURFACES = [
1363
+ { key: "dashboards", kind: "dashboard" },
1364
+ { key: "objects", kind: "object" },
1365
+ { key: "views", kind: "view" },
1366
+ { key: "reports", kind: "report" },
1367
+ { key: "datasets", kind: "dataset" },
1368
+ { key: "pages", kind: "page" },
1369
+ { key: "apps", kind: "app" },
1370
+ { key: "flows", kind: "flow" }
1371
+ ];
1372
+ function isFilterNode(value) {
1373
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1374
+ const proto = Object.getPrototypeOf(value);
1375
+ return proto === Object.prototype || proto === null;
1376
+ }
1377
+ var VERDICT_OF = {
1378
+ $and: (0, import_data4.reduceFilterVerdict)({ $and: [] }),
1379
+ $or: (0, import_data4.reduceFilterVerdict)({ $or: [] }),
1380
+ $not: (0, import_data4.reduceFilterVerdict)({ $not: {} }),
1381
+ node: (0, import_data4.reduceFilterVerdict)({}),
1382
+ /** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
1383
+ orWithEmptyBranch: (0, import_data4.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
1384
+ };
1385
+ function rows(verdict) {
1386
+ if (verdict === "true") return "matches EVERY row";
1387
+ if (verdict === "false") return "matches NO row";
1388
+ return "carries a real predicate";
1389
+ }
1390
+ 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.";
1391
+ 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).';
1392
+ function emitEmptyCombinator(key, path, ctx) {
1393
+ const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
1394
+ 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.`;
1395
+ 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}`;
1396
+ ctx.out.push({
1397
+ severity: "error",
1398
+ rule: FILTER_EMPTY_COMBINATOR,
1399
+ where: ctx.where,
1400
+ path,
1401
+ message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
1402
+ 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.`
1403
+ });
1404
+ }
1405
+ function emitEmptyNode(position, path, ctx) {
1406
+ if (position === "root") {
1407
+ ctx.out.push({
1408
+ severity: "error",
1409
+ rule: FILTER_EMPTY_NODE,
1410
+ where: ctx.where,
1411
+ path,
1412
+ 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.`,
1413
+ hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
1414
+ });
1246
1415
  return;
1247
1416
  }
1248
- for (const [k, v] of Object.entries(node)) {
1249
- const childPath = `${path}.${k}`;
1250
- if (FILTER_KEYS.has(k)) {
1251
- walkFilterValues(v, childPath, where, out, /* @__PURE__ */ new Set());
1417
+ if (position === "or-branch") {
1418
+ ctx.out.push({
1419
+ severity: "error",
1420
+ rule: FILTER_EMPTY_NODE,
1421
+ where: ctx.where,
1422
+ path,
1423
+ 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.`,
1424
+ 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.)"
1425
+ });
1426
+ return;
1427
+ }
1428
+ ctx.out.push({
1429
+ severity: "error",
1430
+ rule: FILTER_EMPTY_NODE,
1431
+ where: ctx.where,
1432
+ path,
1433
+ 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.",
1434
+ 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."
1435
+ });
1436
+ }
1437
+ function scanNodeKeys(node, path, ctx) {
1438
+ for (const [key, value] of Object.entries(node)) {
1439
+ if (key === "$and" || key === "$or") {
1440
+ if (!Array.isArray(value)) continue;
1441
+ if (value.length === 0) {
1442
+ emitEmptyCombinator(key, `${path}.${key}`, ctx);
1443
+ continue;
1444
+ }
1445
+ value.forEach((element, index) => {
1446
+ scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
1447
+ });
1448
+ continue;
1449
+ }
1450
+ if (key === "$not") {
1451
+ if (!isFilterNode(value)) continue;
1452
+ if (Object.keys(value).length === 0) {
1453
+ emitEmptyCombinator("$not", `${path}.$not`, ctx);
1454
+ continue;
1455
+ }
1456
+ scanNodeKeys(value, `${path}.$not`, ctx);
1252
1457
  continue;
1253
1458
  }
1254
- scanForFilters(v, childPath, where, out, seen);
1255
1459
  }
1256
1460
  }
1257
- function validateFilterTokens(stack) {
1461
+ function scanBranch(value, path, position, ctx) {
1462
+ if (!isFilterNode(value)) return;
1463
+ if (Object.keys(value).length === 0) {
1464
+ emitEmptyNode(position, path, ctx);
1465
+ return;
1466
+ }
1467
+ scanNodeKeys(value, path, ctx);
1468
+ }
1469
+ function validateEmptyCombinators(stack) {
1258
1470
  if (!stack || typeof stack !== "object") return [];
1259
1471
  const out = [];
1260
- const surfaces = [
1261
- ["dashboards", "dashboard"],
1262
- ["objects", "object"],
1263
- ["views", "view"],
1264
- ["reports", "report"],
1265
- ["datasets", "dataset"],
1266
- ["pages", "page"],
1267
- ["apps", "app"]
1268
- ];
1269
- for (const [key, kind] of surfaces) {
1270
- const items = asArray5(stack[key]);
1271
- items.forEach((item, i) => {
1272
- const name = label(item.name ?? item.id, `#${i}`);
1273
- if (kind === "dashboard") {
1274
- const widgets = Array.isArray(item.widgets) ? item.widgets : [];
1275
- widgets.forEach((w, wi) => {
1276
- const wName = label(w.id ?? w.title, `#${wi}`);
1277
- scanForFilters(
1278
- w,
1279
- `${key}[${i}].widgets[${wi}]`,
1280
- `dashboard "${name}" \xB7 widget "${wName}"`,
1281
- out,
1282
- /* @__PURE__ */ new Set()
1283
- );
1284
- });
1285
- const { widgets: _skip, ...rest } = item;
1286
- scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
1287
- return;
1288
- }
1289
- scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
1290
- });
1291
- }
1472
+ walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
1473
+ scanBranch(value, path, "root", { where, out });
1474
+ });
1292
1475
  return out;
1293
1476
  }
1294
1477
 
@@ -1482,7 +1665,7 @@ function validateObjectReferences(stack) {
1482
1665
  }
1483
1666
 
1484
1667
  // src/validate-searchable-fields.ts
1485
- var import_data4 = require("@objectstack/spec/data");
1668
+ var import_data5 = require("@objectstack/spec/data");
1486
1669
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1487
1670
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1488
1671
  function asArray7(v) {
@@ -1530,7 +1713,7 @@ function resolveAllowedSet(target) {
1530
1713
  fields = { ...fields };
1531
1714
  for (const f of systemDeclared) fields[f] = {};
1532
1715
  }
1533
- const { allowed, source } = (0, import_data4.resolveSearchFieldResolution)({
1716
+ const { allowed, source } = (0, import_data5.resolveSearchFieldResolution)({
1534
1717
  fields,
1535
1718
  searchableFields: target.searchableFields,
1536
1719
  displayField: target.displayField
@@ -1596,7 +1779,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1596
1779
  where,
1597
1780
  path: `${path}[${i}]`,
1598
1781
  message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest3(name, known)),
1599
- hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a formula field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
1782
+ 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(", ")}.` : "")
1783
+ });
1784
+ continue;
1785
+ }
1786
+ if ((0, import_data5.isVirtualSearchField)(target.fields[name])) {
1787
+ const vtype = target.fields[name]?.type;
1788
+ findings.push({
1789
+ severity: "error",
1790
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1791
+ where,
1792
+ path: `${path}[${i}]`,
1793
+ 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).`,
1794
+ hint: `Mirror the computed value onto a stored text field on "${objectName}" and declare that instead, or drop "${name}". At runtime the ingress gate now refuses this entry with 400 INVALID_FIELD, the same answer a stale entry gets (#4254).`
1600
1795
  });
1601
1796
  continue;
1602
1797
  }
@@ -1616,7 +1811,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1616
1811
  }
1617
1812
  const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
1618
1813
  let why;
1619
- if (import_data4.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
1814
+ if (import_data5.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
1620
1815
  why = "a system/audit column, which the auto-default set never includes";
1621
1816
  } else if (meta?.hidden) {
1622
1817
  why = "hidden";
@@ -1630,8 +1825,8 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1630
1825
  rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1631
1826
  where,
1632
1827
  path: `${path}[${i}]`,
1633
- message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data4.SEARCHABLE_TEXTUAL_TYPES, ...import_data4.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
1634
- hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text/formula field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1828
+ message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data5.SEARCHABLE_TEXTUAL_TYPES, ...import_data5.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).`,
1829
+ hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
1635
1830
  });
1636
1831
  }
1637
1832
  return findings;
@@ -1950,6 +2145,15 @@ function validateActionNameRefs(stack) {
1950
2145
  "Navigation action item"
1951
2146
  );
1952
2147
  }
2148
+ const runAction = strName5(nav.runAction);
2149
+ if (nav.type === "object" && runAction) {
2150
+ check(
2151
+ runAction,
2152
+ `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2153
+ `${navPath}.runAction`,
2154
+ "Navigation deep-link auto-run"
2155
+ );
2156
+ }
1953
2157
  if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
1954
2158
  }
1955
2159
  };
@@ -2015,8 +2219,21 @@ var COMPONENT_FIELD_SPECS = {
2015
2219
  "element:number": { props: ["field"] },
2016
2220
  "element:filter": { props: ["fields"] },
2017
2221
  "element:form": { props: ["fields"] },
2018
- // The schema says `displayField`; real pages author `labelField`. Accept both.
2019
- "element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
2222
+ // `labelField` is the one field-bearing prop this element declares. Its former
2223
+ // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
2224
+ // `searchFields` (deleted, ADR-0049) were retired in #5775 and are
2225
+ // `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
2226
+ // spec-conformant page carries either, and this rule's job (resolve a field
2227
+ // NAME against the object) is not the question a retired key raises (#6629).
2228
+ //
2229
+ // A non-conformant page that writes one anyway is not left unattended: the
2230
+ // #5068 props gate reports the key with its rename/delete prescription. That
2231
+ // gate is advisory and CLI-only and lives in a different registry
2232
+ // (`authoring-rules`) from this suite, so it neither precedes nor suppresses
2233
+ // this rule — what these two entries actually added was a SECOND finding,
2234
+ // saying a field named by a key that no longer exists does not exist either.
2235
+ // The prescription is the useful half; this half was noise on top of it.
2236
+ "element:record_picker": { props: ["labelField"] }
2020
2237
  };
2021
2238
  var RELATED_LIST_TYPE = "record:related_list";
2022
2239
  function componentFieldRefs(type, props, basePath, sep = ".") {
@@ -2585,18 +2802,57 @@ function validateNavTargetRefs(stack) {
2585
2802
  }
2586
2803
 
2587
2804
  // src/validate-translation-references.ts
2805
+ var import_spec = require("@objectstack/spec");
2588
2806
  var import_system4 = require("@objectstack/spec/system");
2807
+
2808
+ // src/view-walk.ts
2809
+ function isRec7(v) {
2810
+ return !!v && typeof v === "object" && !Array.isArray(v);
2811
+ }
2812
+ function strName10(v) {
2813
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2814
+ }
2815
+ function viewObjectName(view) {
2816
+ return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
2817
+ }
2818
+ function viewContainerSites(view, basePath) {
2819
+ if (!isRec7(view)) return [];
2820
+ const sites = [{ view, path: basePath, surface: "", kind: "self" }];
2821
+ if (isRec7(view.form)) {
2822
+ sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
2823
+ }
2824
+ for (const key of ["listViews", "formViews"]) {
2825
+ const container = view[key];
2826
+ if (!isRec7(container)) continue;
2827
+ const kind = key === "listViews" ? "listView" : "formView";
2828
+ for (const [subKey, sub] of Object.entries(container)) {
2829
+ if (!isRec7(sub)) continue;
2830
+ sites.push({
2831
+ view: sub,
2832
+ path: `${basePath}.${key}.${subKey}`,
2833
+ surface: `${key}.${subKey}`,
2834
+ kind
2835
+ });
2836
+ }
2837
+ }
2838
+ return sites;
2839
+ }
2840
+ function formViewSites(view, basePath) {
2841
+ return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
2842
+ }
2843
+
2844
+ // src/validate-translation-references.ts
2589
2845
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
2590
2846
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
2591
- function isRec7(v) {
2847
+ function isRec8(v) {
2592
2848
  return !!v && typeof v === "object" && !Array.isArray(v);
2593
2849
  }
2594
2850
  function asArray14(v) {
2595
2851
  if (Array.isArray(v)) return v;
2596
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
2852
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec8(def) ? def : {} }));
2597
2853
  return [];
2598
2854
  }
2599
- function strName10(v) {
2855
+ function strName11(v) {
2600
2856
  return typeof v === "string" && v.length > 0 ? v : void 0;
2601
2857
  }
2602
2858
  function distance5(a, b) {
@@ -2657,29 +2913,51 @@ function collectViewRecord(view, factsFor) {
2657
2913
  const addSections = (container, binding) => {
2658
2914
  if (!binding) return;
2659
2915
  for (const section of asArray14(container.sections)) {
2660
- const sectionName = strName10(section.name);
2916
+ const sectionName = strName11(section.name);
2661
2917
  if (sectionName) factsFor(binding).sections.add(sectionName);
2662
2918
  }
2663
2919
  };
2664
- const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
2665
- if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
2666
- addView(recordObject ?? listBinding, strName10(view.name));
2667
- for (const key of ["listViews", "formViews"]) {
2668
- const container = view[key];
2669
- if (!isRec7(container)) continue;
2670
- for (const [subKey, sub] of Object.entries(container)) {
2671
- if (!isRec7(sub)) continue;
2920
+ const listBinding = isRec8(view.list) ? bindingOf(view.list) : void 0;
2921
+ if (isRec8(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
2922
+ addView(recordObject ?? listBinding, strName11(view.name));
2923
+ const named = namedViewKeys(view);
2924
+ for (const family of ["listViews", "formViews"]) {
2925
+ const container = view[family];
2926
+ if (!isRec8(container)) continue;
2927
+ const registryKeys = family === "listViews" ? named.list : named.form;
2928
+ let at = 0;
2929
+ for (const sub of Object.values(container)) {
2930
+ if (!sub || typeof sub !== "object") continue;
2931
+ const registryKey = registryKeys[at++];
2932
+ if (!isRec8(sub)) continue;
2672
2933
  const binding = bindingOf(sub) ?? listBinding;
2673
- addView(binding, subKey);
2674
- addView(binding, strName10(sub.name));
2934
+ addView(binding, registryKey);
2675
2935
  addSections(sub, binding);
2676
2936
  }
2677
2937
  }
2678
- if (isRec7(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
2938
+ if (isRec8(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
2679
2939
  addSections(view, recordObject ?? listBinding);
2680
2940
  }
2681
- function viewObjectName(view) {
2682
- return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
2941
+ function defaultListViewKey(object, container) {
2942
+ if (!object || !isRec8(container.list)) return void 0;
2943
+ const item = (0, import_spec.expandViewContainer)(object, container).find(
2944
+ (i) => i.viewKind === "list" && i.isDefault
2945
+ );
2946
+ if (!item) return void 0;
2947
+ const prefix = `${object}.`;
2948
+ return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
2949
+ }
2950
+ function namedViewKeys(container) {
2951
+ const object = "probe";
2952
+ const prefix = `${object}.`;
2953
+ const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
2954
+ const countEntries = (v) => isRec8(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
2955
+ const listCount = countEntries(container.listViews);
2956
+ const formCount = countEntries(container.formViews);
2957
+ if (!listCount && !formCount) return { list: [], form: [] };
2958
+ const items = (0, import_spec.expandViewContainer)(object, container);
2959
+ const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
2960
+ return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
2683
2961
  }
2684
2962
  function readOptions(field) {
2685
2963
  const raw = field.options;
@@ -2691,14 +2969,14 @@ function readOptions(field) {
2691
2969
  values.add(opt);
2692
2970
  continue;
2693
2971
  }
2694
- if (!isRec7(opt)) continue;
2695
- const value = strName10(opt.value);
2972
+ if (!isRec8(opt)) continue;
2973
+ const value = strName11(opt.value);
2696
2974
  if (!value) continue;
2697
2975
  values.add(value);
2698
- const label2 = strName10(opt.label);
2976
+ const label2 = strName11(opt.label);
2699
2977
  if (label2) byLabel.set(label2.toLowerCase(), value);
2700
2978
  }
2701
- } else if (isRec7(raw)) {
2979
+ } else if (isRec8(raw)) {
2702
2980
  for (const [value, label2] of Object.entries(raw)) {
2703
2981
  values.add(value);
2704
2982
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -2719,23 +2997,23 @@ function buildUniverse(stack) {
2719
2997
  return facts;
2720
2998
  };
2721
2999
  for (const obj of asArray14(stack.objects)) {
2722
- const objectName = strName10(obj.name);
3000
+ const objectName = strName11(obj.name);
2723
3001
  if (!objectName) continue;
2724
3002
  const facts = factsFor(objectName);
2725
3003
  for (const field of asArray14(obj.fields)) {
2726
- const fieldName = strName10(field.name);
3004
+ const fieldName = strName11(field.name);
2727
3005
  if (fieldName) facts.fields.set(fieldName, field);
2728
3006
  }
2729
3007
  for (const action of asArray14(obj.actions)) {
2730
- const actionName = strName10(action.name);
3008
+ const actionName = strName11(action.name);
2731
3009
  if (actionName) facts.actions.set(actionName, action);
2732
3010
  }
2733
3011
  for (const view of asArray14(obj.views)) {
2734
- collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
3012
+ collectViewRecord({ ...view, object: strName11(view.object) ?? objectName }, factsFor);
2735
3013
  }
2736
3014
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
2737
3015
  for (const group of asArray14(obj.fieldGroups)) {
2738
- const key = strName10(group.key) ?? strName10(group.name);
3016
+ const key = strName11(group.key) ?? strName11(group.name);
2739
3017
  if (key) facts.sections.add(key);
2740
3018
  }
2741
3019
  }
@@ -2746,10 +3024,10 @@ function buildUniverse(stack) {
2746
3024
  for (let pi = 0; pi < pages.length; pi++) {
2747
3025
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
2748
3026
  if (!walked.objectName) continue;
2749
- const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
3027
+ const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
2750
3028
  if (!props) continue;
2751
3029
  for (const section of asArray14(props.sections)) {
2752
- const sectionName = strName10(section.name);
3030
+ const sectionName = strName11(section.name);
2753
3031
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
2754
3032
  }
2755
3033
  }
@@ -2757,9 +3035,9 @@ function buildUniverse(stack) {
2757
3035
  const globalActions = /* @__PURE__ */ new Map();
2758
3036
  const actionOwners = /* @__PURE__ */ new Map();
2759
3037
  for (const action of asArray14(stack.actions)) {
2760
- const actionName = strName10(action.name);
3038
+ const actionName = strName11(action.name);
2761
3039
  if (!actionName) continue;
2762
- const owner = strName10(action.objectName) ?? strName10(action.object);
3040
+ const owner = strName11(action.objectName) ?? strName11(action.object);
2763
3041
  if (owner) {
2764
3042
  factsFor(owner).actions.set(actionName, action);
2765
3043
  actionOwners.set(actionName, owner);
@@ -2774,19 +3052,19 @@ function buildUniverse(stack) {
2774
3052
  }
2775
3053
  const apps = /* @__PURE__ */ new Map();
2776
3054
  for (const app of asArray14(stack.apps)) {
2777
- const appName = strName10(app.name);
3055
+ const appName = strName11(app.name);
2778
3056
  if (!appName) continue;
2779
3057
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
2780
3058
  const walkNav = (items) => {
2781
3059
  for (const item of asArray14(items)) {
2782
- const id = strName10(item.id);
3060
+ const id = strName11(item.id);
2783
3061
  if (id) navIds.add(id);
2784
3062
  if (item.children) walkNav(item.children);
2785
3063
  }
2786
3064
  };
2787
3065
  walkNav(app.navigation);
2788
3066
  for (const area of asArray14(app.areas)) {
2789
- const areaId = strName10(area.id);
3067
+ const areaId = strName11(area.id);
2790
3068
  if (areaId) navIds.add(areaId);
2791
3069
  walkNav(area.navigation);
2792
3070
  }
@@ -2794,20 +3072,20 @@ function buildUniverse(stack) {
2794
3072
  }
2795
3073
  const dashboards = /* @__PURE__ */ new Map();
2796
3074
  for (const dash of asArray14(stack.dashboards)) {
2797
- const dashName = strName10(dash.name);
3075
+ const dashName = strName11(dash.name);
2798
3076
  if (!dashName) continue;
2799
3077
  const widgets = /* @__PURE__ */ new Set();
2800
3078
  for (const widget of asArray14(dash.widgets)) {
2801
- const id = strName10(widget.id) ?? strName10(widget.name);
3079
+ const id = strName11(widget.id) ?? strName11(widget.name);
2802
3080
  if (id) widgets.add(id);
2803
3081
  }
2804
3082
  const actions = /* @__PURE__ */ new Set();
2805
3083
  const headerActions = [
2806
- ...asArray14(isRec7(dash.header) ? dash.header.actions : void 0),
3084
+ ...asArray14(isRec8(dash.header) ? dash.header.actions : void 0),
2807
3085
  ...asArray14(dash.actions)
2808
3086
  ];
2809
3087
  for (const action of headerActions) {
2810
- const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
3088
+ const key = strName11(action.actionUrl) ?? strName11(action.url) ?? strName11(action.name);
2811
3089
  if (key) actions.add(key);
2812
3090
  }
2813
3091
  dashboards.set(dashName, { widgets, actions });
@@ -2819,7 +3097,7 @@ function localePath(bundleIndex, locale) {
2819
3097
  }
2820
3098
  function validateTranslationReferences(stack) {
2821
3099
  const findings = [];
2822
- if (!isRec7(stack)) return findings;
3100
+ if (!isRec8(stack)) return findings;
2823
3101
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
2824
3102
  if (bundles.length === 0) return findings;
2825
3103
  const universe = buildUniverse(stack);
@@ -2828,13 +3106,13 @@ function validateTranslationReferences(stack) {
2828
3106
  };
2829
3107
  for (let bi = 0; bi < bundles.length; bi++) {
2830
3108
  const bundle = bundles[bi];
2831
- if (!isRec7(bundle)) continue;
3109
+ if (!isRec8(bundle)) continue;
2832
3110
  for (const [locale, rawData] of Object.entries(bundle)) {
2833
- if (!isRec7(rawData)) continue;
3111
+ if (!isRec8(rawData)) continue;
2834
3112
  const base = localePath(bi, locale);
2835
3113
  const inLocale = `locale "${locale}"`;
2836
3114
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
2837
- if (!isRec7(rawNode)) continue;
3115
+ if (!isRec8(rawNode)) continue;
2838
3116
  const objPath = `${base}.objects.${objectName}`;
2839
3117
  const facts = universe.objects.get(objectName);
2840
3118
  if (!facts) {
@@ -2860,7 +3138,7 @@ function validateTranslationReferences(stack) {
2860
3138
  );
2861
3139
  continue;
2862
3140
  }
2863
- if (!isRec7(rawField)) continue;
3141
+ if (!isRec8(rawField)) continue;
2864
3142
  checkOptionKeys(findings, {
2865
3143
  optionMap: rawField.options,
2866
3144
  field,
@@ -2942,7 +3220,7 @@ function validateTranslationReferences(stack) {
2942
3220
  );
2943
3221
  continue;
2944
3222
  }
2945
- if (!isRec7(rawApp)) continue;
3223
+ if (!isRec8(rawApp)) continue;
2946
3224
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
2947
3225
  if (navIds.has(navId)) continue;
2948
3226
  orphan(
@@ -2965,7 +3243,7 @@ function validateTranslationReferences(stack) {
2965
3243
  );
2966
3244
  continue;
2967
3245
  }
2968
- if (!isRec7(rawDash)) continue;
3246
+ if (!isRec8(rawDash)) continue;
2969
3247
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
2970
3248
  if (dash.widgets.has(widgetId)) continue;
2971
3249
  orphan(
@@ -2990,7 +3268,7 @@ function validateTranslationReferences(stack) {
2990
3268
  return findings;
2991
3269
  }
2992
3270
  function asRecord(v) {
2993
- return isRec7(v) ? v : {};
3271
+ return isRec8(v) ? v : {};
2994
3272
  }
2995
3273
  function checkOptionKeys(findings, ctx) {
2996
3274
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -3002,7 +3280,7 @@ function checkOptionKeys(findings, ctx) {
3002
3280
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3003
3281
  where: ctx.where,
3004
3282
  path: ctx.path,
3005
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName10(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
3283
+ 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.`,
3006
3284
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
3007
3285
  });
3008
3286
  return;
@@ -3021,11 +3299,11 @@ function checkOptionKeys(findings, ctx) {
3021
3299
  }
3022
3300
  }
3023
3301
  function checkActionParams(findings, ctx) {
3024
- const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
3302
+ const rawParams = Object.keys(asRecord(isRec8(ctx.rawAction) ? ctx.rawAction.params : void 0));
3025
3303
  if (rawParams.length === 0) return;
3026
3304
  const declared = /* @__PURE__ */ new Set();
3027
3305
  for (const param of asArray14(ctx.action.params)) {
3028
- const name = strName10(param.name) ?? strName10(param.field);
3306
+ const name = strName11(param.name) ?? strName11(param.field);
3029
3307
  if (name) declared.add(name);
3030
3308
  }
3031
3309
  for (const paramName of rawParams) {
@@ -3041,78 +3319,60 @@ function checkActionParams(findings, ctx) {
3041
3319
  }
3042
3320
  }
3043
3321
 
3044
- // src/validate-translatable-sections.ts
3045
- var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3046
- function isRec8(v) {
3322
+ // src/collection-entries.ts
3323
+ function isRec9(v) {
3047
3324
  return !!v && typeof v === "object" && !Array.isArray(v);
3048
3325
  }
3049
- function strName11(v) {
3050
- return typeof v === "string" && v.length > 0 ? v : void 0;
3051
- }
3052
- function viewObjectName2(view) {
3053
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
3054
- }
3055
3326
  function collectionEntries(v, base) {
3056
3327
  if (Array.isArray(v)) {
3057
3328
  const out = [];
3058
3329
  for (let i = 0; i < v.length; i++) {
3059
- if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3330
+ if (isRec9(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3060
3331
  }
3061
3332
  return out;
3062
3333
  }
3063
- if (isRec8(v)) {
3064
- return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3334
+ if (isRec9(v)) {
3335
+ return Object.entries(v).filter(([, def]) => isRec9(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3065
3336
  }
3066
3337
  return [];
3067
3338
  }
3339
+
3340
+ // src/validate-translatable-sections.ts
3341
+ var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3342
+ function isRec10(v) {
3343
+ return !!v && typeof v === "object" && !Array.isArray(v);
3344
+ }
3345
+ function strName12(v) {
3346
+ return typeof v === "string" && v.length > 0 ? v : void 0;
3347
+ }
3068
3348
  function viewLabel(view) {
3069
- const name = strName11(view.name);
3349
+ const name = strName12(view.name);
3070
3350
  return name ? `view "${name}"` : "";
3071
3351
  }
3072
3352
  function joinWhere(...parts) {
3073
3353
  return parts.filter((p) => p.length > 0).join(" \xB7 ");
3074
3354
  }
3075
3355
  function collectViewSites(view, basePath, label2, sites) {
3076
- const recordObject = viewObjectName2(view);
3077
- const listBinding = isRec8(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
3078
- const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
3079
- sites.push({
3080
- path: `${basePath}.sections`,
3081
- surface: label2,
3082
- objectName: recordObject ?? listBinding,
3083
- sections: view.sections
3084
- });
3085
- if (isRec8(view.form)) {
3356
+ const recordObject = viewObjectName(view);
3357
+ const listBinding = isRec10(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3358
+ for (const site of viewContainerSites(view, basePath)) {
3086
3359
  sites.push({
3087
- path: `${basePath}.form.sections`,
3088
- surface: joinWhere(label2, "form"),
3089
- objectName: bindingOf(view.form) ?? listBinding,
3090
- sections: view.form.sections
3360
+ path: `${site.path}.sections`,
3361
+ surface: joinWhere(label2, site.surface),
3362
+ objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
3363
+ sections: site.view.sections
3091
3364
  });
3092
3365
  }
3093
- for (const key of ["listViews", "formViews"]) {
3094
- const container = view[key];
3095
- if (!isRec8(container)) continue;
3096
- for (const [subKey, sub] of Object.entries(container)) {
3097
- if (!isRec8(sub)) continue;
3098
- sites.push({
3099
- path: `${basePath}.${key}.${subKey}.sections`,
3100
- surface: joinWhere(label2, `${key}.${subKey}`),
3101
- objectName: bindingOf(sub) ?? listBinding,
3102
- sections: sub.sections
3103
- });
3104
- }
3105
- }
3106
3366
  }
3107
3367
  function translatedObjectNames(stack) {
3108
3368
  const out = /* @__PURE__ */ new Set();
3109
3369
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3110
3370
  for (const bundle of bundles) {
3111
- if (!isRec8(bundle)) continue;
3371
+ if (!isRec10(bundle)) continue;
3112
3372
  for (const data of Object.values(bundle)) {
3113
- if (!isRec8(data) || !isRec8(data.objects)) continue;
3373
+ if (!isRec10(data) || !isRec10(data.objects)) continue;
3114
3374
  for (const [objectName, node] of Object.entries(data.objects)) {
3115
- if (isRec8(node)) out.add(objectName);
3375
+ if (isRec10(node)) out.add(objectName);
3116
3376
  }
3117
3377
  }
3118
3378
  }
@@ -3124,22 +3384,22 @@ function suggestedName(label2) {
3124
3384
  }
3125
3385
  function validateTranslatableSections(stack) {
3126
3386
  const findings = [];
3127
- if (!isRec8(stack)) return findings;
3387
+ if (!isRec10(stack)) return findings;
3128
3388
  const translated = translatedObjectNames(stack);
3129
3389
  if (translated.size === 0) return findings;
3130
3390
  const sites = [];
3131
3391
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3132
- const objectName = strName11(obj.name);
3392
+ const objectName = strName12(obj.name);
3133
3393
  if (!objectName) continue;
3134
3394
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3135
3395
  collectViewSites(
3136
- { ...view, object: strName11(view.object) ?? objectName },
3396
+ { ...view, object: strName12(view.object) ?? objectName },
3137
3397
  path,
3138
3398
  viewLabel(view),
3139
3399
  sites
3140
3400
  );
3141
3401
  }
3142
- if (isRec8(obj.listViews)) {
3402
+ if (isRec10(obj.listViews)) {
3143
3403
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3144
3404
  }
3145
3405
  }
@@ -3147,13 +3407,13 @@ function validateTranslatableSections(stack) {
3147
3407
  collectViewSites(view, path, viewLabel(view), sites);
3148
3408
  }
3149
3409
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3150
- const pageName = strName11(page.name);
3410
+ const pageName = strName12(page.name);
3151
3411
  const pageLabel = pageName ? `page "${pageName}"` : "";
3152
3412
  for (const walked of walkPageComponents(page, pagePath)) {
3153
3413
  if (!walked.objectName) continue;
3154
- const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
3414
+ const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3155
3415
  if (!props) continue;
3156
- const type = strName11(walked.component.type) ?? "component";
3416
+ const type = strName12(walked.component.type) ?? "component";
3157
3417
  sites.push({
3158
3418
  path: `${walked.path}.properties.sections`,
3159
3419
  surface: joinWhere(pageLabel, type),
@@ -3168,9 +3428,9 @@ function validateTranslatableSections(stack) {
3168
3428
  if (!Array.isArray(site.sections)) continue;
3169
3429
  for (let i = 0; i < site.sections.length; i++) {
3170
3430
  const section = site.sections[i];
3171
- if (!isRec8(section)) continue;
3172
- if (strName11(section.name)) continue;
3173
- const heading = strName11(section.label) ?? strName11(section.title);
3431
+ if (!isRec10(section)) continue;
3432
+ if (strName12(section.name)) continue;
3433
+ const heading = strName12(section.label);
3174
3434
  if (!heading) continue;
3175
3435
  const slug = suggestedName(heading);
3176
3436
  findings.push({
@@ -3188,10 +3448,10 @@ function validateTranslatableSections(stack) {
3188
3448
 
3189
3449
  // src/flow-walk.ts
3190
3450
  var import_automation2 = require("@objectstack/spec/automation");
3191
- function isRec9(v) {
3451
+ function isRec11(v) {
3192
3452
  return !!v && typeof v === "object" && !Array.isArray(v);
3193
3453
  }
3194
- function strName12(v) {
3454
+ function strName13(v) {
3195
3455
  return typeof v === "string" && v.length > 0 ? v : void 0;
3196
3456
  }
3197
3457
  var REGION_SLOTS = new Map(
@@ -3200,10 +3460,10 @@ var REGION_SLOTS = new Map(
3200
3460
  var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
3201
3461
  var MAX_REGION_DEPTH = 16;
3202
3462
  function flowNodeLabel(node, index) {
3203
- return strName12(node.label) ?? strName12(node.id) ?? `#${index}`;
3463
+ return strName13(node.label) ?? strName13(node.id) ?? `#${index}`;
3204
3464
  }
3205
3465
  function stripRegions(config) {
3206
- if (!isRec9(config)) return void 0;
3466
+ if (!isRec11(config)) return void 0;
3207
3467
  let out;
3208
3468
  for (const key of Object.keys(config)) {
3209
3469
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3214,11 +3474,11 @@ function stripRegions(config) {
3214
3474
  }
3215
3475
  function walkFlowNodes(flow, flowPath) {
3216
3476
  const out = [];
3217
- if (!isRec9(flow)) return out;
3477
+ if (!isRec11(flow)) return out;
3218
3478
  const visitList = (nodes, basePath, trail, depth) => {
3219
3479
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3220
3480
  nodes.forEach((raw, index) => {
3221
- if (!isRec9(raw)) return;
3481
+ if (!isRec11(raw)) return;
3222
3482
  const path = `${basePath}[${index}]`;
3223
3483
  out.push({
3224
3484
  node: raw,
@@ -3227,9 +3487,9 @@ function walkFlowNodes(flow, flowPath) {
3227
3487
  regionTrail: trail,
3228
3488
  depth
3229
3489
  });
3230
- const type = strName12(raw.type);
3490
+ const type = strName13(raw.type);
3231
3491
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3232
- if (!slots || !isRec9(raw.config)) return;
3492
+ if (!slots || !isRec11(raw.config)) return;
3233
3493
  const config = raw.config;
3234
3494
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3235
3495
  for (const slot of slots) {
@@ -3237,8 +3497,8 @@ function walkFlowNodes(flow, flowPath) {
3237
3497
  if (slot === "branches") {
3238
3498
  if (!Array.isArray(value)) continue;
3239
3499
  value.forEach((branch, b) => {
3240
- if (!isRec9(branch)) return;
3241
- const branchName = strName12(branch.name) ?? `#${b}`;
3500
+ if (!isRec11(branch)) return;
3501
+ const branchName = strName13(branch.name) ?? `#${b}`;
3242
3502
  visitList(
3243
3503
  branch.nodes,
3244
3504
  `${path}.config.branches[${b}].nodes`,
@@ -3248,7 +3508,7 @@ function walkFlowNodes(flow, flowPath) {
3248
3508
  });
3249
3509
  continue;
3250
3510
  }
3251
- if (!isRec9(value)) continue;
3511
+ if (!isRec11(value)) continue;
3252
3512
  visitList(
3253
3513
  value.nodes,
3254
3514
  `${path}.config.${slot}.nodes`,
@@ -3473,7 +3733,7 @@ function asArray16(v) {
3473
3733
  }
3474
3734
  return [];
3475
3735
  }
3476
- function strName13(v) {
3736
+ function strName14(v) {
3477
3737
  return typeof v === "string" && v.length > 0 ? v : void 0;
3478
3738
  }
3479
3739
  function surfaceOf(v) {
@@ -3484,17 +3744,17 @@ function validateAiSurfaceAffinity(stack) {
3484
3744
  if (!stack || typeof stack !== "object") return findings;
3485
3745
  const skillsByName = /* @__PURE__ */ new Map();
3486
3746
  for (const skill of asArray16(stack.skills)) {
3487
- const n = strName13(skill.name);
3747
+ const n = strName14(skill.name);
3488
3748
  if (n) skillsByName.set(n, skill);
3489
3749
  }
3490
3750
  const agents = asArray16(stack.agents);
3491
3751
  for (let ai = 0; ai < agents.length; ai++) {
3492
3752
  const agent = agents[ai];
3493
- const agentName = strName13(agent.name) ?? `#${ai}`;
3753
+ const agentName = strName14(agent.name) ?? `#${ai}`;
3494
3754
  const agentSurface = surfaceOf(agent.surface);
3495
3755
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
3496
3756
  for (let si = 0; si < skillRefs.length; si++) {
3497
- const ref = strName13(skillRefs[si]);
3757
+ const ref = strName14(skillRefs[si]);
3498
3758
  if (!ref) continue;
3499
3759
  const skill = skillsByName.get(ref);
3500
3760
  if (!skill) continue;
@@ -3523,7 +3783,7 @@ function asArray17(v) {
3523
3783
  }
3524
3784
  return [];
3525
3785
  }
3526
- function strName14(v) {
3786
+ function strName15(v) {
3527
3787
  return typeof v === "string" && v.length > 0 ? v : void 0;
3528
3788
  }
3529
3789
  function distance6(a, b) {
@@ -3564,8 +3824,8 @@ function materialisesAsTool(action) {
3564
3824
  if (!ai || typeof ai !== "object") return false;
3565
3825
  const aiRec = ai;
3566
3826
  if (aiRec.exposed !== true) return false;
3567
- if (!strName14(aiRec.description)) return false;
3568
- const type = strName14(action.type);
3827
+ if (!strName15(aiRec.description)) return false;
3828
+ const type = strName15(action.type);
3569
3829
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
3570
3830
  if (type === "script") return Boolean(action.target || action.body);
3571
3831
  return Boolean(action.target);
@@ -3573,12 +3833,12 @@ function materialisesAsTool(action) {
3573
3833
  function collectToolUniverse(stack) {
3574
3834
  const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
3575
3835
  for (const tool of asArray17(stack.tools)) {
3576
- const n = strName14(tool.name);
3836
+ const n = strName15(tool.name);
3577
3837
  if (n) universe.add(n);
3578
3838
  }
3579
3839
  const addActionFamily = (actions) => {
3580
3840
  for (const action of asArray17(actions)) {
3581
- const n = strName14(action.name);
3841
+ const n = strName15(action.name);
3582
3842
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
3583
3843
  }
3584
3844
  };
@@ -3592,7 +3852,7 @@ function collectUnexposedActionNames(stack) {
3592
3852
  const names = /* @__PURE__ */ new Set();
3593
3853
  const scan = (actions) => {
3594
3854
  for (const action of asArray17(actions)) {
3595
- const n = strName14(action.name);
3855
+ const n = strName15(action.name);
3596
3856
  if (n && !materialisesAsTool(action)) names.add(n);
3597
3857
  }
3598
3858
  };
@@ -3618,10 +3878,10 @@ function validateAiToolReferences(stack) {
3618
3878
  const skills = asArray17(stack.skills);
3619
3879
  for (let si = 0; si < skills.length; si++) {
3620
3880
  const skill = skills[si];
3621
- const skillName = strName14(skill.name) ?? `#${si}`;
3881
+ const skillName = strName15(skill.name) ?? `#${si}`;
3622
3882
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
3623
3883
  for (let ti = 0; ti < refs.length; ti++) {
3624
- const ref = strName14(refs[ti]);
3884
+ const ref = strName15(refs[ti]);
3625
3885
  if (!ref || resolves(ref)) continue;
3626
3886
  const isPattern = ref.endsWith("*");
3627
3887
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -3640,6 +3900,7 @@ function validateAiToolReferences(stack) {
3640
3900
 
3641
3901
  // src/validate-ai-agent-authoring.ts
3642
3902
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
3903
+ var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
3643
3904
  function asArray18(v) {
3644
3905
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
3645
3906
  if (v && typeof v === "object") {
@@ -3647,7 +3908,7 @@ function asArray18(v) {
3647
3908
  }
3648
3909
  return [];
3649
3910
  }
3650
- function strName15(v) {
3911
+ function strName16(v) {
3651
3912
  return typeof v === "string" && v.length > 0 ? v : void 0;
3652
3913
  }
3653
3914
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -3657,7 +3918,7 @@ function validateAiAgentAuthoring(stack) {
3657
3918
  const agents = asArray18(stack.agents);
3658
3919
  for (let ai = 0; ai < agents.length; ai++) {
3659
3920
  const agent = agents[ai];
3660
- const name = strName15(agent.name) ?? `#${ai}`;
3921
+ const name = strName16(agent.name) ?? `#${ai}`;
3661
3922
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
3662
3923
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
3663
3924
  findings.push({
@@ -3669,6 +3930,22 @@ function validateAiAgentAuthoring(stack) {
3669
3930
  hint: isPlatformName ? `Remove the declaration; the platform owns "${name}". Extend it with skills instead.` : `Delete the agent and express its capability as skills. Everything an agent carried that a skill does not is persona text: move the useful parts of \`instructions\` into the skills' own instructions.` + (skillCount > 0 ? ` The ${skillCount} skill${skillCount === 1 ? "" : "s"} this agent references already carry the capability \u2014 they attach to the platform agent by \`surface\` affinity, so nothing is lost by dropping the persona.` : ``)
3670
3931
  });
3671
3932
  }
3933
+ const roster = [...PLATFORM_AGENT_NAMES].join(", ");
3934
+ const apps = asArray18(stack.apps);
3935
+ for (let appIdx = 0; appIdx < apps.length; appIdx++) {
3936
+ const app = apps[appIdx];
3937
+ const defaultAgent = strName16(app.defaultAgent);
3938
+ if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
3939
+ const appName = strName16(app.name) ?? `#${appIdx}`;
3940
+ findings.push({
3941
+ severity: "warning",
3942
+ rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
3943
+ where: `app "${appName}".defaultAgent`,
3944
+ path: `apps[${appIdx}].defaultAgent`,
3945
+ 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.`,
3946
+ 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.`
3947
+ });
3948
+ }
3672
3949
  return findings;
3673
3950
  }
3674
3951
 
@@ -3756,13 +4033,13 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
3756
4033
  "owner",
3757
4034
  "record_type"
3758
4035
  ]);
3759
- var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4036
+ var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3760
4037
  function asArray19(v) {
3761
- if (Array.isArray(v)) return v.filter((x) => isRec10(x));
3762
- if (isRec10(v)) {
4038
+ if (Array.isArray(v)) return v.filter((x) => isRec12(x));
4039
+ if (isRec12(v)) {
3763
4040
  return Object.entries(v).map(([name, def]) => ({
3764
4041
  name,
3765
- ...isRec10(def) ? def : {}
4042
+ ...isRec12(def) ? def : {}
3766
4043
  }));
3767
4044
  }
3768
4045
  return [];
@@ -3908,7 +4185,7 @@ function validateHookBodyWrites(stack) {
3908
4185
  let objectFields = null;
3909
4186
  hooks.forEach((hook, hookIndex) => {
3910
4187
  const body = hook.body;
3911
- if (!isRec10(body) || body.language !== "js") return;
4188
+ if (!isRec12(body) || body.language !== "js") return;
3912
4189
  const source = body.source;
3913
4190
  if (typeof source !== "string" || source.trim() === "") return;
3914
4191
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -3979,13 +4256,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
3979
4256
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
3980
4257
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
3981
4258
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
3982
- var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4259
+ var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3983
4260
  function asArray20(v) {
3984
- if (Array.isArray(v)) return v.filter((x) => isRec11(x));
3985
- if (isRec11(v)) {
4261
+ if (Array.isArray(v)) return v.filter((x) => isRec13(x));
4262
+ if (isRec13(v)) {
3986
4263
  return Object.entries(v).map(([name, def]) => ({
3987
4264
  name,
3988
- ...isRec11(def) ? def : {}
4265
+ ...isRec13(def) ? def : {}
3989
4266
  }));
3990
4267
  }
3991
4268
  return [];
@@ -4003,7 +4280,7 @@ function collectActionBodies(stack) {
4003
4280
  const type = typeof action.type === "string" ? action.type : "script";
4004
4281
  if (type !== "script") return;
4005
4282
  const body = action.body;
4006
- if (!isRec11(body) || body.language !== "js") return;
4283
+ if (!isRec13(body) || body.language !== "js") return;
4007
4284
  const source = body.source;
4008
4285
  if (typeof source !== "string" || source.trim() === "") return;
4009
4286
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4022,7 +4299,7 @@ function collectActionBodies(stack) {
4022
4299
  }
4023
4300
  function validateActionBodyWrites(stack) {
4024
4301
  const findings = [];
4025
- if (!isRec11(stack)) return findings;
4302
+ if (!isRec13(stack)) return findings;
4026
4303
  const sites = collectActionBodies(stack);
4027
4304
  if (sites.length === 0) return findings;
4028
4305
  let objectFields = null;
@@ -4080,13 +4357,13 @@ function fixHint2(field, declared) {
4080
4357
  var import_shared3 = require("@objectstack/spec/shared");
4081
4358
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
4082
4359
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4083
- var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4360
+ var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4084
4361
  function asArray21(v) {
4085
- if (Array.isArray(v)) return v.filter((x) => isRec12(x));
4086
- if (isRec12(v)) {
4362
+ if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4363
+ if (isRec14(v)) {
4087
4364
  return Object.entries(v).map(([name, def]) => ({
4088
4365
  name,
4089
- ...isRec12(def) ? def : {}
4366
+ ...isRec14(def) ? def : {}
4090
4367
  }));
4091
4368
  }
4092
4369
  return [];
@@ -4099,7 +4376,7 @@ function readLiteralObjectName(config) {
4099
4376
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4100
4377
  function validateFlowNodeWrites(stack) {
4101
4378
  const findings = [];
4102
- if (!isRec12(stack)) return findings;
4379
+ if (!isRec14(stack)) return findings;
4103
4380
  const flows = asArray21(stack.flows);
4104
4381
  if (flows.length === 0) return findings;
4105
4382
  let objectFields = null;
@@ -4108,10 +4385,10 @@ function validateFlowNodeWrites(stack) {
4108
4385
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4109
4386
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4110
4387
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4111
- const config = isRec12(node.config) ? node.config : void 0;
4388
+ const config = isRec14(node.config) ? node.config : void 0;
4112
4389
  if (!config) return;
4113
4390
  const fields = config.fields;
4114
- if (!isRec12(fields)) return;
4391
+ if (!isRec14(fields)) return;
4115
4392
  const written = Object.keys(fields);
4116
4393
  if (written.length === 0) return;
4117
4394
  const objectName = readLiteralObjectName(config);
@@ -4236,14 +4513,14 @@ function validateReadonlyFlowWrites(stack) {
4236
4513
  // src/validate-react-page-props.ts
4237
4514
  var import_node_module2 = require("module");
4238
4515
  var import_ui2 = require("@objectstack/spec/ui");
4239
- var import_data5 = require("@objectstack/spec/data");
4516
+ var import_data6 = require("@objectstack/spec/data");
4240
4517
 
4241
4518
  // src/zod-issue-format.ts
4242
- var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4519
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4243
4520
  var valueAtPath = (root, path) => {
4244
4521
  let cur = root;
4245
4522
  for (const key of path) {
4246
- if (!isRec13(cur) && !Array.isArray(cur)) return void 0;
4523
+ if (!isRec15(cur) && !Array.isArray(cur)) return void 0;
4247
4524
  cur = cur[key];
4248
4525
  }
4249
4526
  return cur;
@@ -4382,7 +4659,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4382
4659
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4383
4660
  function checkChartDrillDown(raw, push2) {
4384
4661
  if (raw === void 0 || raw === NOT_STATIC) return;
4385
- if (!isRec14(raw)) {
4662
+ if (!isRec16(raw)) {
4386
4663
  push2(
4387
4664
  "error",
4388
4665
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4405,7 +4682,7 @@ function checkChartDrillDown(raw, push2) {
4405
4682
  }
4406
4683
  function checkChartAggregate(raw, push2) {
4407
4684
  if (raw === void 0 || raw === NOT_STATIC) return;
4408
- if (!isRec14(raw)) {
4685
+ if (!isRec16(raw)) {
4409
4686
  push2(
4410
4687
  "error",
4411
4688
  REACT_CHART_AGGREGATE_INVALID,
@@ -4420,7 +4697,7 @@ function checkChartAggregate(raw, push2) {
4420
4697
  "warning",
4421
4698
  REACT_CHART_AGGREGATE_INVALID,
4422
4699
  "aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
4423
- "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. Deliberate single-value charts are tolerated at warning level for now: ChartAggregateSchema declares groupBy required while ObjectChart honours its absence by falling back to xAxisKey \u2014 objectstack#5583 decides which of the two moves."
4700
+ "Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
4424
4701
  );
4425
4702
  }
4426
4703
  const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
@@ -4436,7 +4713,7 @@ function checkChartAggregate(raw, push2) {
4436
4713
  );
4437
4714
  }
4438
4715
  }
4439
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4716
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4440
4717
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4441
4718
  function checkObjectChart(attrs, objectFields, findings) {
4442
4719
  const { values, where, path } = attrs;
@@ -4446,11 +4723,11 @@ function checkObjectChart(attrs, objectFields, findings) {
4446
4723
  const aggregate = values.get("aggregate");
4447
4724
  checkChartAggregate(aggregate, push2);
4448
4725
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4449
- if (!isRec14(aggregate)) return;
4726
+ if (!isRec16(aggregate)) return;
4450
4727
  const fn = strOf(aggregate.function);
4451
4728
  const field = strOf(aggregate.field);
4452
4729
  const groupBy = aggregate.groupBy;
4453
- const groupByField = strOf(groupBy) ?? (isRec14(groupBy) ? strOf(groupBy.field) : void 0);
4730
+ const groupByField = strOf(groupBy) ?? (isRec16(groupBy) ? strOf(groupBy.field) : void 0);
4454
4731
  const objectName = strOf(values.get("objectName"));
4455
4732
  const known = objectName ? objectFields.get(objectName) : void 0;
4456
4733
  if (objectName && known) {
@@ -4483,18 +4760,18 @@ function checkObjectChart(attrs, objectFields, findings) {
4483
4760
  );
4484
4761
  };
4485
4762
  const xAxisRaw = values.get("xAxis");
4486
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec14(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
4763
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec16(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
4487
4764
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
4488
4765
  axisRef(categoryAxis, categoryProp);
4489
4766
  const yAxisRaw = values.get("yAxis");
4490
4767
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
4491
4768
  for (const a of yAxisList) {
4492
- axisRef(strOf(a) ?? (isRec14(a) ? strOf(a.field) : void 0), "yAxis[].field");
4769
+ axisRef(strOf(a) ?? (isRec16(a) ? strOf(a.field) : void 0), "yAxis[].field");
4493
4770
  }
4494
4771
  const series = values.get("series");
4495
4772
  if (Array.isArray(series)) {
4496
4773
  for (const s of series) {
4497
- if (!isRec14(s)) continue;
4774
+ if (!isRec16(s)) continue;
4498
4775
  const dataKey = strOf(s.dataKey);
4499
4776
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
4500
4777
  }
@@ -4550,7 +4827,7 @@ function subformFieldRefs(value, basePath) {
4550
4827
  if (!Array.isArray(value)) return { child, parent };
4551
4828
  for (let i = 0; i < value.length; i++) {
4552
4829
  const sub = value[i];
4553
- if (!isRec14(sub)) continue;
4830
+ if (!isRec16(sub)) continue;
4554
4831
  const at = (key) => `${basePath}[${i}].${key}`;
4555
4832
  child.push({
4556
4833
  objectName: strOf(sub.childObject),
@@ -4575,7 +4852,7 @@ function filterFieldRefs(node, basePath, out) {
4575
4852
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
4576
4853
  return;
4577
4854
  }
4578
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data5.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
4855
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data6.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
4579
4856
  out.push({ name: head, path: `${basePath}[0]` });
4580
4857
  }
4581
4858
  }
@@ -4595,20 +4872,20 @@ function reactFieldRefs(spec, values, basePath) {
4595
4872
  }
4596
4873
  for (const key of spec.nestedFields ?? []) {
4597
4874
  const v = readable(key);
4598
- if (isRec14(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
4875
+ if (isRec16(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
4599
4876
  }
4600
4877
  for (const key of spec.sections ?? []) {
4601
4878
  const v = readable(key);
4602
4879
  if (!Array.isArray(v)) continue;
4603
4880
  for (let i = 0; i < v.length; i++) {
4604
4881
  const section = v[i];
4605
- if (!isRec14(section)) continue;
4882
+ if (!isRec16(section)) continue;
4606
4883
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
4607
4884
  }
4608
4885
  }
4609
4886
  for (const key of spec.keyedByField ?? []) {
4610
4887
  const v = readable(key);
4611
- if (!isRec14(v)) continue;
4888
+ if (!isRec16(v)) continue;
4612
4889
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
4613
4890
  }
4614
4891
  for (const key of spec.filterArrays ?? []) {
@@ -4892,13 +5169,13 @@ function validateReferenceIntegrity(stack) {
4892
5169
 
4893
5170
  // src/validate-component-props.ts
4894
5171
  var import_ui3 = require("@objectstack/spec/ui");
4895
- var import_spec = require("@objectstack/spec");
5172
+ var import_spec2 = require("@objectstack/spec");
4896
5173
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
4897
5174
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
4898
- function isRec15(v) {
5175
+ function isRec17(v) {
4899
5176
  return !!v && typeof v === "object" && !Array.isArray(v);
4900
5177
  }
4901
- function strName16(v) {
5178
+ function strName17(v) {
4902
5179
  return typeof v === "string" && v.length > 0 ? v : void 0;
4903
5180
  }
4904
5181
  function asArray24(v) {
@@ -4912,27 +5189,27 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
4912
5189
  var DATASOURCE_SUPPLIED_PROP = "object";
4913
5190
  function suppliedByDataSource(issue, component) {
4914
5191
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
4915
- const dataSource = isRec15(component.dataSource) ? component.dataSource : void 0;
4916
- return strName16(dataSource?.object) !== void 0;
5192
+ const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
5193
+ return strName17(dataSource?.object) !== void 0;
4917
5194
  }
4918
5195
  function validateComponentProps(stack) {
4919
5196
  const findings = [];
4920
- if (!isRec15(stack)) return findings;
5197
+ if (!isRec17(stack)) return findings;
4921
5198
  const pages = asArray24(stack.pages);
4922
5199
  for (let pi = 0; pi < pages.length; pi++) {
4923
5200
  const page = pages[pi];
4924
- if (!isRec15(page)) continue;
4925
- const pageName = strName16(page.name) ?? `#${pi}`;
5201
+ if (!isRec17(page)) continue;
5202
+ const pageName = strName17(page.name) ?? `#${pi}`;
4926
5203
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4927
- const type = strName16(component.type);
5204
+ const type = strName17(component.type);
4928
5205
  if (!type) continue;
4929
5206
  const schema = PROPS_SCHEMAS[type];
4930
5207
  if (!schema) continue;
4931
- const props = isRec15(component.properties) ? component.properties : void 0;
5208
+ const props = isRec17(component.properties) ? component.properties : void 0;
4932
5209
  if (!props) continue;
4933
5210
  const where = `page "${pageName}" \xB7 ${type}`;
4934
5211
  const base = `${path}.properties`;
4935
- for (const f of (0, import_spec.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
5212
+ for (const f of (0, import_spec2.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
4936
5213
  findings.push({
4937
5214
  severity: "warning",
4938
5215
  rule: COMPONENT_PROPS_UNKNOWN_KEY,
@@ -5522,6 +5799,7 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
5522
5799
  var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
5523
5800
  var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
5524
5801
  var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
5802
+ var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
5525
5803
  var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
5526
5804
  function asArray30(v) {
5527
5805
  if (Array.isArray(v)) return v;
@@ -5539,6 +5817,11 @@ function renderNonObject(v) {
5539
5817
  if (t === "bigint") return `${String(v)}n (a bigint)`;
5540
5818
  return `a ${t}`;
5541
5819
  }
5820
+ function renderTriggerToken(v) {
5821
+ if (typeof v === "string") return `'${v}'`;
5822
+ const json = JSON.stringify(v);
5823
+ return json === void 0 ? `a ${typeof v}` : json;
5824
+ }
5542
5825
  function startNodeOf(flow) {
5543
5826
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
5544
5827
  const index = nodes.findIndex((n) => n?.type === "start");
@@ -5658,6 +5941,25 @@ function validateFlowTriggerReadiness(stack) {
5658
5941
  hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
5659
5942
  });
5660
5943
  }
5944
+ const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
5945
+ if (start && flow.type === "record_change" && !routesToSomeTrigger) {
5946
+ const hasTriggerType = config.triggerType != null;
5947
+ findings.push({
5948
+ // `error` (#5762's criterion, applied to a fourth id). The verdict is
5949
+ // the engine's own routing chain — literal `startsWith`/`typeof` tests
5950
+ // with no registry lookup in them — so no installed package can make
5951
+ // this token resolve. `registerTrigger` is keyed by the RESOLVED type,
5952
+ // which is the near-miss worth stating: a plugin can supply the
5953
+ // record-change trigger itself, and it still would not help, because
5954
+ // the flow never reaches the point of asking for one.
5955
+ severity: "error",
5956
+ rule: FLOW_TRIGGER_UNROUTABLE,
5957
+ where: `flow "${flowName}" \u203A start node`,
5958
+ path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
5959
+ 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.`,
5960
+ 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.`
5961
+ });
5962
+ }
5661
5963
  if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
5662
5964
  findings.push({
5663
5965
  severity: "warning",
@@ -5674,7 +5976,7 @@ function validateFlowTriggerReadiness(stack) {
5674
5976
 
5675
5977
  // src/validate-approval-approvers.ts
5676
5978
  var import_automation4 = require("@objectstack/spec/automation");
5677
- var import_spec2 = require("@objectstack/spec");
5979
+ var import_spec3 = require("@objectstack/spec");
5678
5980
  var import_formula3 = require("@objectstack/formula");
5679
5981
  var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
5680
5982
  var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
@@ -5689,8 +5991,8 @@ var APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED = "approval-approver-cross-org-unsup
5689
5991
  var EXPRESSION_ROOTS = /* @__PURE__ */ new Set(["current", "trigger", "vars"]);
5690
5992
  var RESERVED_OUTPUT_KEYS = /* @__PURE__ */ new Set(["decision", "requestId"]);
5691
5993
  var GROUP_ROUTED_TYPES = /* @__PURE__ */ new Set(["position", "team", "department"]);
5692
- var MEMBERSHIP_TIERS = new Set(import_spec2.BUILTIN_MEMBERSHIP_ROLES);
5693
- var MEMBERSHIP_TIER_LIST = import_spec2.BUILTIN_MEMBERSHIP_ROLES.join("/");
5994
+ var MEMBERSHIP_TIERS = new Set(import_spec3.BUILTIN_MEMBERSHIP_ROLES);
5995
+ var MEMBERSHIP_TIER_LIST = import_spec3.BUILTIN_MEMBERSHIP_ROLES.join("/");
5694
5996
  var TYPE_FIX = {
5695
5997
  business_unit: "department",
5696
5998
  bu: "department"
@@ -5885,7 +6187,7 @@ function validateApprovalApprovers(stack) {
5885
6187
  }
5886
6188
 
5887
6189
  // src/validate-record-title.ts
5888
- var import_data6 = require("@objectstack/spec/data");
6190
+ var import_data7 = require("@objectstack/spec/data");
5889
6191
  var TITLE_FORMAT_RETIRED = "title-format-retired";
5890
6192
  var TITLE_UNRESOLVABLE = "title-unresolvable";
5891
6193
  function asArray32(v) {
@@ -5913,7 +6215,7 @@ function validateRecordTitle(stack) {
5913
6215
  hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
5914
6216
  });
5915
6217
  }
5916
- const completeness = (0, import_data6.objectTitleCompleteness)(obj);
6218
+ const completeness = (0, import_data7.objectTitleCompleteness)(obj);
5917
6219
  if (completeness.status === "none") {
5918
6220
  findings.push({
5919
6221
  severity: "warning",
@@ -6009,7 +6311,7 @@ function validateSemanticRoles(stack) {
6009
6311
  (h) => typeof h === "string" && h.length > 0
6010
6312
  );
6011
6313
  if (declaredStrings.length > 0 && declaredGroups.size > 0) {
6012
- const declaredTitle = [obj.nameField, obj.primaryField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6314
+ const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
6013
6315
  const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
6014
6316
  const stripSet = new Set(
6015
6317
  declaredStrings.filter((h) => h !== titleField).slice(0, 4)
@@ -6044,6 +6346,12 @@ function asArray34(v) {
6044
6346
  }
6045
6347
  return [];
6046
6348
  }
6349
+ function isRec18(v) {
6350
+ return !!v && typeof v === "object" && !Array.isArray(v);
6351
+ }
6352
+ function strName18(v) {
6353
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6354
+ }
6047
6355
  function fieldNameOf(entry) {
6048
6356
  if (typeof entry === "string") return entry.length > 0 ? entry : null;
6049
6357
  if (entry && typeof entry === "object" && !Array.isArray(entry)) {
@@ -6052,13 +6360,6 @@ function fieldNameOf(entry) {
6052
6360
  }
6053
6361
  return null;
6054
6362
  }
6055
- function boundObject(view) {
6056
- const data = view.data;
6057
- if (data && typeof data === "object" && typeof data.object === "string") {
6058
- return data.object;
6059
- }
6060
- return typeof view.objectName === "string" ? view.objectName : void 0;
6061
- }
6062
6363
  function validateFormLayout(stack) {
6063
6364
  const findings = [];
6064
6365
  const objectFields = /* @__PURE__ */ new Map();
@@ -6068,44 +6369,44 @@ function validateFormLayout(stack) {
6068
6369
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
6069
6370
  objectFields.set(name, new Set(fields));
6070
6371
  }
6071
- const views = asArray34(stack.views);
6072
- for (let i = 0; i < views.length; i++) {
6073
- const view = views[i];
6074
- if (!view || typeof view !== "object") continue;
6075
- const sections = Array.isArray(view.sections) ? view.sections : null;
6076
- if (!sections) continue;
6077
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6078
- const objName = boundObject(view);
6079
- const known = objName ? objectFields.get(objName) : void 0;
6080
- const where = `view "${viewName}"`;
6081
- const base = `views[${i}]`;
6082
- for (let s = 0; s < sections.length; s++) {
6083
- const sec = sections[s];
6084
- const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
6085
- for (let f = 0; f < secFields.length; f++) {
6086
- const entry = secFields[f];
6087
- const fname = fieldNameOf(entry);
6088
- const fpath = `${base}.sections[${s}].fields[${f}]`;
6089
- if (fname && known && !known.has(fname)) {
6090
- findings.push({
6091
- severity: "warning",
6092
- rule: FORM_FIELD_UNKNOWN,
6093
- where,
6094
- path: fpath,
6095
- message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6096
- hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6097
- });
6098
- }
6099
- const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
6100
- if (colSpan != null) {
6101
- findings.push({
6102
- severity: "warning",
6103
- rule: FORM_COLSPAN_ABSOLUTE,
6104
- where,
6105
- path: `${fpath}.colSpan`,
6106
- message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
6107
- hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6108
- });
6372
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6373
+ const viewName = strName18(view.name) ?? strName18(view.object) ?? viewPath;
6374
+ const containerObject = viewObjectName(view);
6375
+ for (const site of formViewSites(view, viewPath)) {
6376
+ const objName = viewObjectName(site.view) ?? containerObject;
6377
+ const known = objName ? objectFields.get(objName) : void 0;
6378
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
6379
+ for (const bucket of ["sections", "groups"]) {
6380
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6381
+ for (let s = 0; s < sections.length; s++) {
6382
+ const sec = sections[s];
6383
+ const secFields = isRec18(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6384
+ for (let f = 0; f < secFields.length; f++) {
6385
+ const entry = secFields[f];
6386
+ const fname = fieldNameOf(entry);
6387
+ const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
6388
+ if (fname && known && !known.has(fname)) {
6389
+ findings.push({
6390
+ severity: "warning",
6391
+ rule: FORM_FIELD_UNKNOWN,
6392
+ where,
6393
+ path: fpath,
6394
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
6395
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6396
+ });
6397
+ }
6398
+ const colSpan = isRec18(entry) ? entry.colSpan : void 0;
6399
+ if (colSpan != null) {
6400
+ findings.push({
6401
+ severity: "warning",
6402
+ rule: FORM_COLSPAN_ABSOLUTE,
6403
+ where,
6404
+ path: `${fpath}.colSpan`,
6405
+ 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`,
6406
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
6407
+ });
6408
+ }
6409
+ }
6109
6410
  }
6110
6411
  }
6111
6412
  }
@@ -6204,17 +6505,12 @@ function validateSeedStateMachine(stack) {
6204
6505
  }
6205
6506
 
6206
6507
  // src/validate-visibility-predicates.ts
6207
- var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
6508
+ var import_formula4 = require("@objectstack/formula");
6208
6509
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
6510
+ var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
6511
+ var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
6512
+ var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
6209
6513
  var CANONICAL = "visibleWhen";
6210
- var ALIASES = ["visibleOn", "visibility"];
6211
- function asArray35(v) {
6212
- if (Array.isArray(v)) return v;
6213
- if (v && typeof v === "object") {
6214
- return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6215
- }
6216
- return [];
6217
- }
6218
6514
  function predicateSource(v) {
6219
6515
  if (typeof v === "string") return v;
6220
6516
  if (v && typeof v === "object" && typeof v.source === "string") {
@@ -6225,6 +6521,67 @@ function predicateSource(v) {
6225
6521
  function usesRoot(source, root) {
6226
6522
  return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
6227
6523
  }
6524
+ function withoutStringLiterals(source) {
6525
+ return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
6526
+ }
6527
+ var NON_CEL_SPELLINGS = [
6528
+ { wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
6529
+ { wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
6530
+ { wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
6531
+ { wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
6532
+ { wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
6533
+ { wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
6534
+ // Assignment where a comparison was meant. Last, and fenced off from every
6535
+ // operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
6536
+ { wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
6537
+ ];
6538
+ function quoteSource(source) {
6539
+ const flat = source.replace(/\s+/g, " ").trim();
6540
+ return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
6541
+ }
6542
+ function celRefusal(source) {
6543
+ if (!source.trim()) return null;
6544
+ const parsed = (0, import_formula4.parseCelToAstWithReason)(source);
6545
+ if (parsed.ok || parsed.kind === "empty") return null;
6546
+ if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
6547
+ const identifiers = (0, import_formula4.collectCelRootIdentifiers)(source);
6548
+ const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
6549
+ const scannable = withoutStringLiterals(source);
6550
+ return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
6551
+ }
6552
+ function boundName(overrun) {
6553
+ return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
6554
+ }
6555
+ var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
6556
+ function isNode2(v) {
6557
+ return !!v && typeof v === "object" && typeof v.op === "string";
6558
+ }
6559
+ function namespaceRoots(node, out) {
6560
+ if (Array.isArray(node)) {
6561
+ for (const child of node) namespaceRoots(child, out);
6562
+ return;
6563
+ }
6564
+ if (!isNode2(node)) return;
6565
+ const args = node.args;
6566
+ if (Array.isArray(args)) {
6567
+ const receiver = node.op === "rcall" ? args[1] : args[0];
6568
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6569
+ out.add(receiver.args);
6570
+ }
6571
+ }
6572
+ namespaceRoots(args, out);
6573
+ }
6574
+ function firstBareIdentifier(source) {
6575
+ const ast = (0, import_formula4.parseCelToAst)(source);
6576
+ if (!ast) return null;
6577
+ const rooted = /* @__PURE__ */ new Set();
6578
+ namespaceRoots(ast, rooted);
6579
+ return (0, import_formula4.firstUndeclaredReference)(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
6580
+ }
6581
+ var CANONICAL_ROOT_BY_LAYER = {
6582
+ runtime: "record",
6583
+ metadata: "data"
6584
+ };
6228
6585
  var MISLAYER_BY_LAYER = {
6229
6586
  runtime: {
6230
6587
  forbiddenRoot: "data",
@@ -6238,18 +6595,6 @@ var MISLAYER_BY_LAYER = {
6238
6595
  }
6239
6596
  };
6240
6597
  function checkElement(el, where, path, layer, findings) {
6241
- for (const alias of ALIASES) {
6242
- if (el[alias] !== void 0) {
6243
- findings.push({
6244
- severity: "warning",
6245
- rule: VISIBILITY_ALIAS_DEPRECATED,
6246
- where,
6247
- path: `${path}.${alias}`,
6248
- message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
6249
- hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
6250
- });
6251
- }
6252
- }
6253
6598
  const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
6254
6599
  const source = predicateSource(raw);
6255
6600
  const rule = MISLAYER_BY_LAYER[layer];
@@ -6263,6 +6608,43 @@ function checkElement(el, where, path, layer, findings) {
6263
6608
  hint: rule.hint
6264
6609
  });
6265
6610
  }
6611
+ const refusal = source ? celRefusal(source) : null;
6612
+ if (source && refusal?.kind === "bounds") {
6613
+ const bound = boundName(refusal.overrun);
6614
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
6615
+ findings.push({
6616
+ severity: "error",
6617
+ rule: VISIBILITY_PREDICATE_OVER_BUDGET,
6618
+ where,
6619
+ path,
6620
+ 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).`,
6621
+ 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.`
6622
+ });
6623
+ }
6624
+ if (source && refusal?.kind === "syntax") {
6625
+ findings.push({
6626
+ severity: "error",
6627
+ rule: VISIBILITY_PREDICATE_SYNTAX,
6628
+ where,
6629
+ path,
6630
+ 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).`,
6631
+ 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\`).`
6632
+ });
6633
+ }
6634
+ if (source && !refusal) {
6635
+ const bare = firstBareIdentifier(source);
6636
+ if (bare) {
6637
+ const root = CANONICAL_ROOT_BY_LAYER[layer];
6638
+ findings.push({
6639
+ severity: "error",
6640
+ rule: VISIBILITY_BARE_IDENTIFIER,
6641
+ where,
6642
+ path,
6643
+ 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).`,
6644
+ 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`).")
6645
+ });
6646
+ }
6647
+ }
6266
6648
  }
6267
6649
  function isFieldObject(entry) {
6268
6650
  return !!entry && typeof entry === "object" && !Array.isArray(entry);
@@ -6270,43 +6652,304 @@ function isFieldObject(entry) {
6270
6652
  function validateVisibilityPredicates(stack, opts = {}) {
6271
6653
  const layer = opts.layer ?? "runtime";
6272
6654
  const findings = [];
6273
- const views = asArray35(stack.views);
6274
- for (let i = 0; i < views.length; i++) {
6275
- const view = views[i];
6276
- if (!view || typeof view !== "object") continue;
6277
- const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
6278
- const where = `view "${viewName}"`;
6279
- for (const bucket of ["sections", "groups"]) {
6280
- const sections = Array.isArray(view[bucket]) ? view[bucket] : [];
6281
- for (let s = 0; s < sections.length; s++) {
6282
- const sec = sections[s];
6283
- if (!sec || typeof sec !== "object") continue;
6284
- const secPath = `views[${i}].${bucket}[${s}]`;
6285
- checkElement(sec, where, secPath, layer, findings);
6286
- const secFields = Array.isArray(sec.fields) ? sec.fields : [];
6287
- for (let f = 0; f < secFields.length; f++) {
6288
- const entry = secFields[f];
6289
- if (isFieldObject(entry)) {
6290
- checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
6655
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6656
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
6657
+ for (const site of formViewSites(view, viewPath)) {
6658
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
6659
+ for (const bucket of ["sections", "groups"]) {
6660
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6661
+ for (let s = 0; s < sections.length; s++) {
6662
+ const sec = sections[s];
6663
+ if (!sec || typeof sec !== "object") continue;
6664
+ const secPath = `${site.path}.${bucket}[${s}]`;
6665
+ checkElement(sec, where, secPath, layer, findings);
6666
+ const secFields = Array.isArray(sec.fields) ? sec.fields : [];
6667
+ for (let f = 0; f < secFields.length; f++) {
6668
+ const entry = secFields[f];
6669
+ if (isFieldObject(entry)) {
6670
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
6671
+ }
6291
6672
  }
6292
6673
  }
6293
6674
  }
6294
6675
  }
6295
6676
  }
6296
- const pages = asArray35(stack.pages);
6297
- for (let i = 0; i < pages.length; i++) {
6298
- const page = pages[i];
6299
- if (!page || typeof page !== "object") continue;
6300
- const pageName = typeof page.name === "string" ? page.name : `(page ${i})`;
6301
- const where = `page "${pageName}"`;
6302
- const regions = Array.isArray(page.regions) ? page.regions : [];
6303
- for (let r = 0; r < regions.length; r++) {
6304
- const region = regions[r];
6305
- const components = region && typeof region === "object" && Array.isArray(region.components) ? region.components : [];
6306
- for (let c = 0; c < components.length; c++) {
6307
- const comp = components[c];
6308
- if (comp && typeof comp === "object") {
6309
- checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
6677
+ for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
6678
+ const pageName = typeof page.name === "string" ? page.name : void 0;
6679
+ const where = `page "${pageName ?? pagePath}"`;
6680
+ for (const walked of walkPageComponents(page, pagePath)) {
6681
+ checkElement(walked.component, where, walked.path, layer, findings);
6682
+ }
6683
+ }
6684
+ return findings;
6685
+ }
6686
+
6687
+ // src/validate-predicate-path-refs.ts
6688
+ var import_formula5 = require("@objectstack/formula");
6689
+ var import_kernel2 = require("@objectstack/spec/kernel");
6690
+ var import_spec4 = require("@objectstack/spec");
6691
+ var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
6692
+ var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
6693
+ var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
6694
+ var ROOT = "data";
6695
+ var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
6696
+ function defOf(schema) {
6697
+ if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
6698
+ const s = schema;
6699
+ return s.def ?? s._def;
6700
+ }
6701
+ function peel(schema, depth = 0) {
6702
+ if (!schema || depth > 25) return schema;
6703
+ const d = defOf(schema);
6704
+ if (!d) return schema;
6705
+ switch (d.type) {
6706
+ case "optional":
6707
+ case "nullable":
6708
+ case "default":
6709
+ case "prefault":
6710
+ case "readonly":
6711
+ case "catch":
6712
+ case "nonoptional":
6713
+ return peel(d.innerType, depth + 1);
6714
+ case "lazy":
6715
+ return peel(d.getter(), depth + 1);
6716
+ case "pipe": {
6717
+ const inner = peel(d.in, depth + 1);
6718
+ return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
6719
+ }
6720
+ default:
6721
+ return schema;
6722
+ }
6723
+ }
6724
+ function optionsOf(d) {
6725
+ return Array.isArray(d?.options) ? d.options : [];
6726
+ }
6727
+ function keysOf(schema, depth = 0) {
6728
+ if (depth > 25) return null;
6729
+ const u = peel(schema);
6730
+ const d = defOf(u);
6731
+ if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
6732
+ if (d?.type === "union" || d?.type === "discriminated_union") {
6733
+ const all = /* @__PURE__ */ new Set();
6734
+ let keyBearing = false;
6735
+ for (const option of optionsOf(d)) {
6736
+ const k = keysOf(option, depth + 1);
6737
+ if (!k) continue;
6738
+ keyBearing = true;
6739
+ for (const key of k) all.add(key);
6740
+ }
6741
+ return keyBearing ? [...all] : null;
6742
+ }
6743
+ if (d?.type === "intersection") {
6744
+ const left = keysOf(d.left, depth + 1);
6745
+ const right = keysOf(d.right, depth + 1);
6746
+ if (!left && !right) return null;
6747
+ return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
6748
+ }
6749
+ return null;
6750
+ }
6751
+ function propertyOf(schema, key, depth = 0) {
6752
+ if (depth > 25) return void 0;
6753
+ const u = peel(schema);
6754
+ const d = defOf(u);
6755
+ if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
6756
+ if (d?.type === "union" || d?.type === "discriminated_union") {
6757
+ for (const option of optionsOf(d)) {
6758
+ const found = propertyOf(option, key, depth + 1);
6759
+ if (found !== void 0) return found;
6760
+ }
6761
+ }
6762
+ if (d?.type === "intersection") {
6763
+ return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
6764
+ }
6765
+ return void 0;
6766
+ }
6767
+ function rowScopeOf(scope, key) {
6768
+ const prop = propertyOf(scope, key);
6769
+ if (prop === void 0) return void 0;
6770
+ let node = peel(prop);
6771
+ for (let i = 0; i < 25; i++) {
6772
+ const d = defOf(node);
6773
+ if (d?.type === "array") node = peel(d.element);
6774
+ else if (d?.type === "record") node = peel(d.valueType);
6775
+ else return node;
6776
+ }
6777
+ return node;
6778
+ }
6779
+ function stepInto(scope, segment) {
6780
+ const u = peel(scope);
6781
+ const d = defOf(u);
6782
+ if (d?.type === "record") return { kind: "declared", next: d.valueType };
6783
+ const declared = keysOf(u);
6784
+ if (declared === null) return { kind: "opaque" };
6785
+ if (!declared.includes(segment)) return { kind: "undeclared", declared };
6786
+ return { kind: "declared", next: propertyOf(u, segment) };
6787
+ }
6788
+ function isNode3(v) {
6789
+ return !!v && typeof v === "object" && typeof v.op === "string";
6790
+ }
6791
+ function memberChain(node) {
6792
+ if (!isNode3(node)) return null;
6793
+ if (node.op === "id" && typeof node.args === "string") return [node.args];
6794
+ if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
6795
+ const head = memberChain(node.args[0]);
6796
+ return head ? [...head, node.args[1]] : null;
6797
+ }
6798
+ return null;
6799
+ }
6800
+ function rootedPaths(node, out) {
6801
+ if (Array.isArray(node)) {
6802
+ for (const child of node) rootedPaths(child, out);
6803
+ return;
6804
+ }
6805
+ if (!isNode3(node)) return;
6806
+ if (node.op === ".") {
6807
+ const chain = memberChain(node);
6808
+ if (chain && chain[0] === ROOT && chain.length > 1) {
6809
+ out.push(chain.slice(1));
6810
+ return;
6811
+ }
6812
+ }
6813
+ rootedPaths(node.args, out);
6814
+ }
6815
+ function classifyIdentifiers(node, values, excluded) {
6816
+ if (Array.isArray(node)) {
6817
+ for (const child of node) classifyIdentifiers(child, values, excluded);
6818
+ return;
6819
+ }
6820
+ if (!isNode3(node)) return;
6821
+ const args = node.args;
6822
+ if (Array.isArray(args)) {
6823
+ const receiver = node.op === "rcall" ? args[1] : args[0];
6824
+ if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
6825
+ excluded.add(receiver.args);
6826
+ }
6827
+ if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
6828
+ const macroArgs = args[2];
6829
+ if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
6830
+ const bound = macroArgs[0];
6831
+ if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
6832
+ excluded.add(bound.args);
6833
+ }
6834
+ }
6835
+ }
6836
+ }
6837
+ if (node.op === "id" && typeof node.args === "string") {
6838
+ values.add(node.args);
6839
+ return;
6840
+ }
6841
+ classifyIdentifiers(args, values, excluded);
6842
+ }
6843
+ function predicateSource2(v) {
6844
+ if (typeof v === "string") return v;
6845
+ if (v && typeof v === "object" && typeof v.source === "string") {
6846
+ return v.source;
6847
+ }
6848
+ return void 0;
6849
+ }
6850
+ function isRec19(v) {
6851
+ return !!v && typeof v === "object" && !Array.isArray(v);
6852
+ }
6853
+ function schemaIdOf(view) {
6854
+ const data = view.data;
6855
+ if (!isRec19(data)) return void 0;
6856
+ if (data.provider !== "schema") return void 0;
6857
+ return typeof data.schemaId === "string" ? data.schemaId : void 0;
6858
+ }
6859
+ function checkPredicate(source, scope, where, path, findings) {
6860
+ const ast = (0, import_formula5.parseCelToAst)(source);
6861
+ if (!ast) return;
6862
+ const paths = [];
6863
+ rootedPaths(ast, paths);
6864
+ for (const segments of paths) {
6865
+ let cursor = scope;
6866
+ const walked = [];
6867
+ for (const segment of segments) {
6868
+ const step = stepInto(cursor, segment);
6869
+ if (step.kind === "opaque") break;
6870
+ if (step.kind === "undeclared") {
6871
+ const full = [ROOT, ...walked, segment].join(".");
6872
+ const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
6873
+ findings.push({
6874
+ severity: "error",
6875
+ rule: PREDICATE_PATH_UNRESOLVED,
6876
+ where,
6877
+ path,
6878
+ 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).`,
6879
+ hint: `${(0, import_spec4.formatSuggestion)((0, import_spec4.findClosestMatches)(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
6880
+ });
6881
+ break;
6882
+ }
6883
+ walked.push(segment);
6884
+ cursor = step.next;
6885
+ }
6886
+ }
6887
+ const declaredHere = keysOf(scope);
6888
+ if (!declaredHere) return;
6889
+ const values = /* @__PURE__ */ new Set();
6890
+ const excluded = /* @__PURE__ */ new Set();
6891
+ classifyIdentifiers(ast, values, excluded);
6892
+ for (const id of values) {
6893
+ if (excluded.has(id) || !declaredHere.includes(id)) continue;
6894
+ findings.push({
6895
+ severity: "error",
6896
+ rule: PREDICATE_PATH_UNROOTED,
6897
+ where,
6898
+ path,
6899
+ 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).`,
6900
+ 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).`
6901
+ });
6902
+ }
6903
+ }
6904
+ function walkFields(entries, scope, where, base, findings, depth) {
6905
+ if (!Array.isArray(entries) || depth > 12) return;
6906
+ for (let i = 0; i < entries.length; i++) {
6907
+ const entry = entries[i];
6908
+ if (!isRec19(entry)) continue;
6909
+ const path = `${base}[${i}]`;
6910
+ for (const key of PREDICATE_KEYS) {
6911
+ const source = predicateSource2(entry[key]);
6912
+ if (source !== void 0 && source.trim()) {
6913
+ checkPredicate(source, scope, where, `${path}.${key}`, findings);
6914
+ break;
6915
+ }
6916
+ }
6917
+ if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
6918
+ const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
6919
+ walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
6920
+ }
6921
+ }
6922
+ }
6923
+ function validatePredicatePathRefs(stack, opts = {}) {
6924
+ const resolveSchema = opts.resolveSchema ?? ((schemaId) => (0, import_kernel2.getMetadataTypeSchema)(schemaId));
6925
+ const findings = [];
6926
+ for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6927
+ const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
6928
+ for (const site of formViewSites(view, viewPath)) {
6929
+ const schemaId = schemaIdOf(site.view);
6930
+ if (!schemaId) continue;
6931
+ let root;
6932
+ try {
6933
+ root = resolveSchema(schemaId);
6934
+ } catch {
6935
+ continue;
6936
+ }
6937
+ if (!root) continue;
6938
+ const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
6939
+ for (const bucket of ["sections", "groups"]) {
6940
+ const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6941
+ for (let s = 0; s < sections.length; s++) {
6942
+ const section = sections[s];
6943
+ if (!isRec19(section)) continue;
6944
+ const sectionPath = `${site.path}.${bucket}[${s}]`;
6945
+ for (const key of PREDICATE_KEYS) {
6946
+ const source = predicateSource2(section[key]);
6947
+ if (source !== void 0 && source.trim()) {
6948
+ checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
6949
+ break;
6950
+ }
6951
+ }
6952
+ walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
6310
6953
  }
6311
6954
  }
6312
6955
  }
@@ -6340,7 +6983,7 @@ var OWD_WIDTH = {
6340
6983
  public_read: 1,
6341
6984
  public_read_write: 2
6342
6985
  };
6343
- function asArray36(v) {
6986
+ function asArray35(v) {
6344
6987
  if (Array.isArray(v)) return v;
6345
6988
  if (v && typeof v === "object") {
6346
6989
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6366,7 +7009,7 @@ function refOf(def) {
6366
7009
  return typeof r === "string" && r ? r : void 0;
6367
7010
  }
6368
7011
  function firstMasterDetailField(obj) {
6369
- for (const f of asArray36(obj.fields)) {
7012
+ for (const f of asArray35(obj.fields)) {
6370
7013
  if (f.type === "master_detail") {
6371
7014
  return { name: String(f.name ?? "?"), parent: refOf(f) };
6372
7015
  }
@@ -6379,8 +7022,8 @@ function grantsObjectAccess(p) {
6379
7022
  function validateSecurityPosture(stack, opts) {
6380
7023
  const findings = [];
6381
7024
  if (!stack || typeof stack !== "object") return findings;
6382
- const objects = asArray36(stack.objects);
6383
- const permissionSets = asArray36(stack.permissions);
7025
+ const objects = asArray35(stack.objects);
7026
+ const permissionSets = asArray35(stack.permissions);
6384
7027
  for (let i = 0; i < objects.length; i++) {
6385
7028
  const obj = objects[i];
6386
7029
  if (!obj || typeof obj !== "object") continue;
@@ -6509,10 +7152,10 @@ function validateSecurityPosture(stack, opts) {
6509
7152
  if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
6510
7153
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
6511
7154
  flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
6512
- for (const f of asArray36(obj.fields)) {
7155
+ for (const f of asArray35(obj.fields)) {
6513
7156
  flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
6514
7157
  }
6515
- for (const [ai, action] of asArray36(obj.actions).entries()) {
7158
+ for (const [ai, action] of asArray35(obj.actions).entries()) {
6516
7159
  flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
6517
7160
  }
6518
7161
  }
@@ -6521,19 +7164,19 @@ function validateSecurityPosture(stack, opts) {
6521
7164
  if (!ps || typeof ps !== "object") continue;
6522
7165
  flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
6523
7166
  }
6524
- for (const [i, pos] of asArray36(stack.positions).entries()) {
7167
+ for (const [i, pos] of asArray35(stack.positions).entries()) {
6525
7168
  flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
6526
7169
  }
6527
- for (const [i, app] of asArray36(stack.apps).entries()) {
7170
+ for (const [i, app] of asArray35(stack.apps).entries()) {
6528
7171
  flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
6529
7172
  }
6530
- for (const [i, book] of asArray36(stack.books).entries()) {
7173
+ for (const [i, book] of asArray35(stack.books).entries()) {
6531
7174
  flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
6532
7175
  }
6533
7176
  const stackSetNames = new Set(
6534
7177
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
6535
7178
  );
6536
- for (const [i, book] of asArray36(stack.books).entries()) {
7179
+ for (const [i, book] of asArray35(stack.books).entries()) {
6537
7180
  const audience = book.audience;
6538
7181
  if (!audience || typeof audience !== "object") continue;
6539
7182
  const setName = audience.permissionSet;
@@ -6611,7 +7254,7 @@ function validateSecurityPosture(stack, opts) {
6611
7254
  }
6612
7255
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
6613
7256
  const nowMs = opts?.nowMs ?? Date.now();
6614
- for (const [i, seed] of asArray36(stack.data).entries()) {
7257
+ for (const [i, seed] of asArray35(stack.data).entries()) {
6615
7258
  const seedObject = typeof seed.object === "string" ? seed.object : "";
6616
7259
  if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
6617
7260
  const records = Array.isArray(seed.records) ? seed.records : [];
@@ -6656,7 +7299,7 @@ var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
6656
7299
  var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
6657
7300
  var ORG_PARENT_FIELD = "parent_organization_id";
6658
7301
  var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
6659
- function asArray37(v) {
7302
+ function asArray36(v) {
6660
7303
  if (Array.isArray(v)) return v;
6661
7304
  if (v && typeof v === "object") {
6662
7305
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6686,9 +7329,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
6686
7329
  function validateOrgAxisRedLines(stack) {
6687
7330
  const findings = [];
6688
7331
  const cfg = stack ?? {};
6689
- const permissionSets = asArray37(cfg.permissions);
7332
+ const permissionSets = asArray36(cfg.permissions);
6690
7333
  permissionSets.forEach((ps, psIndex) => {
6691
- asArray37(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7334
+ asArray36(ps.rowLevelSecurity).forEach((policy, pIndex) => {
6692
7335
  for (const clause of ["using", "check"]) {
6693
7336
  if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
6694
7337
  findings.push({
@@ -6702,7 +7345,7 @@ function validateOrgAxisRedLines(stack) {
6702
7345
  }
6703
7346
  });
6704
7347
  });
6705
- asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
7348
+ asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
6706
7349
  const slots = [
6707
7350
  { key: "condition", text: expressionText(rule.condition) },
6708
7351
  { key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
@@ -6720,9 +7363,9 @@ function validateOrgAxisRedLines(stack) {
6720
7363
  }
6721
7364
  });
6722
7365
  const tenancyDisabledObjects = new Set(
6723
- asArray37(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
7366
+ asArray36(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
6724
7367
  );
6725
- asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
7368
+ asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
6726
7369
  const target = str(rule.object);
6727
7370
  if (!target || !tenancyDisabledObjects.has(target)) return;
6728
7371
  const sharedWith = rule.sharedWith;
@@ -6742,10 +7385,10 @@ function validateOrgAxisRedLines(stack) {
6742
7385
  }
6743
7386
 
6744
7387
  // src/validate-sharing-rule-enforceability.ts
6745
- var import_formula4 = require("@objectstack/formula");
7388
+ var import_formula6 = require("@objectstack/formula");
6746
7389
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
6747
7390
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
6748
- function asArray38(v) {
7391
+ function asArray37(v) {
6749
7392
  if (Array.isArray(v)) return v;
6750
7393
  if (v && typeof v === "object") {
6751
7394
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6772,10 +7415,10 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
6772
7415
  function validateSharingRuleEnforceability(stack) {
6773
7416
  const findings = [];
6774
7417
  const cfg = stack ?? {};
6775
- asArray38(cfg.sharingRules).forEach((rule, index) => {
7418
+ asArray37(cfg.sharingRules).forEach((rule, index) => {
6776
7419
  const input = toCompilerInput(rule.condition);
6777
7420
  if (input === null) return;
6778
- const result = (0, import_formula4.compileCelToFilter)(input, { variables: {} });
7421
+ const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
6779
7422
  if (result.ok) return;
6780
7423
  if (result.reason === "parse-error") return;
6781
7424
  const name = str2(rule.name) || String(index);
@@ -6808,10 +7451,11 @@ function validateSharingRuleEnforceability(stack) {
6808
7451
  }
6809
7452
 
6810
7453
  // src/validate-rls-predicate-enforceability.ts
6811
- var import_formula5 = require("@objectstack/formula");
7454
+ var import_formula7 = require("@objectstack/formula");
6812
7455
  var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
6813
7456
  var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
6814
- function asArray39(v) {
7457
+ var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
7458
+ function asArray38(v) {
6815
7459
  if (Array.isArray(v)) return v;
6816
7460
  if (v && typeof v === "object") {
6817
7461
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -6822,6 +7466,13 @@ function str3(v) {
6822
7466
  return typeof v === "string" ? v : "";
6823
7467
  }
6824
7468
  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.";
7469
+ function boundsOverrunOf(bridged) {
7470
+ const parsed = (0, import_formula7.parseCelToAstWithReason)(bridged);
7471
+ return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
7472
+ }
7473
+ function quote(source) {
7474
+ return source.length > 200 ? `${source.slice(0, 197)}...` : source;
7475
+ }
6825
7476
  function consequence(clause) {
6826
7477
  const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
6827
7478
  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.";
@@ -6829,20 +7480,36 @@ function consequence(clause) {
6829
7480
  function validateRlsPredicateEnforceability(stack) {
6830
7481
  const findings = [];
6831
7482
  const cfg = stack ?? {};
6832
- asArray39(cfg.permissions).forEach((ps, psIndex) => {
6833
- asArray39(ps.rowLevelSecurity).forEach((policy, pIndex) => {
7483
+ asArray38(cfg.permissions).forEach((ps, psIndex) => {
7484
+ asArray38(ps.rowLevelSecurity).forEach((policy, pIndex) => {
6834
7485
  for (const clause of ["using", "check"]) {
6835
7486
  const source = str3(policy[clause]);
6836
7487
  if (!source.trim()) continue;
6837
- if ((0, import_formula5.isSupportedRlsExpression)(source)) continue;
6838
- const why = (0, import_formula5.isPushdownableCel)((0, import_formula5.sqlPredicateToCel)(source));
7488
+ if ((0, import_formula7.isSupportedRlsExpression)(source)) continue;
7489
+ const bridged = (0, import_formula7.sqlPredicateToCel)(source);
7490
+ const why = (0, import_formula7.isPushdownableCel)(bridged);
6839
7491
  const detail = why.ok ? "" : why.detail;
6840
7492
  const parseError = !why.ok && why.reason === "parse-error";
7493
+ const overrun = parseError ? boundsOverrunOf(bridged) : null;
6841
7494
  const psName = str3(ps.name) || String(psIndex);
6842
7495
  const policyName = str3(policy.name) || String(pIndex);
6843
7496
  const object = str3(policy.object);
6844
7497
  const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
6845
7498
  const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
7499
+ if (overrun) {
7500
+ const bound = overrun.limit ?? "an unnamed platform CEL bound";
7501
+ const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
7502
+ const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
7503
+ findings.push({
7504
+ severity: "error",
7505
+ rule: RLS_PREDICATE_OVER_BUDGET,
7506
+ where,
7507
+ path,
7508
+ message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
7509
+ 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).`
7510
+ });
7511
+ continue;
7512
+ }
6846
7513
  if (parseError) {
6847
7514
  findings.push({
6848
7515
  severity: "error",
@@ -6874,11 +7541,11 @@ var import_meta4 = {};
6874
7541
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
6875
7542
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
6876
7543
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
6877
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6878
- function asArray40(v) {
6879
- if (Array.isArray(v)) return v.filter(isRec16);
6880
- if (isRec16(v)) {
6881
- return Object.entries(v).filter(([, def]) => isRec16(def)).map(([name, def]) => ({ name, ...def }));
7544
+ var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7545
+ function asArray39(v) {
7546
+ if (Array.isArray(v)) return v.filter(isRec20);
7547
+ if (isRec20(v)) {
7548
+ return Object.entries(v).filter(([, def]) => isRec20(def)).map(([name, def]) => ({ name, ...def }));
6882
7549
  }
6883
7550
  return [];
6884
7551
  }
@@ -6895,7 +7562,7 @@ function loadAjv() {
6895
7562
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
6896
7563
  );
6897
7564
  }
6898
- const ctor = isRec16(mod) && "default" in mod ? mod.default : mod;
7565
+ const ctor = isRec20(mod) && "default" in mod ? mod.default : mod;
6899
7566
  cachedAjv = ctor;
6900
7567
  return ctor;
6901
7568
  }
@@ -6910,7 +7577,7 @@ function loadAddFormats() {
6910
7577
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
6911
7578
  );
6912
7579
  }
6913
- const plugin = isRec16(mod) && "default" in mod ? mod.default : mod;
7580
+ const plugin = isRec20(mod) && "default" in mod ? mod.default : mod;
6914
7581
  cachedAddFormats = plugin;
6915
7582
  return plugin;
6916
7583
  }
@@ -6940,17 +7607,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
6940
7607
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
6941
7608
  for (const branch of ["then", "otherwise"]) {
6942
7609
  const nested = rule[branch];
6943
- if (isRec16(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
7610
+ if (isRec20(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6944
7611
  }
6945
7612
  return out;
6946
7613
  }
6947
7614
  function walkObjectValidationRules(stack) {
6948
7615
  const walked = [];
6949
- if (!isRec16(stack)) return walked;
6950
- for (const obj of asArray40(stack.objects)) {
7616
+ if (!isRec20(stack)) return walked;
7617
+ for (const obj of asArray39(stack.objects)) {
6951
7618
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6952
7619
  const validations = obj.validations;
6953
- for (const authored of asArray40(validations)) {
7620
+ for (const authored of asArray39(validations)) {
6954
7621
  for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
6955
7622
  walked.push({
6956
7623
  rule,
@@ -6981,7 +7648,7 @@ function validateRuleCompilability(stack) {
6981
7648
  });
6982
7649
  }
6983
7650
  }
6984
- if (rule.type === "json_schema" && isRec16(rule.schema)) {
7651
+ if (rule.type === "json_schema" && isRec20(rule.schema)) {
6985
7652
  try {
6986
7653
  createRuntimeAjv().compile(rule.schema);
6987
7654
  } catch (err) {
@@ -7001,7 +7668,7 @@ function validateRuleCompilability(stack) {
7001
7668
 
7002
7669
  // src/validate-rule-schema-formats.ts
7003
7670
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7004
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7671
+ var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7005
7672
  var SUBSCHEMA_KEYS = [
7006
7673
  "additionalItems",
7007
7674
  "additionalProperties",
@@ -7025,7 +7692,7 @@ var SUBSCHEMA_MAP_KEYS = [
7025
7692
  var MAX_SCHEMA_WALK_DEPTH = 32;
7026
7693
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7027
7694
  function collectFormatUses(schema, pointer, out, depth) {
7028
- if (!isRec17(schema)) return;
7695
+ if (!isRec21(schema)) return;
7029
7696
  if (typeof schema.format === "string") {
7030
7697
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7031
7698
  }
@@ -7044,7 +7711,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7044
7711
  }
7045
7712
  for (const key of SUBSCHEMA_MAP_KEYS) {
7046
7713
  const value = schema[key];
7047
- if (!isRec17(value)) continue;
7714
+ if (!isRec21(value)) continue;
7048
7715
  for (const [name, entry] of Object.entries(value)) {
7049
7716
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7050
7717
  }
@@ -7052,13 +7719,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7052
7719
  const items = schema.items;
7053
7720
  if (Array.isArray(items)) {
7054
7721
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7055
- } else if (isRec17(items)) {
7722
+ } else if (isRec21(items)) {
7056
7723
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7057
7724
  }
7058
7725
  const dependencies = schema.dependencies;
7059
- if (isRec17(dependencies)) {
7726
+ if (isRec21(dependencies)) {
7060
7727
  for (const [name, entry] of Object.entries(dependencies)) {
7061
- if (!isRec17(entry)) continue;
7728
+ if (!isRec21(entry)) continue;
7062
7729
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7063
7730
  }
7064
7731
  }
@@ -7097,7 +7764,7 @@ function validateRuleSchemaFormats(stack) {
7097
7764
  const findings = [];
7098
7765
  const pending = [];
7099
7766
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7100
- if (rule.type !== "json_schema" || !isRec17(rule.schema)) continue;
7767
+ if (rule.type !== "json_schema" || !isRec21(rule.schema)) continue;
7101
7768
  const uses = [];
7102
7769
  collectFormatUses(rule.schema, "", uses, 0);
7103
7770
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7123,14 +7790,14 @@ function validateRuleSchemaFormats(stack) {
7123
7790
 
7124
7791
  // src/validate-action-locations.ts
7125
7792
  var ACTION_NO_PLACEMENT = "action-no-placement";
7126
- function asArray41(v) {
7793
+ function asArray40(v) {
7127
7794
  if (Array.isArray(v)) return v;
7128
7795
  if (v && typeof v === "object") {
7129
7796
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7130
7797
  }
7131
7798
  return [];
7132
7799
  }
7133
- function strName17(v) {
7800
+ function strName19(v) {
7134
7801
  return typeof v === "string" && v.length > 0 ? v : void 0;
7135
7802
  }
7136
7803
  function strList3(v) {
@@ -7144,8 +7811,8 @@ function collectNamePlacedActions(stack) {
7144
7811
  for (const key of ["rowActions", "bulkActions"]) {
7145
7812
  for (const n of strList3(list3[key])) placed.add(n);
7146
7813
  }
7147
- for (const def of asArray41(list3.bulkActionDefs)) {
7148
- const n = strName17(def?.name);
7814
+ for (const def of asArray40(list3.bulkActionDefs)) {
7815
+ const n = strName19(def?.name);
7149
7816
  if (n) placed.add(n);
7150
7817
  }
7151
7818
  };
@@ -7153,12 +7820,12 @@ function collectNamePlacedActions(stack) {
7153
7820
  if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
7154
7821
  for (const lv of Object.values(listViews)) harvest(lv);
7155
7822
  };
7156
- for (const view of asArray41(stack.views)) {
7823
+ for (const view of asArray40(stack.views)) {
7157
7824
  if (!view || typeof view !== "object") continue;
7158
7825
  harvest(view.list);
7159
7826
  harvestListViews(view.listViews);
7160
7827
  }
7161
- for (const obj of asArray41(stack.objects)) {
7828
+ for (const obj of asArray40(stack.objects)) {
7162
7829
  if (!obj || typeof obj !== "object") continue;
7163
7830
  harvestListViews(obj.listViews);
7164
7831
  }
@@ -7171,7 +7838,7 @@ function validateActionLocations(stack) {
7171
7838
  const check = (action, path) => {
7172
7839
  if (!action || typeof action !== "object") return;
7173
7840
  if ("locations" in action) return;
7174
- const name = strName17(action.name);
7841
+ const name = strName19(action.name);
7175
7842
  if (!name) return;
7176
7843
  if (namePlaced.has(name)) return;
7177
7844
  findings.push({
@@ -7180,16 +7847,16 @@ function validateActionLocations(stack) {
7180
7847
  where: `action "${name}"`,
7181
7848
  path,
7182
7849
  message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
7183
- hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`, `global_nav`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
7850
+ hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
7184
7851
  });
7185
7852
  };
7186
- const actions = asArray41(stack.actions);
7853
+ const actions = asArray40(stack.actions);
7187
7854
  for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
7188
- const objects = asArray41(stack.objects);
7855
+ const objects = asArray40(stack.objects);
7189
7856
  for (let oi = 0; oi < objects.length; oi++) {
7190
7857
  const obj = objects[oi];
7191
7858
  if (!obj || typeof obj !== "object") continue;
7192
- const own = asArray41(obj.actions);
7859
+ const own = asArray40(obj.actions);
7193
7860
  for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
7194
7861
  }
7195
7862
  return findings;
@@ -7197,7 +7864,8 @@ function validateActionLocations(stack) {
7197
7864
 
7198
7865
  // src/lint-flow-patterns.ts
7199
7866
  var import_automation5 = require("@objectstack/spec/automation");
7200
- function asArray42(v) {
7867
+ var import_data8 = require("@objectstack/spec/data");
7868
+ function asArray41(v) {
7201
7869
  if (Array.isArray(v)) return v;
7202
7870
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7203
7871
  return [];
@@ -7471,7 +8139,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
7471
8139
  function filterCarriesNoCondition(filter) {
7472
8140
  if (filter === void 0 || filter === null) return true;
7473
8141
  if (typeof filter !== "object" || Array.isArray(filter)) return false;
7474
- return Object.keys(filter).length === 0;
8142
+ return (0, import_data8.reduceFilterVerdict)(filter) === "true";
8143
+ }
8144
+ function describeUnboundedFilter(filter) {
8145
+ if (filter === void 0 || filter === null) return "no `filter` key";
8146
+ if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
8147
+ return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
8148
+ }
8149
+ function previewFilter(filter) {
8150
+ try {
8151
+ const json = JSON.stringify(filter);
8152
+ if (typeof json !== "string") return typeof filter;
8153
+ return json.length > 80 ? `${json.slice(0, 77)}...` : json;
8154
+ } catch {
8155
+ return typeof filter;
8156
+ }
7475
8157
  }
7476
8158
  function scanUnboundedBulkWrites(at, nodes, findings) {
7477
8159
  for (const node of nodes) {
@@ -7482,10 +8164,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
7482
8164
  if (cfg.multi !== true) continue;
7483
8165
  if (!filterCarriesNoCondition(cfg.filter)) continue;
7484
8166
  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`";
8167
+ const filterState = describeUnboundedFilter(cfg.filter);
7486
8168
  findings.push({
7487
8169
  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.`,
8170
+ 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
8171
  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
8172
  // Warning, not `error`: see the severity policy at the top of this file.
7491
8173
  // The shape has a legitimate reading the engine grants on purpose, so it is
@@ -7567,7 +8249,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
7567
8249
  }
7568
8250
  function lintFlowPatterns(stack) {
7569
8251
  const findings = [];
7570
- for (const flow of asArray42(stack.flows)) {
8252
+ for (const flow of asArray41(stack.flows)) {
7571
8253
  const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
7572
8254
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
7573
8255
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
@@ -7665,7 +8347,7 @@ var import_node_fs = require("fs");
7665
8347
  var import_meta5 = {};
7666
8348
  var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
7667
8349
  var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
7668
- function asArray43(v) {
8350
+ function asArray42(v) {
7669
8351
  if (Array.isArray(v)) return v;
7670
8352
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7671
8353
  return [];
@@ -7787,6 +8469,13 @@ var TYPE_COLLECTIONS = [
7787
8469
  // checks every widget on the dashboard. Registering it here is not optional
7788
8470
  // bookkeeping: without it the ledger would be newly correct and newly
7789
8471
  // silent, which is the shape this lint exists to prevent.
8472
+ //
8473
+ // As of #6774 the dashboard ledger warns on NOTHING — four of those five were
8474
+ // retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
8475
+ // gave it a renderer. The type STAYS listed, the resolved state `webhook` and
8476
+ // `email_template` already sit in: a zero-warn entry costs one empty map
8477
+ // lookup, and it means a future regression that re-deadens a widget key warns
8478
+ // on its own instead of waiting for someone to notice this list again.
7790
8479
  { type: "dashboard", key: "dashboards" }
7791
8480
  ];
7792
8481
  function lintLivenessProperties(stack) {
@@ -7795,11 +8484,11 @@ function lintLivenessProperties(stack) {
7795
8484
  const findings = [];
7796
8485
  const objectWarn = loadWarnMap(dir, "object");
7797
8486
  const fieldWarn = loadWarnMap(dir, "field");
7798
- for (const obj of asArray43(stack.objects)) {
8487
+ for (const obj of asArray42(stack.objects)) {
7799
8488
  const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7800
8489
  if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
7801
8490
  if (fieldWarn.size > 0) {
7802
- for (const field of asArray43(obj.fields)) {
8491
+ for (const field of asArray42(obj.fields)) {
7803
8492
  const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
7804
8493
  checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
7805
8494
  }
@@ -7808,7 +8497,7 @@ function lintLivenessProperties(stack) {
7808
8497
  for (const { type, key } of TYPE_COLLECTIONS) {
7809
8498
  const warnMap = loadWarnMap(dir, type);
7810
8499
  if (warnMap.size === 0) continue;
7811
- for (const item of asArray43(stack[key])) {
8500
+ for (const item of asArray42(stack[key])) {
7812
8501
  const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
7813
8502
  checkItem(type, item, `${type} '${name}'`, warnMap, findings);
7814
8503
  }
@@ -7817,12 +8506,12 @@ function lintLivenessProperties(stack) {
7817
8506
  }
7818
8507
 
7819
8508
  // src/lint-autonumber-formats.ts
7820
- var import_data7 = require("@objectstack/spec/data");
8509
+ var import_data9 = require("@objectstack/spec/data");
7821
8510
  var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7822
8511
  var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7823
8512
  var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
7824
8513
  var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
7825
- function asArray44(v) {
8514
+ function asArray43(v) {
7826
8515
  if (Array.isArray(v)) return v;
7827
8516
  if (v && typeof v === "object") {
7828
8517
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -7831,9 +8520,9 @@ function asArray44(v) {
7831
8520
  }
7832
8521
  function lintAutonumberFormats(stack) {
7833
8522
  const findings = [];
7834
- for (const obj of asArray44(stack.objects)) {
8523
+ for (const obj of asArray43(stack.objects)) {
7835
8524
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7836
- const fields = asArray44(obj.fields);
8525
+ const fields = asArray43(obj.fields);
7837
8526
  const fieldMeta = /* @__PURE__ */ new Map();
7838
8527
  for (const f of fields) {
7839
8528
  if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
@@ -7843,8 +8532,8 @@ function lintAutonumberFormats(stack) {
7843
8532
  const name = typeof f.name === "string" ? f.name : "(unnamed field)";
7844
8533
  const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
7845
8534
  if (!fmt) continue;
7846
- const tokens = (0, import_data7.parseAutonumberFormat)(fmt);
7847
- const refs = (0, import_data7.referencedFields)(tokens);
8535
+ const tokens = (0, import_data9.parseAutonumberFormat)(fmt);
8536
+ const refs = (0, import_data9.referencedFields)(tokens);
7848
8537
  const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
7849
8538
  for (const t of tokens) {
7850
8539
  if (t.kind !== "literal") continue;
@@ -7898,8 +8587,8 @@ function lintAutonumberFormats(stack) {
7898
8587
  }
7899
8588
 
7900
8589
  // src/lint-view-refs.ts
7901
- var import_spec3 = require("@objectstack/spec");
7902
- function asArray45(v) {
8590
+ var import_spec5 = require("@objectstack/spec");
8591
+ function asArray44(v) {
7903
8592
  if (Array.isArray(v)) return v;
7904
8593
  if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7905
8594
  return [];
@@ -7927,16 +8616,16 @@ function lintViewRefs(stack) {
7927
8616
  s.add(kind);
7928
8617
  };
7929
8618
  const containers = [];
7930
- for (const v of asArray45(stack.views)) {
8619
+ for (const v of asArray44(stack.views)) {
7931
8620
  if (v.viewKind) {
7932
8621
  if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7933
8622
  continue;
7934
8623
  }
7935
- if (!(0, import_spec3.isAggregatedViewContainer)(v)) continue;
8624
+ if (!(0, import_spec5.isAggregatedViewContainer)(v)) continue;
7936
8625
  const object = viewContainerObjectName(v);
7937
8626
  if (object) containers.push({ object, container: v });
7938
8627
  }
7939
- for (const obj of asArray45(stack.objects)) {
8628
+ for (const obj of asArray44(stack.objects)) {
7940
8629
  const object = typeof obj.name === "string" ? obj.name : void 0;
7941
8630
  if (!object) continue;
7942
8631
  if (obj.list || obj.form || obj.listViews || obj.formViews) {
@@ -7944,7 +8633,7 @@ function lintViewRefs(stack) {
7944
8633
  }
7945
8634
  }
7946
8635
  for (const { object, container } of containers) {
7947
- const { items, collisions } = (0, import_spec3.expandViewContainerWithDiagnostics)(object, container);
8636
+ const { items, collisions } = (0, import_spec5.expandViewContainerWithDiagnostics)(object, container);
7948
8637
  for (const it of items) indexKind(it.name, it.viewKind);
7949
8638
  for (const col of collisions) {
7950
8639
  findings.push({
@@ -7990,11 +8679,11 @@ function lintViewRefs(stack) {
7990
8679
  });
7991
8680
  }
7992
8681
  };
7993
- for (const obj of asArray45(stack.objects)) {
8682
+ for (const obj of asArray44(stack.objects)) {
7994
8683
  const object = typeof obj.name === "string" ? obj.name : void 0;
7995
- for (const action of asArray45(obj.actions)) checkAction(action, object);
8684
+ for (const action of asArray44(obj.actions)) checkAction(action, object);
7996
8685
  }
7997
- for (const action of asArray45(stack.actions)) checkAction(action);
8686
+ for (const action of asArray44(stack.actions)) checkAction(action);
7998
8687
  return findings;
7999
8688
  }
8000
8689
 
@@ -8129,6 +8818,7 @@ var CLI_ONLY = ["cli"];
8129
8818
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
8130
8819
  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.";
8131
8820
  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.";
8821
+ 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.";
8132
8822
  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.";
8133
8823
  var EXPRESSION_INVALID = "expression-invalid";
8134
8824
  var AUTHORING_RULES = [
@@ -8158,8 +8848,12 @@ var AUTHORING_RULES = [
8158
8848
  }))
8159
8849
  },
8160
8850
  // ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
8161
- // mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
8162
- // read the pre-parse tier or the evidence is already gone.
8851
+ // mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
8852
+ // is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
8853
+ // refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
8854
+ // THROWS on both. `normalized` here therefore means "needs no parsed stack"
8855
+ // (so `os lint`, which never parses, can run it), not "sees evidence the
8856
+ // parse would have eaten".
8163
8857
  {
8164
8858
  name: "validateListViewMode",
8165
8859
  tier: "gating",
@@ -8190,9 +8884,13 @@ var AUTHORING_RULES = [
8190
8884
  surfaceReason: RUNTIME_OBJECT_WRITES_P2,
8191
8885
  run: (stack) => validateFunctionalCompleteness(stack)
8192
8886
  },
8193
- // A flat list-view object in `views: []` parses to an EMPTY container
8194
- // (ViewSchema strips unknown keys): the schema step passes, zero views
8195
- // register, and the Console renders nothing. Pre-parse for the same reason.
8887
+ // A view container in `views: []` that registers zero views: nothing appears
8888
+ // in the Console, and the schema step cannot tell it from an intentionally
8889
+ // empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
8890
+ // went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
8891
+ // data }` by name with the wrap-it hint (measured under #6073); the arm that
8892
+ // still needs a rule is the all-slots-empty container, whose keys are all
8893
+ // declared and which survives the parse untouched.
8196
8894
  {
8197
8895
  name: "validateViewContainers",
8198
8896
  tier: "gating",
@@ -8242,6 +8940,31 @@ var AUTHORING_RULES = [
8242
8940
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8243
8941
  run: (stack) => validateFilterTokens(stack)
8244
8942
  },
8943
+ // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
8944
+ // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
8945
+ // this rule does not touch it: it refuses the literal SPELLINGS at authoring
8946
+ // time with a per-shape prescription, which is Prime Directive #12's standard
8947
+ // shape (reject at the producer, never tolerate at the consumer) and #5240's
8948
+ // same-direction precedent one shape over.
8949
+ {
8950
+ name: "validateEmptyCombinators",
8951
+ tier: "gating",
8952
+ input: "parsed",
8953
+ commands: ALL,
8954
+ source: "packages/lint/src/validate-empty-combinators.ts",
8955
+ // The one type #4463's P1 slice opened, and the one this rule most needs:
8956
+ // a flow CRUD node's `config.filter` is where an empty combinator has the
8957
+ // largest blast radius, and the write path is the only door an AI author
8958
+ // uses. This rule needs NO resolution context at all — it judges the filter
8959
+ // literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
8960
+ // it, and widening to the other filter-carrying types (`object`, `view`,
8961
+ // `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
8962
+ // opens them at the gate. Making that call here would widen the gate's
8963
+ // dispatch surface on this rule's authority, which is P2's decision.
8964
+ surfaces: CLI_AND_RUNTIME,
8965
+ runtimeTypes: ["flow"],
8966
+ run: (stack) => validateEmptyCombinators(stack)
8967
+ },
8245
8968
  // The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
8246
8969
  // rules that answer "does this name resolve to anything?". It reached all
8247
8970
  // three commands before this file existed; it is an entry here so the two
@@ -8290,6 +9013,16 @@ var AUTHORING_RULES = [
8290
9013
  // `displayField` (#5775) — so gating today would fail the platform's own pages
8291
9014
  // to enforce declarations the platform does not keep. The error upgrade is a
8292
9015
  // separate step, once the warning-period inventory is empty.
9016
+ //
9017
+ // #5775 settled the record picker's half: `displayField` is retired in favour
9018
+ // of the `labelField` the renderer actually reads. Its claim that "the rest of
9019
+ // the keys the renderers honour are declared" did NOT hold — #6776 found five
9020
+ // more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
9021
+ // `page:accordion.variant`, and the tab strip's visual style, whose declared
9022
+ // spelling `page:tabs.type` collided with the component node's own dispatch
9023
+ // key and so was unauthorable in the flat and JSX carriers). All five are
9024
+ // declared as of #6776, the last as the renamed `tabStyle`. What remains
9025
+ // before the error upgrade is #5728 and two page rewrites.
8293
9026
  {
8294
9027
  name: "validateComponentProps",
8295
9028
  tier: "advisory",
@@ -8368,10 +9101,12 @@ var AUTHORING_RULES = [
8368
9101
  //
8369
9102
  // `gating` since #5762, which reviewed the file's rules as one family and
8370
9103
  // split them on a single question: is THIS STACK enough to know the flow is
8371
- // dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
9104
+ // dead? Four rules answer yes and emit `error` — a `config.timeRelative`
8372
9105
  // the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
8373
- // predicate cannot route at all, and a `record-*` triggerType outside the
8374
- // closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
9106
+ // predicate cannot route at all, a `record-*` triggerType outside the
9107
+ // closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
9108
+ // `type: 'record_change'` flow whose triggerType the engine's binding resolver
9109
+ // routes nowhere, silently demoting it to a manual flow. None of those verdicts
8375
9110
  // can be changed by installing a package, so there is no reading under which
8376
9111
  // the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
8377
9112
  // (the object may come from another installed package — a hedge this rule
@@ -8498,12 +9233,39 @@ var AUTHORING_RULES = [
8498
9233
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8499
9234
  run: (stack) => validateSeedStateMachine(stack)
8500
9235
  },
8501
- // ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
8502
- // Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
8503
- // during parse, so the alias the author wrote is gone from `result.data`.
9236
+ // ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
9237
+ // gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
9238
+ // schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
9239
+ // the alias the author wrote is gone from `result.data`". Measured false at
9240
+ // #6073: the ADR-0087 D2 conversions do that fold INSIDE
9241
+ // `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
9242
+ // alias site the alias-KEY rule reported zero here too.
9243
+ //
9244
+ // #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
9245
+ // re-anchored. Re-anchoring would have had to move this entry's input to a
9246
+ // pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
9247
+ // a change to this package's external input contract, and the maintainer's
9248
+ // call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
9249
+ // costs no author a signal: the same D2 conversion already shouts through
9250
+ // `warnConversionNotice` in `defineStack`, naming the site, the conversion and
9251
+ // the protocol-16 retirement window — better wording than the rule ever had.
9252
+ //
9253
+ // Every rule left in the family judges the predicate's VALUE, and the value
9254
+ // moves into `visibleWhen` intact, so all three report normally on this tier.
9255
+ // The tier therefore stays `normalized` on its SURVIVING justification (a
9256
+ // finding still reaches the author when an unrelated schema error would stop
9257
+ // the parse — see `AuthoringRuleInputTier`), never on the retired
9258
+ // "pre-parse evidence" one.
9259
+ //
9260
+ // `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
9261
+ // ADR-0089 rules stay advisory findings within it — the tier is a property of
9262
+ // the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
9263
+ // what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
9264
+ // has worked since #3760. The promotion follows the #5762 precedent: a family
9265
+ // that gains an `error` finding moves its registry tier in the same edit.
8504
9266
  {
8505
9267
  name: "validateVisibilityPredicates",
8506
- tier: "advisory",
9268
+ tier: "gating",
8507
9269
  input: "normalized",
8508
9270
  commands: ALL,
8509
9271
  source: "packages/lint/src/validate-visibility-predicates.ts",
@@ -8511,6 +9273,30 @@ var AUTHORING_RULES = [
8511
9273
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
8512
9274
  run: (stack) => validateVisibilityPredicates(stack)
8513
9275
  },
9276
+ // #7010 — the same predicate surface, one question further in. The three
9277
+ // ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
9278
+ // rooted, is the root right for the layer) and never open the target schema,
9279
+ // so `data.tpye == 'formula'` passes all three and still resolves to nothing.
9280
+ // This rule resolves the PATH against the schema the form edits — the closed
9281
+ // `getMetadataTypeSchema` key set — and is therefore immune to the CEL
9282
+ // type-name blind spot that made #6248's gate structurally unable to catch
9283
+ // #6254's 16 bare `type ==` predicates.
9284
+ //
9285
+ // Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
9286
+ // the `record.*` layer is deliberately out of scope because an ObjectQL
9287
+ // object's addressable path set is NOT closed (lookup traversal, system
9288
+ // columns, formula outputs), and an `error` gate over an open set generates
9289
+ // false build errors. See the rule's module note.
9290
+ {
9291
+ name: "validatePredicatePathRefs",
9292
+ tier: "gating",
9293
+ input: "normalized",
9294
+ commands: ALL,
9295
+ source: "packages/lint/src/validate-predicate-path-refs.ts",
9296
+ surfaces: CLI_ONLY,
9297
+ surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
9298
+ run: (stack) => validatePredicatePathRefs(stack)
9299
+ },
8514
9300
  // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
8515
9301
  // `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
8516
9302
  // runtime REFUSES to execute), plus `flow-branch-label-unmatched` and