@objectstack/lint 17.0.0-rc.1 → 17.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -176,7 +176,7 @@ function validateWidgetBindings(stack) {
176
176
  const where = `dashboard "${dashName}" \u203A widget "${widgetId}"`;
177
177
  const path = `dashboards[${i}].widgets[${j}]`;
178
178
  const suppressed = (rule) => Array.isArray(w.suppressWarnings) && w.suppressWarnings.includes(rule);
179
- const push = (f) => {
179
+ const push2 = (f) => {
180
180
  if (f.severity === "warning" && suppressed(f.rule)) return;
181
181
  findings.push({ ...f, where, path });
182
182
  };
@@ -188,14 +188,14 @@ function validateWidgetBindings(stack) {
188
188
  const plural = legacyUsed.length > 1;
189
189
  const datasetHint = `Bind a semantic dataset and select fields BY NAME: \`dataset: '<name>', dimensions: [...], values: [...]\`. Dataset-bound widgets render through DatasetWidget (pivot rows/cols come from \`dimensions\`, cell values from \`values\`).`;
190
190
  if (!hasDataSource) {
191
- push({
191
+ push2({
192
192
  severity: "error",
193
193
  rule: WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
194
194
  message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} (removed by the ADR-0021 single-form cutover) and binds no data source (no \`dataset\`, \`object\`, or inline \`data\`) \u2014 it renders nothing.`,
195
195
  hint: `${datasetHint} The renderer ignores the legacy keys, so without a data source this widget has no data at all.`
196
196
  });
197
197
  } else {
198
- push({
198
+ push2({
199
199
  severity: "warning",
200
200
  rule: WIDGET_LEGACY_ANALYTICS_SHAPE,
201
201
  message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} that the ADR-0021 single-form cutover removed \u2014 the dashboard renderer ignores ${plural ? "them" : "it"}.`,
@@ -206,7 +206,7 @@ function validateWidgetBindings(stack) {
206
206
  const dsName = typeof w.dataset === "string" ? w.dataset : void 0;
207
207
  const dataset = dsName ? datasets.get(dsName) : void 0;
208
208
  if (dsName && !dataset) {
209
- push({
209
+ push2({
210
210
  severity: "error",
211
211
  rule: WIDGET_DATASET_UNKNOWN,
212
212
  message: `dataset "${dsName}" does not resolve to a declared dataset.`,
@@ -214,7 +214,7 @@ function validateWidgetBindings(stack) {
214
214
  });
215
215
  }
216
216
  if (!dsName) {
217
- push({
217
+ push2({
218
218
  severity: "error",
219
219
  rule: WIDGET_DATASET_UNKNOWN,
220
220
  message: `binds no \`dataset\` \u2014 the ADR-0021 widget shape requires one, so this widget resolves no data and renders empty.`,
@@ -233,7 +233,7 @@ function validateWidgetBindings(stack) {
233
233
  const field = eff.field;
234
234
  if (field.includes(".")) continue;
235
235
  if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
236
- push({
236
+ push2({
237
237
  severity: "error",
238
238
  rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
239
239
  message: eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.` : `inherits dashboard filter \`${def.name}(${field})\`, but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.`,
@@ -253,7 +253,7 @@ function validateWidgetBindings(stack) {
253
253
  const dims = asStrings(w.dimensions);
254
254
  for (let k = 0; k < dims.length; k++) {
255
255
  if (dimensionNames.has(dims[k])) continue;
256
- push({
256
+ push2({
257
257
  severity: "error",
258
258
  rule: WIDGET_DIMENSION_UNKNOWN,
259
259
  message: `dimensions[${k}] "${dims[k]}" is not a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
@@ -263,7 +263,7 @@ function validateWidgetBindings(stack) {
263
263
  const values = asStrings(w.values);
264
264
  for (let k = 0; k < values.length; k++) {
265
265
  if (measures.has(values[k])) continue;
266
- push({
266
+ push2({
267
267
  severity: "error",
268
268
  rule: WIDGET_MEASURE_UNKNOWN,
269
269
  message: `values[${k}] "${values[k]}" is not a measure of dataset "${dsName}" (declared measures: ${list(measures.keys())}).`,
@@ -276,7 +276,7 @@ function validateWidgetBindings(stack) {
276
276
  const selectedValues = new Set(values.filter((v) => measures.has(v)));
277
277
  const xAxis = chartConfig.xAxis && typeof chartConfig.xAxis === "object" ? chartConfig.xAxis : void 0;
278
278
  if (xAxis && typeof xAxis.field === "string" && !dimensionNames.has(xAxis.field) && !dims.includes(xAxis.field)) {
279
- push({
279
+ push2({
280
280
  severity: "error",
281
281
  rule: CHART_FIELD_UNKNOWN,
282
282
  message: `chartConfig.xAxis.field "${xAxis.field}" does not resolve to a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
@@ -286,7 +286,7 @@ function validateWidgetBindings(stack) {
286
286
  const measureField = (label2, field) => {
287
287
  if (values.includes(field)) return;
288
288
  const declaredButUnselected = measures.has(field);
289
- push({
289
+ push2({
290
290
  severity: "error",
291
291
  rule: CHART_FIELD_UNKNOWN,
292
292
  message: declaredButUnselected ? `chartConfig.${label2} "${field}" is a measure of dataset "${dsName}" but is not selected in the widget's values (${list(values)}), so the query result will not contain it.` : `chartConfig.${label2} "${field}" does not resolve to a measure of dataset "${dsName}" (declared measures: ${list(measures.keys())}).`,
@@ -304,7 +304,7 @@ function validateWidgetBindings(stack) {
304
304
  if (typeof name === "string") measureField(`series[${k}].name`, name);
305
305
  }
306
306
  } else if (isChartType) {
307
- push({
307
+ push2({
308
308
  severity: "warning",
309
309
  rule: CHART_CONFIG_MISSING,
310
310
  message: `chart-type widget ('${w.type}') has no chartConfig \u2014 the renderer cannot determine which measure to plot, so the series renders empty.`,
@@ -318,7 +318,7 @@ function validateWidgetBindings(stack) {
318
318
  if (resolved.some((m) => !m)) continue;
319
319
  const countOnly = resolved.every((m) => m.aggregate === "count" && !m.derived);
320
320
  if (!countOnly) continue;
321
- push({
321
+ push2({
322
322
  severity: "warning",
323
323
  rule: TABLE_COUNT_ONLY,
324
324
  message: `a '${w.type}' widget bound to dataset "${dsName}" selects only count measure(s) (${values.join(", ")}) and no dimensions, so it renders a single summary row \u2014 not a per-record list.`,
@@ -332,6 +332,226 @@ function validateWidgetBindings(stack) {
332
332
  // src/validate-expressions.ts
333
333
  import { validateExpression } from "@objectstack/formula";
334
334
  import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
335
+
336
+ // src/validate-null-guards.ts
337
+ import { Environment } from "@marcbachmann/cel-js";
338
+ var NULL_GUARD_HINT = `Guard it with '!= null' \u2014 'has(x)' does NOT do that: a declared field holding null is still PRESENT, so has(x) is true.`;
339
+ var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
340
+ var DEFAULT_RECORD_ROOTS = ["record", "previous"];
341
+ var parseEnv;
342
+ function getParseEnv() {
343
+ if (!parseEnv) {
344
+ parseEnv = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
345
+ }
346
+ return parseEnv;
347
+ }
348
+ function isNode(v) {
349
+ return !!v && typeof v === "object" && typeof v.op === "string";
350
+ }
351
+ function isNullLiteral(node) {
352
+ return isNode(node) && node.op === "value" && node.args === null;
353
+ }
354
+ function fieldOf(node, roots) {
355
+ if (!isNode(node)) return null;
356
+ if (node.op !== "." && node.op !== ".?") return null;
357
+ const args = node.args;
358
+ if (!Array.isArray(args) || args.length < 2) return null;
359
+ const [recv, seg] = args;
360
+ if (typeof seg !== "string") return null;
361
+ if (!isNode(recv) || recv.op !== "id") return null;
362
+ const root = recv.args;
363
+ if (typeof root !== "string" || !roots.includes(root)) return null;
364
+ return { operand: `${root}.${seg}`, field: seg };
365
+ }
366
+ function childNodes(node) {
367
+ const args = node.args;
368
+ if (isNode(args)) return [args];
369
+ if (!Array.isArray(args)) return [];
370
+ const out = [];
371
+ for (const a of args) {
372
+ if (isNode(a)) out.push(a);
373
+ else if (Array.isArray(a)) {
374
+ for (const b of a) if (isNode(b)) out.push(b);
375
+ }
376
+ }
377
+ return out;
378
+ }
379
+ function callName(node) {
380
+ if (node.op !== "call") return null;
381
+ const args = node.args;
382
+ if (!Array.isArray(args) || typeof args[0] !== "string") return null;
383
+ return args[0];
384
+ }
385
+ function callArgs(node) {
386
+ const args = node.args;
387
+ if (!Array.isArray(args) || !Array.isArray(args[1])) return [];
388
+ return args[1];
389
+ }
390
+ function union(a, b) {
391
+ return /* @__PURE__ */ new Set([...a, ...b]);
392
+ }
393
+ function intersect(a, b) {
394
+ const out = /* @__PURE__ */ new Set();
395
+ for (const v of a) if (b.has(v)) out.add(v);
396
+ return out;
397
+ }
398
+ function truthGuards(node, roots) {
399
+ if (!isNode(node)) return /* @__PURE__ */ new Set();
400
+ switch (node.op) {
401
+ case "!=": {
402
+ const [l, r] = node.args ?? [];
403
+ if (isNullLiteral(r)) {
404
+ const f = fieldOf(l, roots);
405
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
406
+ }
407
+ if (isNullLiteral(l)) {
408
+ const f = fieldOf(r, roots);
409
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
410
+ }
411
+ return /* @__PURE__ */ new Set();
412
+ }
413
+ case "&&": {
414
+ const [l, r] = node.args;
415
+ return union(truthGuards(l, roots), truthGuards(r, roots));
416
+ }
417
+ case "||": {
418
+ const [l, r] = node.args;
419
+ return intersect(truthGuards(l, roots), truthGuards(r, roots));
420
+ }
421
+ case "!_":
422
+ return falseGuards(node.args, roots);
423
+ case "?:": {
424
+ const [, t, f] = node.args;
425
+ return intersect(truthGuards(t, roots), truthGuards(f, roots));
426
+ }
427
+ default:
428
+ return /* @__PURE__ */ new Set();
429
+ }
430
+ }
431
+ function falseGuards(node, roots) {
432
+ if (!isNode(node)) return /* @__PURE__ */ new Set();
433
+ switch (node.op) {
434
+ case "==": {
435
+ const [l, r] = node.args ?? [];
436
+ if (isNullLiteral(r)) {
437
+ const f = fieldOf(l, roots);
438
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
439
+ }
440
+ if (isNullLiteral(l)) {
441
+ const f = fieldOf(r, roots);
442
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
443
+ }
444
+ return /* @__PURE__ */ new Set();
445
+ }
446
+ case "||": {
447
+ const [l, r] = node.args;
448
+ return union(falseGuards(l, roots), falseGuards(r, roots));
449
+ }
450
+ case "&&": {
451
+ const [l, r] = node.args;
452
+ return intersect(falseGuards(l, roots), falseGuards(r, roots));
453
+ }
454
+ case "!_":
455
+ return truthGuards(node.args, roots);
456
+ case "call": {
457
+ if (callName(node) !== "isBlank") return /* @__PURE__ */ new Set();
458
+ const [only] = callArgs(node);
459
+ const f = fieldOf(only, roots);
460
+ return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
461
+ }
462
+ default:
463
+ return /* @__PURE__ */ new Set();
464
+ }
465
+ }
466
+ function collectHasOperands(node, roots, out) {
467
+ if (!isNode(node)) return;
468
+ if (callName(node) === "has") {
469
+ for (const a of callArgs(node)) {
470
+ const f = fieldOf(a, roots);
471
+ if (f) out.add(f.operand);
472
+ }
473
+ }
474
+ for (const child of childNodes(node)) collectHasOperands(child, roots, out);
475
+ }
476
+ function findUnguardedNullableOperands(source, opts) {
477
+ if (typeof source !== "string" || !source.trim()) return [];
478
+ if (opts.nullableFields.size === 0) return [];
479
+ const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
480
+ let ast;
481
+ try {
482
+ ast = getParseEnv().parse(source).ast;
483
+ } catch {
484
+ return [];
485
+ }
486
+ const hasOperands = /* @__PURE__ */ new Set();
487
+ collectHasOperands(ast, roots, hasOperands);
488
+ const findings = [];
489
+ const seen = /* @__PURE__ */ new Set();
490
+ const report = (operandNode, operator, guards) => {
491
+ const f = fieldOf(operandNode, roots);
492
+ if (!f) return;
493
+ if (!opts.nullableFields.has(f.field)) return;
494
+ if (guards.has(f.operand)) return;
495
+ const key = `${f.operand}\0${operator}`;
496
+ if (seen.has(key)) return;
497
+ seen.add(key);
498
+ findings.push({
499
+ operand: f.operand,
500
+ field: f.field,
501
+ operator,
502
+ hasOnlyGuard: hasOperands.has(f.operand)
503
+ });
504
+ };
505
+ const visit = (node, guards) => {
506
+ if (!isNode(node)) return;
507
+ const op = node.op;
508
+ if (op === "&&") {
509
+ const [l, r] = node.args;
510
+ visit(l, guards);
511
+ visit(r, union(guards, truthGuards(l, roots)));
512
+ return;
513
+ }
514
+ if (op === "||") {
515
+ const [l, r] = node.args;
516
+ visit(l, guards);
517
+ visit(r, union(guards, falseGuards(l, roots)));
518
+ return;
519
+ }
520
+ if (op === "?:") {
521
+ const [c, t, f] = node.args;
522
+ visit(c, guards);
523
+ visit(t, union(guards, truthGuards(c, roots)));
524
+ visit(f, union(guards, falseGuards(c, roots)));
525
+ return;
526
+ }
527
+ if (op === "call" && callName(node) === "has") {
528
+ return;
529
+ }
530
+ if (FAULTING_BINARY_OPS.has(op)) {
531
+ const [l, r] = node.args;
532
+ report(l, op, guards);
533
+ report(r, op, guards);
534
+ visit(l, guards);
535
+ visit(r, guards);
536
+ return;
537
+ }
538
+ if (op === "-_") {
539
+ report(node.args, "-", guards);
540
+ visit(node.args, guards);
541
+ return;
542
+ }
543
+ for (const child of childNodes(node)) visit(child, guards);
544
+ };
545
+ visit(ast, /* @__PURE__ */ new Set());
546
+ return findings;
547
+ }
548
+ function nullGuardMessage(subject, objectName, finding) {
549
+ const owner = objectName ? `'${objectName}'` : "this object";
550
+ const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
551
+ return `${subject} applies \`${finding.operator}\` to \`${finding.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding.operator}\` overload for null, and the whole predicate aborts \u2014 so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763). The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
552
+ }
553
+
554
+ // src/validate-expressions.ts
335
555
  function asArray2(v) {
336
556
  if (Array.isArray(v)) return v;
337
557
  if (v && typeof v === "object") {
@@ -375,11 +595,84 @@ function buildFieldTypeIndex(objects) {
375
595
  }
376
596
  return idx;
377
597
  }
598
+ function fieldEntries(obj) {
599
+ const fields = obj.fields;
600
+ if (Array.isArray(fields)) {
601
+ return fields.filter((f) => f && typeof f === "object" && typeof f.name === "string").map((f) => [f.name, f]);
602
+ }
603
+ if (fields && typeof fields === "object") {
604
+ return Object.entries(fields).filter(([, def]) => !!def && typeof def === "object").map(([n, def]) => [n, def]);
605
+ }
606
+ return [];
607
+ }
608
+ function isNullableField(def) {
609
+ if (def.required === true) return false;
610
+ if (def.defaultValue !== void 0 && def.defaultValue !== null) return false;
611
+ if (def.type === "autonumber") return false;
612
+ const options = def.options;
613
+ if (Array.isArray(options) && options.some((o) => !!o && typeof o === "object" && o.default === true)) {
614
+ return false;
615
+ }
616
+ return true;
617
+ }
618
+ function buildNullableFieldIndex(objects) {
619
+ const idx = /* @__PURE__ */ new Map();
620
+ for (const obj of objects) {
621
+ const name = typeof obj.name === "string" ? obj.name : void 0;
622
+ if (!name) continue;
623
+ const nullable = /* @__PURE__ */ new Set();
624
+ for (const [fname, def] of fieldEntries(obj)) {
625
+ if (isNullableField(def)) nullable.add(fname);
626
+ }
627
+ idx.set(name, nullable);
628
+ }
629
+ return idx;
630
+ }
631
+ function celSourceOf(raw) {
632
+ if (typeof raw === "string") return raw;
633
+ if (raw && typeof raw === "object") {
634
+ const rec = raw;
635
+ if (typeof rec.dialect === "string" && rec.dialect !== "cel") return void 0;
636
+ if (typeof rec.source === "string") return rec.source;
637
+ }
638
+ return void 0;
639
+ }
640
+ function rulePredicates(rule, path) {
641
+ const out = [];
642
+ const name = typeof rule.name === "string" ? rule.name : "?";
643
+ const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
644
+ const main = rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula;
645
+ if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
646
+ if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
647
+ for (const branch of ["then", "otherwise"]) {
648
+ const nested = rule[branch];
649
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
650
+ out.push(...rulePredicates(nested, `${here} ${branch}`));
651
+ }
652
+ }
653
+ return out;
654
+ }
378
655
  function validateStackExpressions(stack) {
379
656
  const issues = [];
380
657
  const objects = asArray2(stack.objects);
381
658
  const fieldIndex = buildFieldIndex(objects);
382
659
  const fieldTypeIndex = buildFieldTypeIndex(objects);
660
+ const nullableIndex = buildNullableFieldIndex(objects);
661
+ const checkNullGuards = (where, subject, raw, objectName) => {
662
+ if (!objectName) return;
663
+ const nullableFields = nullableIndex.get(objectName);
664
+ if (!nullableFields || nullableFields.size === 0) return;
665
+ const source = celSourceOf(raw);
666
+ if (!source) return;
667
+ for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
668
+ issues.push({
669
+ where,
670
+ message: nullGuardMessage(subject, objectName, finding),
671
+ source,
672
+ severity: "error"
673
+ });
674
+ }
675
+ };
383
676
  const check = (where, raw, objectName, scope = "flattened") => {
384
677
  if (raw == null) return;
385
678
  const fields = objectName ? fieldIndex.get(objectName) : void 0;
@@ -420,17 +713,17 @@ function validateStackExpressions(stack) {
420
713
  if (node.type === "script") {
421
714
  const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
422
715
  const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
423
- const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
424
- if (!fn && !action && !inline) {
716
+ const retired = ["actionType", "template", "recipients", "variables", "script"].filter((k) => cfg[k] != null);
717
+ if (retired.length > 0) {
425
718
  issues.push({
426
719
  where: `${at} \xB7 node '${node.id}' (script) callable`,
427
- message: `script node declares neither \`actionType\` nor \`function\` \u2014 it would do nothing at runtime. Name a built-in action (e.g. \`actionType: 'email'\`) or a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
720
+ 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.`,
428
721
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
429
722
  });
430
- } else if (action === "invoke_function" && !fn) {
723
+ } else if (!fn) {
431
724
  issues.push({
432
725
  where: `${at} \xB7 node '${node.id}' (script) callable`,
433
- message: `script node uses \`actionType: 'invoke_function'\` but no \`function\` (or \`functionName\`) \u2014 it names no callable. Set \`function: 'my_fn'\` and register it via \`defineStack({ functions })\`.`,
726
+ message: `script node declares no \`function\` \u2014 it would do nothing at runtime. Name a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
434
727
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
435
728
  });
436
729
  }
@@ -448,6 +741,9 @@ function validateStackExpressions(stack) {
448
741
  const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
449
742
  check(where, rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula, objectName, "record");
450
743
  check(`${where} when`, rule.when, objectName, "record");
744
+ for (const p of rulePredicates(rule, "")) {
745
+ checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
746
+ }
451
747
  }
452
748
  const fields = obj.fields;
453
749
  const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
@@ -500,6 +796,12 @@ function validateStackExpressions(stack) {
500
796
  const hookName = hook.name ?? "?";
501
797
  if (typeof hook.object === "string") {
502
798
  check(`hook '${hookName}' (${hook.object}) condition`, hook.condition, hook.object, "record");
799
+ checkNullGuards(
800
+ `hook '${hookName}' (${hook.object}) condition`,
801
+ `hook '${hookName}' condition`,
802
+ hook.condition,
803
+ hook.object
804
+ );
503
805
  continue;
504
806
  }
505
807
  const targets = Array.isArray(hook.object) ? hook.object.filter((o) => typeof o === "string" && o !== "*") : [];
@@ -513,6 +815,12 @@ function validateStackExpressions(stack) {
513
815
  for (const target of targets) {
514
816
  const mark = issues.length;
515
817
  check(`hook '${hookName}' (${target}) condition`, hook.condition, target, "record");
818
+ checkNullGuards(
819
+ `hook '${hookName}' (${target}) condition`,
820
+ `hook '${hookName}' condition`,
821
+ hook.condition,
822
+ target
823
+ );
516
824
  for (let i = mark; i < issues.length; i++) {
517
825
  const issue = issues[i];
518
826
  const key = `${issue.message}\0${issue.source ?? ""}`;
@@ -594,6 +902,76 @@ function validateListViewMode(stack) {
594
902
  return out;
595
903
  }
596
904
 
905
+ // src/validate-functional-completeness.ts
906
+ import {
907
+ checkFieldCompleteness,
908
+ checkViewCompleteness,
909
+ checkWebhookCompleteness
910
+ } from "@objectstack/spec/kernel";
911
+ var isRec = (v) => !!v && typeof v === "object" && !Array.isArray(v);
912
+ function entriesOf(v) {
913
+ if (Array.isArray(v)) {
914
+ return v.flatMap(
915
+ (def, i) => isRec(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
916
+ );
917
+ }
918
+ if (isRec(v)) {
919
+ return Object.entries(v).flatMap(
920
+ ([name, def]) => isRec(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
921
+ );
922
+ }
923
+ return [];
924
+ }
925
+ function push(out, found, where, basePath) {
926
+ for (const f of found) {
927
+ out.push({
928
+ severity: f.severity,
929
+ rule: f.rule,
930
+ where,
931
+ path: `${basePath}.${f.path}`,
932
+ message: f.message,
933
+ hint: f.fix
934
+ });
935
+ }
936
+ }
937
+ function validateFunctionalCompleteness(stack) {
938
+ const out = [];
939
+ if (!isRec(stack)) return out;
940
+ for (const [oi, obj] of entriesOf(stack.objects).entries()) {
941
+ for (const field of entriesOf(obj.def.fields)) {
942
+ push(
943
+ out,
944
+ checkFieldCompleteness(field.def),
945
+ `object "${obj.name}" \u203A fields.${field.name}`,
946
+ `objects[${oi}].fields${field.key}`
947
+ );
948
+ }
949
+ }
950
+ for (const [vi, container] of entriesOf(stack.views).entries()) {
951
+ const where = container.def.object ? `view container "${container.name}"` : `view container [${vi}]`;
952
+ if (isRec(container.def.list)) {
953
+ push(out, checkViewCompleteness(container.def.list), `${where} \u203A list`, `views[${vi}].list`);
954
+ }
955
+ for (const lv of entriesOf(container.def.listViews)) {
956
+ push(
957
+ out,
958
+ checkViewCompleteness(lv.def),
959
+ `${where} \u203A listViews.${lv.name}`,
960
+ `views[${vi}].listViews${lv.key}`
961
+ );
962
+ }
963
+ }
964
+ for (const hook of entriesOf(stack.webhooks)) {
965
+ push(
966
+ out,
967
+ checkWebhookCompleteness(hook.def),
968
+ `webhook "${hook.name}"`,
969
+ `webhooks${hook.key}`
970
+ );
971
+ }
972
+ return out;
973
+ }
974
+
597
975
  // src/validate-flow-trigger-readiness.ts
598
976
  var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
599
977
  var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
@@ -693,7 +1071,7 @@ function validateFlowTriggerReadiness(stack) {
693
1071
 
694
1072
  // src/flow-walk.ts
695
1073
  import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
696
- function isRec(v) {
1074
+ function isRec2(v) {
697
1075
  return !!v && typeof v === "object" && !Array.isArray(v);
698
1076
  }
699
1077
  function strName(v) {
@@ -708,7 +1086,7 @@ function flowNodeLabel(node, index) {
708
1086
  return strName(node.label) ?? strName(node.id) ?? `#${index}`;
709
1087
  }
710
1088
  function stripRegions(config) {
711
- if (!isRec(config)) return void 0;
1089
+ if (!isRec2(config)) return void 0;
712
1090
  let out;
713
1091
  for (const key of Object.keys(config)) {
714
1092
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -719,11 +1097,11 @@ function stripRegions(config) {
719
1097
  }
720
1098
  function walkFlowNodes(flow, flowPath) {
721
1099
  const out = [];
722
- if (!isRec(flow)) return out;
1100
+ if (!isRec2(flow)) return out;
723
1101
  const visitList = (nodes, basePath, trail, depth) => {
724
1102
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
725
1103
  nodes.forEach((raw, index) => {
726
- if (!isRec(raw)) return;
1104
+ if (!isRec2(raw)) return;
727
1105
  const path = `${basePath}[${index}]`;
728
1106
  out.push({
729
1107
  node: raw,
@@ -734,7 +1112,7 @@ function walkFlowNodes(flow, flowPath) {
734
1112
  });
735
1113
  const type = strName(raw.type);
736
1114
  const slots = type ? REGION_SLOTS.get(type) : void 0;
737
- if (!slots || !isRec(raw.config)) return;
1115
+ if (!slots || !isRec2(raw.config)) return;
738
1116
  const config = raw.config;
739
1117
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
740
1118
  for (const slot of slots) {
@@ -742,7 +1120,7 @@ function walkFlowNodes(flow, flowPath) {
742
1120
  if (slot === "branches") {
743
1121
  if (!Array.isArray(value)) continue;
744
1122
  value.forEach((branch, b) => {
745
- if (!isRec(branch)) return;
1123
+ if (!isRec2(branch)) return;
746
1124
  const branchName = strName(branch.name) ?? `#${b}`;
747
1125
  visitList(
748
1126
  branch.nodes,
@@ -753,7 +1131,7 @@ function walkFlowNodes(flow, flowPath) {
753
1131
  });
754
1132
  continue;
755
1133
  }
756
- if (!isRec(value)) continue;
1134
+ if (!isRec2(value)) continue;
757
1135
  visitList(
758
1136
  value.nodes,
759
1137
  `${path}.config.${slot}.nodes`,
@@ -1500,11 +1878,24 @@ function validateReactPages(stack) {
1500
1878
 
1501
1879
  // src/validate-react-page-props.ts
1502
1880
  import { createRequire as createRequire2 } from "module";
1503
- import { REACT_BLOCKS, chartAggregateResultKeys } from "@objectstack/spec/ui";
1881
+ import {
1882
+ REACT_BLOCKS,
1883
+ RECORD_CONTEXT_BLOCK_TAGS,
1884
+ REACT_RECORD_BLOCK_ALTERNATIVES,
1885
+ chartAggregateResultKeys,
1886
+ isRecordContextBlockType
1887
+ } from "@objectstack/spec/ui";
1504
1888
  import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
1505
1889
 
1506
1890
  // src/validate-searchable-fields.ts
1891
+ import {
1892
+ resolveSearchFieldResolution,
1893
+ SEARCHABLE_TEXTUAL_TYPES,
1894
+ SEARCHABLE_ENUM_TYPES,
1895
+ SEARCH_AUTO_EXCLUDED_FIELDS
1896
+ } from "@objectstack/spec/data";
1507
1897
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1898
+ var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1508
1899
  function asArray10(v) {
1509
1900
  if (Array.isArray(v)) return v;
1510
1901
  if (v && typeof v === "object") {
@@ -1512,21 +1903,50 @@ function asArray10(v) {
1512
1903
  }
1513
1904
  return [];
1514
1905
  }
1515
- function isRec2(v) {
1906
+ function isRec3(v) {
1516
1907
  return !!v && typeof v === "object" && !Array.isArray(v);
1517
1908
  }
1518
1909
  function strName2(v) {
1519
1910
  return typeof v === "string" && v.length > 0 ? v : void 0;
1520
1911
  }
1521
- function declaredFieldNames(obj) {
1912
+ function declaredFieldTarget(obj) {
1522
1913
  const fields = obj.fields;
1523
1914
  if (!fields || typeof fields !== "object") return null;
1524
1915
  const names = /* @__PURE__ */ new Set();
1916
+ const metas = {};
1525
1917
  for (const f of asArray10(fields)) {
1526
1918
  const n = strName2(f.name);
1527
- if (n) names.add(n);
1919
+ if (!n) continue;
1920
+ names.add(n);
1921
+ metas[n] = {
1922
+ type: typeof f.type === "string" ? f.type : void 0,
1923
+ hidden: f.hidden === true
1924
+ };
1528
1925
  }
1529
- return names.size > 0 ? names : null;
1926
+ if (names.size === 0) return null;
1927
+ const searchableFields = Array.isArray(obj.searchableFields) ? obj.searchableFields.filter((e) => typeof e === "string") : void 0;
1928
+ return {
1929
+ names,
1930
+ fields: metas,
1931
+ searchableFields,
1932
+ displayField: strName2(obj.nameField) ?? strName2(obj.displayNameField)
1933
+ };
1934
+ }
1935
+ function resolveAllowedSet(target) {
1936
+ let fields = target.fields;
1937
+ const systemDeclared = (target.searchableFields ?? []).filter(
1938
+ (f) => !target.names.has(f) && SYSTEM_FIELDS.has(f)
1939
+ );
1940
+ if (systemDeclared.length > 0) {
1941
+ fields = { ...fields };
1942
+ for (const f of systemDeclared) fields[f] = {};
1943
+ }
1944
+ const { allowed, source } = resolveSearchFieldResolution({
1945
+ fields,
1946
+ searchableFields: target.searchableFields,
1947
+ displayField: target.displayField
1948
+ });
1949
+ return { allowed: new Set(allowed), source, declaredList: allowed };
1530
1950
  }
1531
1951
  function suggest2(target, known) {
1532
1952
  let best;
@@ -1559,50 +1979,87 @@ function distance(a, b) {
1559
1979
  }
1560
1980
  function indexObjectSearchTargets(stack) {
1561
1981
  const fieldsByObject = /* @__PURE__ */ new Map();
1562
- if (!isRec2(stack)) return fieldsByObject;
1982
+ if (!isRec3(stack)) return fieldsByObject;
1563
1983
  for (const obj of asArray10(stack.objects)) {
1564
1984
  const name = strName2(obj.name);
1565
- if (name) fieldsByObject.set(name, declaredFieldNames(obj));
1985
+ if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
1566
1986
  }
1567
1987
  return fieldsByObject;
1568
1988
  }
1569
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
1989
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
1570
1990
  const findings = [];
1571
1991
  if (!Array.isArray(declared) || declared.length === 0) return findings;
1572
1992
  if (!objectName) return findings;
1573
1993
  if (!fieldsByObject.has(objectName)) return findings;
1574
- const known = fieldsByObject.get(objectName);
1575
- if (!known) return findings;
1994
+ const target = fieldsByObject.get(objectName);
1995
+ if (!target) return findings;
1996
+ const known = target.names;
1997
+ const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
1576
1998
  for (let i = 0; i < declared.length; i++) {
1577
1999
  const entry = declared[i];
1578
2000
  const name = strName2(entry);
1579
2001
  if (!name) continue;
1580
- if (known.has(name) || SYSTEM_FIELDS.has(name)) continue;
1581
- const dotted = name.includes(".");
2002
+ if (!known.has(name) && !SYSTEM_FIELDS.has(name)) {
2003
+ const dotted = name.includes(".");
2004
+ findings.push({
2005
+ severity: "error",
2006
+ rule: SEARCHABLE_FIELD_UNKNOWN,
2007
+ where,
2008
+ path: `${path}[${i}]`,
2009
+ 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 ? "" : suggest2(name, known)),
2010
+ 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(", ")}.` : "")
2011
+ });
2012
+ continue;
2013
+ }
2014
+ if (!resolution || resolution.allowed.has(name)) continue;
2015
+ if (!known.has(name)) continue;
2016
+ const meta = target.fields[name];
2017
+ if (resolution.source === "declared") {
2018
+ findings.push({
2019
+ severity: "error",
2020
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
2021
+ where,
2022
+ path: `${path}[${i}]`,
2023
+ message: `${subject} entry "${name}" is outside object "${objectName}"'s declared searchableFields (${resolution.declaredList.join(", ")}) \u2014 the set 'search' scans. Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses an entry outside the allowed set: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
2024
+ hint: `Add "${name}" to ${objectName}.searchableFields, or drop it from this view \u2014 a view narrows the object's searchable set, never widens it (ADR-0061).`
2025
+ });
2026
+ continue;
2027
+ }
2028
+ const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
2029
+ let why;
2030
+ if (SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
2031
+ why = "a system/audit column, which the auto-default set never includes";
2032
+ } else if (meta?.hidden) {
2033
+ why = "hidden";
2034
+ } else if (typeof meta?.type === "string") {
2035
+ why = `of type '${meta.type}', which 'search' cannot scan`;
2036
+ } else {
2037
+ continue;
2038
+ }
1582
2039
  findings.push({
1583
2040
  severity: "error",
1584
- rule: SEARCHABLE_FIELD_UNKNOWN,
2041
+ rule: SEARCHABLE_FIELD_UNSEARCHABLE,
1585
2042
  where,
1586
2043
  path: `${path}[${i}]`,
1587
- 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 ? "" : suggest2(name, known)),
1588
- 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(", ")}.` : "")
2044
+ message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
2045
+ 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.`
1589
2046
  });
1590
2047
  }
1591
2048
  return findings;
1592
2049
  }
1593
2050
  function validateSearchableFields(stack) {
1594
2051
  const findings = [];
1595
- if (!isRec2(stack)) return findings;
2052
+ if (!isRec3(stack)) return findings;
1596
2053
  const objects = asArray10(stack.objects);
1597
2054
  const fieldsByObject = indexObjectSearchTargets(stack);
1598
- const check = (declared, objectName, where, path, subject) => {
2055
+ const check = (declared, objectName, where, path, subject, role) => {
1599
2056
  findings.push(
1600
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
2057
+ ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
1601
2058
  );
1602
2059
  };
1603
2060
  for (let oi = 0; oi < objects.length; oi++) {
1604
2061
  const obj = objects[oi];
1605
- if (!isRec2(obj)) continue;
2062
+ if (!isRec3(obj)) continue;
1606
2063
  const objName = strName2(obj.name);
1607
2064
  const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
1608
2065
  check(
@@ -1610,11 +2067,12 @@ function validateSearchableFields(stack) {
1610
2067
  objName,
1611
2068
  label2,
1612
2069
  `objects[${oi}].searchableFields`,
1613
- "searchableFields"
2070
+ "searchableFields",
2071
+ "canonical"
1614
2072
  );
1615
- if (isRec2(obj.listViews)) {
2073
+ if (isRec3(obj.listViews)) {
1616
2074
  for (const [key, lv] of Object.entries(obj.listViews)) {
1617
- if (!isRec2(lv)) continue;
2075
+ if (!isRec3(lv)) continue;
1618
2076
  check(
1619
2077
  lv.searchableFields,
1620
2078
  // A built-in list view belongs to its object; an inline `data.object`
@@ -1622,7 +2080,8 @@ function validateSearchableFields(stack) {
1622
2080
  listViewObject(lv) ?? objName,
1623
2081
  `${label2} \u203A listViews.${key}`,
1624
2082
  `objects[${oi}].listViews.${key}.searchableFields`,
1625
- "list-view searchableFields"
2083
+ "list-view searchableFields",
2084
+ "narrowing"
1626
2085
  );
1627
2086
  }
1628
2087
  }
@@ -1630,27 +2089,29 @@ function validateSearchableFields(stack) {
1630
2089
  const views = asArray10(stack.views);
1631
2090
  for (let vi = 0; vi < views.length; vi++) {
1632
2091
  const view = views[vi];
1633
- if (!isRec2(view)) continue;
2092
+ if (!isRec3(view)) continue;
1634
2093
  const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
1635
2094
  const viewObject = strName2(view.objectName) ?? strName2(view.object);
1636
- if (isRec2(view.list)) {
2095
+ if (isRec3(view.list)) {
1637
2096
  check(
1638
2097
  view.list.searchableFields,
1639
2098
  listViewObject(view.list) ?? viewObject,
1640
2099
  `view "${viewLabel}" \u203A list`,
1641
2100
  `views[${vi}].list.searchableFields`,
1642
- "list-view searchableFields"
2101
+ "list-view searchableFields",
2102
+ "narrowing"
1643
2103
  );
1644
2104
  }
1645
- if (isRec2(view.listViews)) {
2105
+ if (isRec3(view.listViews)) {
1646
2106
  for (const [key, lv] of Object.entries(view.listViews)) {
1647
- if (!isRec2(lv)) continue;
2107
+ if (!isRec3(lv)) continue;
1648
2108
  check(
1649
2109
  lv.searchableFields,
1650
2110
  listViewObject(lv) ?? viewObject,
1651
2111
  `view "${viewLabel}" \u203A listViews.${key}`,
1652
2112
  `views[${vi}].listViews.${key}.searchableFields`,
1653
- "list-view searchableFields"
2113
+ "list-view searchableFields",
2114
+ "narrowing"
1654
2115
  );
1655
2116
  }
1656
2117
  }
@@ -1659,11 +2120,11 @@ function validateSearchableFields(stack) {
1659
2120
  }
1660
2121
  function listViewObject(listView) {
1661
2122
  const data = listView.data;
1662
- return isRec2(data) ? strName2(data.object) : void 0;
2123
+ return isRec3(data) ? strName2(data.object) : void 0;
1663
2124
  }
1664
2125
 
1665
2126
  // src/page-walk.ts
1666
- function isRec3(v) {
2127
+ function isRec4(v) {
1667
2128
  return !!v && typeof v === "object" && !Array.isArray(v);
1668
2129
  }
1669
2130
  function strName3(v) {
@@ -1676,19 +2137,19 @@ function isSourceAuthoredPage(page) {
1676
2137
  }
1677
2138
  function walkPageComponents(page, pagePath) {
1678
2139
  const out = [];
1679
- if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
2140
+ if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
1680
2141
  const pageObject = strName3(page.object);
1681
2142
  const visit = (node, path, inheritedObject) => {
1682
- if (!isRec3(node)) return;
1683
- const props = isRec3(node.properties) ? node.properties : void 0;
1684
- const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
2143
+ if (!isRec4(node)) return;
2144
+ const props = isRec4(node.properties) ? node.properties : void 0;
2145
+ const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
1685
2146
  const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
1686
2147
  out.push({ component: node, path, objectName });
1687
2148
  if (!props) return;
1688
2149
  if (Array.isArray(props.items)) {
1689
2150
  for (let i = 0; i < props.items.length; i++) {
1690
2151
  const item = props.items[i];
1691
- if (!isRec3(item) || !Array.isArray(item.children)) continue;
2152
+ if (!isRec4(item) || !Array.isArray(item.children)) continue;
1692
2153
  for (let c = 0; c < item.children.length; c++) {
1693
2154
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
1694
2155
  }
@@ -1710,12 +2171,12 @@ function walkPageComponents(page, pagePath) {
1710
2171
  const regions = Array.isArray(page.regions) ? page.regions : [];
1711
2172
  for (let r = 0; r < regions.length; r++) {
1712
2173
  const region = regions[r];
1713
- if (!isRec3(region) || !Array.isArray(region.components)) continue;
2174
+ if (!isRec4(region) || !Array.isArray(region.components)) continue;
1714
2175
  for (let c = 0; c < region.components.length; c++) {
1715
2176
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
1716
2177
  }
1717
2178
  }
1718
- const slots = isRec3(page.slots) ? page.slots : void 0;
2179
+ const slots = isRec4(page.slots) ? page.slots : void 0;
1719
2180
  if (slots) {
1720
2181
  for (const [slot, value] of Object.entries(slots)) {
1721
2182
  const list3 = Array.isArray(value) ? value : [value];
@@ -1740,7 +2201,7 @@ function asArray11(v) {
1740
2201
  function strName4(v) {
1741
2202
  return typeof v === "string" && v.length > 0 ? v : void 0;
1742
2203
  }
1743
- function isRec4(v) {
2204
+ function isRec5(v) {
1744
2205
  return !!v && typeof v === "object" && !Array.isArray(v);
1745
2206
  }
1746
2207
  function fieldRefsFrom(value, basePath) {
@@ -1751,7 +2212,7 @@ function fieldRefsFrom(value, basePath) {
1751
2212
  out.push({ name: bare, path });
1752
2213
  return;
1753
2214
  }
1754
- if (!isRec4(v)) return;
2215
+ if (!isRec5(v)) return;
1755
2216
  const named = strName4(v.field) ?? strName4(v.name);
1756
2217
  if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
1757
2218
  };
@@ -1793,15 +2254,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
1793
2254
  const sections = Array.isArray(props[key]) ? props[key] : [];
1794
2255
  for (let si = 0; si < sections.length; si++) {
1795
2256
  const section = sections[si];
1796
- if (!isRec4(section)) continue;
2257
+ if (!isRec5(section)) continue;
1797
2258
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
1798
2259
  }
1799
2260
  }
1800
2261
  return refs;
1801
2262
  }
1802
2263
  function relatedListFieldRefs(props, basePath, sep = ".") {
1803
- const add = isRec4(props.add) ? props.add : void 0;
1804
- const picker = add && isRec4(add.picker) ? add.picker : void 0;
2264
+ const add = isRec5(props.add) ? props.add : void 0;
2265
+ const picker = add && isRec5(add.picker) ? add.picker : void 0;
1805
2266
  const at = (key) => `${basePath}${sep}${key}`;
1806
2267
  return {
1807
2268
  relatedObject: strName4(props.objectName),
@@ -1822,7 +2283,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
1822
2283
  }
1823
2284
  function indexObjectFields(stack) {
1824
2285
  const objectFields = /* @__PURE__ */ new Map();
1825
- if (!isRec4(stack)) return objectFields;
2286
+ if (!isRec5(stack)) return objectFields;
1826
2287
  for (const obj of asArray11(stack.objects)) {
1827
2288
  const name = strName4(obj.name);
1828
2289
  if (!name) continue;
@@ -1868,7 +2329,7 @@ function validatePageFieldBindings(stack) {
1868
2329
  };
1869
2330
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
1870
2331
  const type = strName4(component.type);
1871
- const props = isRec4(component.properties) ? component.properties : void 0;
2332
+ const props = isRec5(component.properties) ? component.properties : void 0;
1872
2333
  if (!type || !props) continue;
1873
2334
  const where = `page "${pageName}" \xB7 ${type}`;
1874
2335
  const base = `${path}.properties`;
@@ -1883,7 +2344,7 @@ function validatePageFieldBindings(stack) {
1883
2344
  if (!refs) continue;
1884
2345
  checkRefs(refs, objectName, where);
1885
2346
  }
1886
- const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
2347
+ const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
1887
2348
  if (cfg) {
1888
2349
  const cfgObject = strName4(cfg.source) ?? strName4(page.object);
1889
2350
  const base = `pages[${pi}].interfaceConfig`;
@@ -1892,7 +2353,7 @@ function validatePageFieldBindings(stack) {
1892
2353
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
1893
2354
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
1894
2355
  ];
1895
- const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
2356
+ const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
1896
2357
  if (userFilters) {
1897
2358
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
1898
2359
  }
@@ -2007,28 +2468,28 @@ var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
2007
2468
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
2008
2469
  var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
2009
2470
  var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
2010
- var isRec5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2471
+ var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2011
2472
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
2012
2473
  function checkObjectChart(attrs, objectFields, findings) {
2013
2474
  const { values, where, path } = attrs;
2014
- const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2475
+ const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
2015
2476
  if (values.has("data")) return;
2016
2477
  const aggregate = values.get("aggregate");
2017
2478
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
2018
- if (!isRec5(aggregate)) return;
2479
+ if (!isRec6(aggregate)) return;
2019
2480
  const fn = strOf(aggregate.function);
2020
2481
  const field = strOf(aggregate.field);
2021
2482
  const groupBy = aggregate.groupBy;
2022
- const groupByField = strOf(groupBy) ?? (isRec5(groupBy) ? strOf(groupBy.field) : void 0);
2483
+ const groupByField = strOf(groupBy) ?? (isRec6(groupBy) ? strOf(groupBy.field) : void 0);
2023
2484
  if (fn && !CHART_FUNCTIONS.includes(fn)) {
2024
- push(
2485
+ push2(
2025
2486
  "error",
2026
2487
  REACT_CHART_AGGREGATE_INVALID,
2027
2488
  `aggregate.function "${fn}" is not an aggregation this chart can run.`,
2028
2489
  `Use one of: ${CHART_FUNCTIONS.join(", ")}.`
2029
2490
  );
2030
2491
  } else if (fn && fn !== "count" && !field) {
2031
- push(
2492
+ push2(
2032
2493
  "error",
2033
2494
  REACT_CHART_AGGREGATE_INVALID,
2034
2495
  `aggregate.function "${fn}" has no "field" to aggregate.`,
@@ -2042,7 +2503,7 @@ function checkObjectChart(attrs, objectFields, findings) {
2042
2503
  if (!name) return;
2043
2504
  if (name.includes(".")) return;
2044
2505
  if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
2045
- push(
2506
+ push2(
2046
2507
  "error",
2047
2508
  REACT_CHART_FIELD_UNKNOWN,
2048
2509
  `aggregate.${prop} "${name}" is not a field on object "${objectName}" \u2014 the aggregate query has nothing to ${prop === "groupBy" ? "group by" : "aggregate"}, so the chart comes back empty.`,
@@ -2059,7 +2520,7 @@ function checkObjectChart(attrs, objectFields, findings) {
2059
2520
  if (!name) return;
2060
2521
  if (columns.includes(name)) return;
2061
2522
  if (keys.comparison && name === keys.comparison) return;
2062
- push(
2523
+ push2(
2063
2524
  "error",
2064
2525
  REACT_CHART_AXIS_UNKNOWN,
2065
2526
  `"${name}" is not a column this aggregate returns, so the axis plots nothing. Object-bound aggregate rows are keyed by the RAW FIELD NAMES (unlike a dataset, whose rows are keyed by measure name).`,
@@ -2067,24 +2528,24 @@ function checkObjectChart(attrs, objectFields, findings) {
2067
2528
  );
2068
2529
  };
2069
2530
  const xAxisRaw = values.get("xAxis");
2070
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec5(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2531
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec6(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
2071
2532
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
2072
2533
  axisRef(categoryAxis, categoryProp);
2073
2534
  const yAxisRaw = values.get("yAxis");
2074
2535
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
2075
2536
  for (const a of yAxisList) {
2076
- axisRef(strOf(a) ?? (isRec5(a) ? strOf(a.field) : void 0), "yAxis[].field");
2537
+ axisRef(strOf(a) ?? (isRec6(a) ? strOf(a.field) : void 0), "yAxis[].field");
2077
2538
  }
2078
2539
  const series = values.get("series");
2079
2540
  if (Array.isArray(series)) {
2080
2541
  for (const s of series) {
2081
- if (!isRec5(s)) continue;
2542
+ if (!isRec6(s)) continue;
2082
2543
  const dataKey = strOf(s.dataKey);
2083
2544
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
2084
2545
  }
2085
2546
  }
2086
2547
  if (categoryAxis && keys.category && categoryAxis !== keys.category && categoryAxis === keys.value) {
2087
- push(
2548
+ push2(
2088
2549
  "error",
2089
2550
  REACT_CHART_AXIS_UNKNOWN,
2090
2551
  `${categoryProp} "${categoryAxis}" is the aggregate's VALUE column, not its category column.`,
@@ -2134,7 +2595,7 @@ function subformFieldRefs(value, basePath) {
2134
2595
  if (!Array.isArray(value)) return { child, parent };
2135
2596
  for (let i = 0; i < value.length; i++) {
2136
2597
  const sub = value[i];
2137
- if (!isRec5(sub)) continue;
2598
+ if (!isRec6(sub)) continue;
2138
2599
  const at = (key) => `${basePath}[${i}].${key}`;
2139
2600
  child.push({
2140
2601
  objectName: strOf(sub.childObject),
@@ -2179,20 +2640,20 @@ function reactFieldRefs(spec, values, basePath) {
2179
2640
  }
2180
2641
  for (const key of spec.nestedFields ?? []) {
2181
2642
  const v = readable(key);
2182
- if (isRec5(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2643
+ if (isRec6(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
2183
2644
  }
2184
2645
  for (const key of spec.sections ?? []) {
2185
2646
  const v = readable(key);
2186
2647
  if (!Array.isArray(v)) continue;
2187
2648
  for (let i = 0; i < v.length; i++) {
2188
2649
  const section = v[i];
2189
- if (!isRec5(section)) continue;
2650
+ if (!isRec6(section)) continue;
2190
2651
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
2191
2652
  }
2192
2653
  }
2193
2654
  for (const key of spec.keyedByField ?? []) {
2194
2655
  const v = readable(key);
2195
- if (!isRec5(v)) continue;
2656
+ if (!isRec6(v)) continue;
2196
2657
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
2197
2658
  }
2198
2659
  for (const key of spec.filterArrays ?? []) {
@@ -2218,25 +2679,41 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
2218
2679
  out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
2219
2680
  }
2220
2681
  const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
2221
- if (schemaType) {
2222
- const props = readableProps(values);
2223
- if (schemaType === RELATED_LIST_TYPE) {
2224
- const split = relatedListFieldRefs(props, path, PATH_SEP);
2225
- out.push(...checkFieldRefs(split.related, split.relatedObject, objectFields, where));
2226
- out.push(...checkFieldRefs(split.picker, split.pickerObject, objectFields, where));
2227
- } else if (COMPONENT_FIELD_SPECS[schemaType]) {
2228
- out.push(
2229
- ...checkFieldRefs(
2230
- componentFieldRefs(schemaType, props, path, PATH_SEP) ?? [],
2231
- objectName,
2232
- objectFields,
2233
- where
2234
- )
2235
- );
2236
- }
2682
+ if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
2683
+ out.push(
2684
+ ...checkFieldRefs(
2685
+ componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
2686
+ objectName,
2687
+ objectFields,
2688
+ where
2689
+ )
2690
+ );
2237
2691
  }
2238
2692
  return out;
2239
2693
  }
2694
+ var REACT_BLOCK_NEEDS_RECORD_CONTEXT = "react-block-needs-record-context";
2695
+ var RECORD_BLOCK_GENERIC_FIX = "author the page as `type:'record'` \u2014 a record page mounts the record context these blocks render from.";
2696
+ function recordContextFinding(tag, schemaType, where, path) {
2697
+ return {
2698
+ severity: "error",
2699
+ rule: REACT_BLOCK_NEEDS_RECORD_CONTEXT,
2700
+ where,
2701
+ path,
2702
+ message: `<${tag}> renders "${schemaType}", which reads its record from the record context a record page mounts \u2014 a kind:'react' page never mounts one, so the block renders empty no matter how it is bound (its objectName/recordId are not read by the renderer).`,
2703
+ hint: `On a react page bind the record yourself: ${REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
2704
+ };
2705
+ }
2706
+ function localComponentNames(tsc, sf) {
2707
+ const names = /* @__PURE__ */ new Set();
2708
+ const walk = (node) => {
2709
+ if (tsc.isFunctionDeclaration(node) && node.name) names.add(node.name.text);
2710
+ else if (tsc.isVariableDeclaration(node) && tsc.isIdentifier(node.name)) names.add(node.name.text);
2711
+ else if (tsc.isImportSpecifier(node)) names.add(node.name.text);
2712
+ tsc.forEachChild(node, walk);
2713
+ };
2714
+ walk(sf);
2715
+ return names;
2716
+ }
2240
2717
  function validateReactPageProps(stack) {
2241
2718
  const findings = [];
2242
2719
  const objectFields = indexObjectFields(stack);
@@ -2255,9 +2732,18 @@ function validateReactPageProps(stack) {
2255
2732
  } catch {
2256
2733
  continue;
2257
2734
  }
2735
+ const locals = localComponentNames(tsc, sf);
2258
2736
  const visit = (node) => {
2259
2737
  if (tsc.isJsxOpeningElement(node) || tsc.isJsxSelfClosingElement(node)) {
2260
2738
  const tag = node.tagName.getText(sf);
2739
+ const where = `page "${name}" \u203A <${tag}>`;
2740
+ const path = `pages[${p}].source`;
2741
+ const recordType = RECORD_CONTEXT_BLOCK_TAGS.get(tag);
2742
+ if (recordType && !locals.has(tag)) {
2743
+ findings.push(recordContextFinding(tag, recordType, where, path));
2744
+ tsc.forEachChild(node, visit);
2745
+ return;
2746
+ }
2261
2747
  const block = BLOCKS.get(tag);
2262
2748
  if (block) {
2263
2749
  let hasSpread = false;
@@ -2277,8 +2763,14 @@ function validateReactPageProps(stack) {
2277
2763
  );
2278
2764
  }
2279
2765
  }
2280
- const where = `page "${name}" \u203A <${tag}>`;
2281
- const path = `pages[${p}].source`;
2766
+ if (tag === "Block") {
2767
+ const blockType = strOf(values.get("type"));
2768
+ if (blockType && isRecordContextBlockType(blockType)) {
2769
+ findings.push(recordContextFinding(tag, blockType, where, path));
2770
+ tsc.forEachChild(node, visit);
2771
+ return;
2772
+ }
2773
+ }
2282
2774
  if (!hasSpread) {
2283
2775
  for (const req of block.requiredBindings) {
2284
2776
  if (!used.has(req)) {
@@ -3996,16 +4488,86 @@ function validateObjectReferences(stack) {
3996
4488
  return findings;
3997
4489
  }
3998
4490
 
4491
+ // src/validate-nav-target-refs.ts
4492
+ var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
4493
+ var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4494
+ function asArray25(v) {
4495
+ if (Array.isArray(v)) return v.filter(isRec7);
4496
+ if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
4497
+ return [];
4498
+ }
4499
+ function strName7(v) {
4500
+ return typeof v === "string" && v.length > 0 ? v : void 0;
4501
+ }
4502
+ var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
4503
+ var NAV_TARGETS = [
4504
+ ["page", "pageName", "pages", "page"],
4505
+ ["report", "reportName", "reports", "report"],
4506
+ ["dashboard", "dashboardName", "dashboards", "dashboard"]
4507
+ ];
4508
+ function namesOf(collection) {
4509
+ const out = /* @__PURE__ */ new Set();
4510
+ for (const entry of asArray25(collection)) {
4511
+ const n = strName7(entry.name);
4512
+ if (n) out.add(n);
4513
+ }
4514
+ return out;
4515
+ }
4516
+ function validateNavTargetRefs(stack) {
4517
+ const findings = [];
4518
+ if (!isRec7(stack)) return findings;
4519
+ const apps = asArray25(stack.apps);
4520
+ if (apps.length === 0) return findings;
4521
+ const declared = /* @__PURE__ */ new Map();
4522
+ for (const [, , collection] of NAV_TARGETS) {
4523
+ declared.set(collection, namesOf(stack[collection]));
4524
+ }
4525
+ for (const [ai, app] of apps.entries()) {
4526
+ const appName = strName7(app.name) ?? `#${ai}`;
4527
+ const walk = (items, basePath) => {
4528
+ if (!Array.isArray(items)) return;
4529
+ for (const [ni, raw] of items.entries()) {
4530
+ if (!isRec7(raw)) continue;
4531
+ const nav = raw;
4532
+ const navPath = `${basePath}[${ni}]`;
4533
+ for (const [type, prop, collection, noun] of NAV_TARGETS) {
4534
+ if (nav.type !== type) continue;
4535
+ const target = strName7(nav[prop]);
4536
+ if (!target || isInterpolated2(target)) continue;
4537
+ const known = declared.get(collection);
4538
+ if (known.has(target)) continue;
4539
+ const emptyCollection = known.size === 0;
4540
+ findings.push({
4541
+ severity: "warning",
4542
+ rule: NAV_TARGET_UNRESOLVED,
4543
+ where: `app "${appName}" \xB7 nav "${strName7(nav.id) ?? strName7(nav.label) ?? `#${ni}`}"`,
4544
+ path: `${navPath}.${prop}`,
4545
+ message: `Navigation targets ${noun} '${target}', which this stack does not declare in \`${collection}\`. ` + (emptyCollection ? `The stack declares NO ${collection} at all, so \`defineStack\`'s own cross-reference check skipped this entry entirely (it is gated on \`${collection === "pages" ? "pageNames" : collection === "reports" ? "reportNames" : "dashboardNames"}.size > 0\`) \u2014 nothing else will report it. ` : "") + `The entry renders in the sidebar and resolves to nothing when clicked. If another package provides this ${noun}, this is expected and advisory only.`,
4546
+ hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
4547
+ });
4548
+ }
4549
+ if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
4550
+ }
4551
+ };
4552
+ walk(app.navigation, `apps[${ai}].navigation`);
4553
+ for (const [ari, area] of asArray25(app.areas).entries()) {
4554
+ walk(area.items, `apps[${ai}].areas[${ari}].items`);
4555
+ walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
4556
+ }
4557
+ }
4558
+ return findings;
4559
+ }
4560
+
3999
4561
  // src/validate-action-name-refs.ts
4000
4562
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
4001
- function asArray25(v) {
4563
+ function asArray26(v) {
4002
4564
  if (Array.isArray(v)) return v;
4003
4565
  if (v && typeof v === "object") {
4004
4566
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4005
4567
  }
4006
4568
  return [];
4007
4569
  }
4008
- function strName7(v) {
4570
+ function strName8(v) {
4009
4571
  return typeof v === "string" && v.length > 0 ? v : void 0;
4010
4572
  }
4011
4573
  function strList(v) {
@@ -4042,14 +4604,14 @@ function suggest4(target, known) {
4042
4604
  }
4043
4605
  function collectActionNames(stack) {
4044
4606
  const names = /* @__PURE__ */ new Set();
4045
- for (const action of asArray25(stack.actions)) {
4046
- const n = strName7(action?.name);
4607
+ for (const action of asArray26(stack.actions)) {
4608
+ const n = strName8(action?.name);
4047
4609
  if (n) names.add(n);
4048
4610
  }
4049
- for (const obj of asArray25(stack.objects)) {
4611
+ for (const obj of asArray26(stack.objects)) {
4050
4612
  if (!obj || typeof obj !== "object") continue;
4051
- for (const action of asArray25(obj.actions)) {
4052
- const n = strName7(action?.name);
4613
+ for (const action of asArray26(obj.actions)) {
4614
+ const n = strName8(action?.name);
4053
4615
  if (n) names.add(n);
4054
4616
  }
4055
4617
  }
@@ -4059,7 +4621,7 @@ function validateActionNameRefs(stack) {
4059
4621
  const findings = [];
4060
4622
  if (!stack || typeof stack !== "object") return findings;
4061
4623
  const known = collectActionNames(stack);
4062
- const check = (name, where, path, surface) => {
4624
+ const check = (name, where, path, surface, placement = "with the location this surface needs") => {
4063
4625
  if (known.has(name)) return;
4064
4626
  findings.push({
4065
4627
  severity: "error",
@@ -4067,42 +4629,72 @@ function validateActionNameRefs(stack) {
4067
4629
  where,
4068
4630
  path,
4069
4631
  message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest4(name, known),
4070
- hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
4632
+ hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) ${placement}, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
4071
4633
  });
4072
4634
  };
4073
- const views = asArray25(stack.views);
4635
+ const SELECTION_BAR_PLACEMENT = "(no `locations` entry needed \u2014 the selection bar places it by name)";
4636
+ const checkListContainer = (container, owner, label2, path) => {
4637
+ if (!container || typeof container !== "object") return;
4638
+ const list3 = container;
4639
+ for (const key of ["rowActions", "bulkActions"]) {
4640
+ const names = strList(list3[key]);
4641
+ for (let ai = 0; ai < names.length; ai++) {
4642
+ check(
4643
+ names[ai],
4644
+ `${owner} \xB7 ${label2} \xB7 ${key}`,
4645
+ `${path}.${key}[${ai}]`,
4646
+ key === "bulkActions" ? "Bulk-action menu" : "Row-action menu",
4647
+ key === "bulkActions" ? SELECTION_BAR_PLACEMENT : void 0
4648
+ );
4649
+ }
4650
+ }
4651
+ const defs = Array.isArray(list3.bulkActionDefs) ? list3.bulkActionDefs : [];
4652
+ for (let di = 0; di < defs.length; di++) {
4653
+ const def = defs[di];
4654
+ if (!def || typeof def !== "object") continue;
4655
+ if (def.execution !== "aggregate") continue;
4656
+ if (def.actionDef !== void 0) continue;
4657
+ const name = strName8(def.name);
4658
+ if (!name) continue;
4659
+ check(
4660
+ name,
4661
+ `${owner} \xB7 ${label2} \xB7 bulkActionDefs[${di}]`,
4662
+ `${path}.bulkActionDefs[${di}].name`,
4663
+ "Aggregate bulk action",
4664
+ SELECTION_BAR_PLACEMENT
4665
+ );
4666
+ }
4667
+ };
4668
+ const views = asArray26(stack.views);
4074
4669
  for (let vi = 0; vi < views.length; vi++) {
4075
4670
  const view = views[vi];
4076
4671
  if (!view || typeof view !== "object") continue;
4077
- const viewName = strName7(view.name) ?? strName7(view.object) ?? `#${vi}`;
4078
- const checkListContainer = (container, label2, path) => {
4079
- if (!container || typeof container !== "object") return;
4080
- const list3 = container;
4081
- for (const key of ["rowActions", "bulkActions"]) {
4082
- const names = strList(list3[key]);
4083
- for (let ai = 0; ai < names.length; ai++) {
4084
- check(
4085
- names[ai],
4086
- `view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
4087
- `${path}.${key}[${ai}]`,
4088
- key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
4089
- );
4090
- }
4091
- }
4092
- };
4093
- checkListContainer(view.list, "list", `views[${vi}].list`);
4672
+ const viewName = strName8(view.name) ?? strName8(view.object) ?? `#${vi}`;
4673
+ const owner = `view "${viewName}"`;
4674
+ checkListContainer(view.list, owner, "list", `views[${vi}].list`);
4094
4675
  const listViews = view.listViews;
4095
4676
  if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
4096
4677
  for (const [key, lv] of Object.entries(listViews)) {
4097
- checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
4678
+ checkListContainer(lv, owner, `listViews.${key}`, `views[${vi}].listViews.${key}`);
4098
4679
  }
4099
4680
  }
4100
4681
  }
4101
- const pages = asArray25(stack.pages);
4682
+ const objects = asArray26(stack.objects);
4683
+ for (let oi = 0; oi < objects.length; oi++) {
4684
+ const obj = objects[oi];
4685
+ if (!obj || typeof obj !== "object") continue;
4686
+ const objListViews = obj.listViews;
4687
+ if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
4688
+ const owner = `object "${strName8(obj.name) ?? `#${oi}`}"`;
4689
+ for (const [key, lv] of Object.entries(objListViews)) {
4690
+ checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
4691
+ }
4692
+ }
4693
+ const pages = asArray26(stack.pages);
4102
4694
  for (let pi = 0; pi < pages.length; pi++) {
4103
4695
  const page = pages[pi];
4104
4696
  if (!page || typeof page !== "object") continue;
4105
- const pageName = strName7(page.name) ?? `#${pi}`;
4697
+ const pageName = strName8(page.name) ?? `#${pi}`;
4106
4698
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4107
4699
  const props = component.properties;
4108
4700
  if (!props || typeof props !== "object") continue;
@@ -4110,30 +4702,30 @@ function validateActionNameRefs(stack) {
4110
4702
  for (let ai = 0; ai < names.length; ai++) {
4111
4703
  check(
4112
4704
  names[ai],
4113
- `page "${pageName}" \xB7 component "${strName7(component.type) ?? "?"}"`,
4705
+ `page "${pageName}" \xB7 component "${strName8(component.type) ?? "?"}"`,
4114
4706
  `${path}.properties.actionNames[${ai}]`,
4115
4707
  "Quick-actions bar"
4116
4708
  );
4117
4709
  }
4118
4710
  }
4119
4711
  }
4120
- const apps = asArray25(stack.apps);
4712
+ const apps = asArray26(stack.apps);
4121
4713
  for (let ai = 0; ai < apps.length; ai++) {
4122
4714
  const app = apps[ai];
4123
4715
  if (!app || typeof app !== "object") continue;
4124
- const appName = strName7(app.name) ?? `#${ai}`;
4716
+ const appName = strName8(app.name) ?? `#${ai}`;
4125
4717
  const walkNav = (items, basePath) => {
4126
- const navItems = asArray25(items);
4718
+ const navItems = asArray26(items);
4127
4719
  for (let ni = 0; ni < navItems.length; ni++) {
4128
4720
  const nav = navItems[ni];
4129
4721
  if (!nav || typeof nav !== "object") continue;
4130
4722
  const navPath = `${basePath}[${ni}]`;
4131
4723
  const actionDef = nav.actionDef;
4132
- const actionName = strName7(actionDef?.actionName);
4724
+ const actionName = strName8(actionDef?.actionName);
4133
4725
  if (nav.type === "action" && actionName) {
4134
4726
  check(
4135
4727
  actionName,
4136
- `app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
4728
+ `app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
4137
4729
  `${navPath}.actionDef.actionName`,
4138
4730
  "Navigation action item"
4139
4731
  );
@@ -4142,7 +4734,7 @@ function validateActionNameRefs(stack) {
4142
4734
  }
4143
4735
  };
4144
4736
  walkNav(app.navigation, `apps[${ai}].navigation`);
4145
- const areas = asArray25(app.areas);
4737
+ const areas = asArray26(app.areas);
4146
4738
  for (let ri = 0; ri < areas.length; ri++) {
4147
4739
  walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4148
4740
  }
@@ -4150,46 +4742,120 @@ function validateActionNameRefs(stack) {
4150
4742
  return findings;
4151
4743
  }
4152
4744
 
4153
- // src/validate-chart-bindings.ts
4154
- var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
4155
- var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
4156
- var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
4157
- var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
4158
- function asArray26(v) {
4745
+ // src/validate-action-locations.ts
4746
+ var ACTION_NO_PLACEMENT = "action-no-placement";
4747
+ function asArray27(v) {
4159
4748
  if (Array.isArray(v)) return v;
4160
4749
  if (v && typeof v === "object") {
4161
4750
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4162
4751
  }
4163
4752
  return [];
4164
4753
  }
4165
- function strName8(v) {
4754
+ function strName9(v) {
4166
4755
  return typeof v === "string" && v.length > 0 ? v : void 0;
4167
4756
  }
4168
4757
  function strList2(v) {
4169
4758
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
4170
4759
  }
4171
- function isRec6(v) {
4172
- return !!v && typeof v === "object" && !Array.isArray(v);
4173
- }
4174
- function distance4(a, b) {
4175
- const m = a.length;
4176
- const n = b.length;
4177
- if (m === 0) return n;
4178
- if (n === 0) return m;
4179
- let prev = Array.from({ length: n + 1 }, (_, j) => j);
4180
- for (let i = 1; i <= m; i++) {
4181
- const curr = [i, ...new Array(n).fill(0)];
4182
- for (let j = 1; j <= n; j++) {
4183
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4184
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
4760
+ function collectNamePlacedActions(stack) {
4761
+ const placed = /* @__PURE__ */ new Set();
4762
+ const harvest = (container) => {
4763
+ if (!container || typeof container !== "object") return;
4764
+ const list3 = container;
4765
+ for (const key of ["rowActions", "bulkActions"]) {
4766
+ for (const n of strList2(list3[key])) placed.add(n);
4185
4767
  }
4186
- prev = curr;
4768
+ for (const def of asArray27(list3.bulkActionDefs)) {
4769
+ const n = strName9(def?.name);
4770
+ if (n) placed.add(n);
4771
+ }
4772
+ };
4773
+ const harvestListViews = (listViews) => {
4774
+ if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
4775
+ for (const lv of Object.values(listViews)) harvest(lv);
4776
+ };
4777
+ for (const view of asArray27(stack.views)) {
4778
+ if (!view || typeof view !== "object") continue;
4779
+ harvest(view.list);
4780
+ harvestListViews(view.listViews);
4187
4781
  }
4188
- return prev[n];
4782
+ for (const obj of asArray27(stack.objects)) {
4783
+ if (!obj || typeof obj !== "object") continue;
4784
+ harvestListViews(obj.listViews);
4785
+ }
4786
+ return placed;
4189
4787
  }
4190
- function suggest5(target, known) {
4191
- let best;
4192
- let bestScore = Infinity;
4788
+ function validateActionLocations(stack) {
4789
+ const findings = [];
4790
+ if (!stack || typeof stack !== "object") return findings;
4791
+ const namePlaced = collectNamePlacedActions(stack);
4792
+ const check = (action, path) => {
4793
+ if (!action || typeof action !== "object") return;
4794
+ if ("locations" in action) return;
4795
+ const name = strName9(action.name);
4796
+ if (!name) return;
4797
+ if (namePlaced.has(name)) return;
4798
+ findings.push({
4799
+ severity: "warning",
4800
+ rule: ACTION_NO_PLACEMENT,
4801
+ where: `action "${name}"`,
4802
+ path,
4803
+ 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.`,
4804
+ 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."
4805
+ });
4806
+ };
4807
+ const actions = asArray27(stack.actions);
4808
+ for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
4809
+ const objects = asArray27(stack.objects);
4810
+ for (let oi = 0; oi < objects.length; oi++) {
4811
+ const obj = objects[oi];
4812
+ if (!obj || typeof obj !== "object") continue;
4813
+ const own = asArray27(obj.actions);
4814
+ for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
4815
+ }
4816
+ return findings;
4817
+ }
4818
+
4819
+ // src/validate-chart-bindings.ts
4820
+ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
4821
+ var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
4822
+ var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
4823
+ var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
4824
+ function asArray28(v) {
4825
+ if (Array.isArray(v)) return v;
4826
+ if (v && typeof v === "object") {
4827
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4828
+ }
4829
+ return [];
4830
+ }
4831
+ function strName10(v) {
4832
+ return typeof v === "string" && v.length > 0 ? v : void 0;
4833
+ }
4834
+ function strList3(v) {
4835
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
4836
+ }
4837
+ function isRec8(v) {
4838
+ return !!v && typeof v === "object" && !Array.isArray(v);
4839
+ }
4840
+ function distance4(a, b) {
4841
+ const m = a.length;
4842
+ const n = b.length;
4843
+ if (m === 0) return n;
4844
+ if (n === 0) return m;
4845
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
4846
+ for (let i = 1; i <= m; i++) {
4847
+ const curr = [i, ...new Array(n).fill(0)];
4848
+ for (let j = 1; j <= n; j++) {
4849
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4850
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
4851
+ }
4852
+ prev = curr;
4853
+ }
4854
+ return prev[n];
4855
+ }
4856
+ function suggest5(target, known) {
4857
+ let best;
4858
+ let bestScore = Infinity;
4193
4859
  for (const c of known) {
4194
4860
  const d = distance4(target, c);
4195
4861
  if (d < bestScore) {
@@ -4206,17 +4872,17 @@ function list2(names) {
4206
4872
  }
4207
4873
  function indexDatasets(stack) {
4208
4874
  const out = /* @__PURE__ */ new Map();
4209
- for (const ds of asArray26(stack.datasets)) {
4210
- const name = strName8(ds.name);
4875
+ for (const ds of asArray28(stack.datasets)) {
4876
+ const name = strName10(ds.name);
4211
4877
  if (!name) continue;
4212
4878
  const dimensions = /* @__PURE__ */ new Set();
4213
- for (const d of asArray26(ds.dimensions)) {
4214
- const n = strName8(d.name);
4879
+ for (const d of asArray28(ds.dimensions)) {
4880
+ const n = strName10(d.name);
4215
4881
  if (n) dimensions.add(n);
4216
4882
  }
4217
4883
  const measures = /* @__PURE__ */ new Set();
4218
- for (const m of asArray26(ds.measures)) {
4219
- const n = strName8(m.name);
4884
+ for (const m of asArray28(ds.measures)) {
4885
+ const n = strName10(m.name);
4220
4886
  if (n) measures.add(n);
4221
4887
  }
4222
4888
  out.set(name, { dimensions, measures });
@@ -4294,75 +4960,75 @@ function validateChartBindings(stack) {
4294
4960
  if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
4295
4961
  for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
4296
4962
  };
4297
- const reports = asArray26(stack.reports);
4963
+ const reports = asArray28(stack.reports);
4298
4964
  for (let ri = 0; ri < reports.length; ri++) {
4299
4965
  const report = reports[ri];
4300
- if (!isRec6(report)) continue;
4301
- const reportName = strName8(report.name) ?? `#${ri}`;
4966
+ if (!isRec8(report)) continue;
4967
+ const reportName = strName10(report.name) ?? `#${ri}`;
4302
4968
  const checkReportChart = (chart, dataset, values, where, path) => {
4303
- if (!isRec6(chart)) return;
4969
+ if (!isRec8(chart)) return;
4304
4970
  check({
4305
4971
  dataset,
4306
4972
  // `values` is the report's measure SELECTION, not a chart ref; feeding
4307
4973
  // it in lets the yAxis "declared but not selected" check work without
4308
4974
  // reporting the selection itself twice.
4309
4975
  values: { names: values, path: `${path}.values` },
4310
- xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
4311
- yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
4312
- series: asArray26(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
4976
+ xAxis: strName10(chart.xAxis) ? { name: strName10(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
4977
+ yAxis: strName10(chart.yAxis) ? { name: strName10(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
4978
+ series: asArray28(chart.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
4313
4979
  where,
4314
4980
  path: `${path}.chart`
4315
4981
  });
4316
4982
  };
4317
4983
  checkReportChart(
4318
4984
  report.chart,
4319
- strName8(report.dataset),
4320
- strList2(report.values),
4985
+ strName10(report.dataset),
4986
+ strList3(report.values),
4321
4987
  `report "${reportName}" \xB7 chart`,
4322
4988
  `reports[${ri}]`
4323
4989
  );
4324
4990
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
4325
4991
  for (let bi = 0; bi < blocks.length; bi++) {
4326
4992
  const block = blocks[bi];
4327
- if (!isRec6(block)) continue;
4993
+ if (!isRec8(block)) continue;
4328
4994
  checkReportChart(
4329
4995
  block.chart,
4330
- strName8(block.dataset),
4331
- strList2(block.values),
4332
- `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
4996
+ strName10(block.dataset),
4997
+ strList3(block.values),
4998
+ `report "${reportName}" \xB7 block "${strName10(block.name) ?? `#${bi}`}" chart`,
4333
4999
  `reports[${ri}].blocks[${bi}]`
4334
5000
  );
4335
5001
  }
4336
5002
  }
4337
5003
  const checkListChart = (container, where, path) => {
4338
- if (!isRec6(container)) return;
5004
+ if (!isRec8(container)) return;
4339
5005
  const chart = container.chart;
4340
- if (!isRec6(chart)) return;
5006
+ if (!isRec8(chart)) return;
4341
5007
  check({
4342
- dataset: strName8(chart.dataset),
4343
- dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
4344
- values: { names: strList2(chart.values), path: `${path}.chart.values` },
5008
+ dataset: strName10(chart.dataset),
5009
+ dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
5010
+ values: { names: strList3(chart.values), path: `${path}.chart.values` },
4345
5011
  where,
4346
5012
  path: `${path}.chart`
4347
5013
  });
4348
5014
  };
4349
- const views = asArray26(stack.views);
5015
+ const views = asArray28(stack.views);
4350
5016
  for (let vi = 0; vi < views.length; vi++) {
4351
5017
  const view = views[vi];
4352
- if (!isRec6(view)) continue;
4353
- const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
5018
+ if (!isRec8(view)) continue;
5019
+ const viewName = strName10(view.name) ?? strName10(view.objectName) ?? `#${vi}`;
4354
5020
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
4355
- if (isRec6(view.listViews)) {
5021
+ if (isRec8(view.listViews)) {
4356
5022
  for (const [key, lv] of Object.entries(view.listViews)) {
4357
5023
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
4358
5024
  }
4359
5025
  }
4360
5026
  }
4361
- const objects = asArray26(stack.objects);
5027
+ const objects = asArray28(stack.objects);
4362
5028
  for (let oi = 0; oi < objects.length; oi++) {
4363
5029
  const obj = objects[oi];
4364
- if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
4365
- const objName = strName8(obj.name) ?? `#${oi}`;
5030
+ if (!isRec8(obj) || !isRec8(obj.listViews)) continue;
5031
+ const objName = strName10(obj.name) ?? `#${oi}`;
4366
5032
  for (const [key, lv] of Object.entries(obj.listViews)) {
4367
5033
  checkListChart(
4368
5034
  lv,
@@ -4371,22 +5037,22 @@ function validateChartBindings(stack) {
4371
5037
  );
4372
5038
  }
4373
5039
  }
4374
- const pages = asArray26(stack.pages);
5040
+ const pages = asArray28(stack.pages);
4375
5041
  for (let pi = 0; pi < pages.length; pi++) {
4376
5042
  const page = pages[pi];
4377
- if (!isRec6(page)) continue;
4378
- const pageName = strName8(page.name) ?? `#${pi}`;
5043
+ if (!isRec8(page)) continue;
5044
+ const pageName = strName10(page.name) ?? `#${pi}`;
4379
5045
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
4380
- const props = isRec6(component.properties) ? component.properties : void 0;
4381
- if (!props || !strName8(props.dataset)) continue;
4382
- const axisRefs = asArray26(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
4383
- const seriesRefs = asArray26(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
5046
+ const props = isRec8(component.properties) ? component.properties : void 0;
5047
+ if (!props || !strName10(props.dataset)) continue;
5048
+ const axisRefs = asArray28(props.yAxis).map((a, ai) => ({ name: strName10(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
5049
+ const seriesRefs = asArray28(props.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
4384
5050
  check({
4385
- dataset: strName8(props.dataset),
4386
- dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
4387
- values: { names: strList2(props.values), path: `${path}.properties.values` },
5051
+ dataset: strName10(props.dataset),
5052
+ dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
5053
+ values: { names: strList3(props.values), path: `${path}.properties.values` },
4388
5054
  series: [...axisRefs, ...seriesRefs],
4389
- where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
5055
+ where: `page "${pageName}" \xB7 ${strName10(component.type) ?? "chart"}`,
4390
5056
  path: `${path}.properties`
4391
5057
  });
4392
5058
  }
@@ -4398,7 +5064,7 @@ function validateChartBindings(stack) {
4398
5064
  import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
4399
5065
 
4400
5066
  // src/build-access-matrix.ts
4401
- function asArray27(v) {
5067
+ function asArray29(v) {
4402
5068
  if (Array.isArray(v)) return v;
4403
5069
  if (v && typeof v === "object") {
4404
5070
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -4409,13 +5075,13 @@ function buildAccessMatrix(stack) {
4409
5075
  const entries = [];
4410
5076
  if (!stack || typeof stack !== "object") return { version: 1, entries };
4411
5077
  const owdByObject = /* @__PURE__ */ new Map();
4412
- for (const obj of asArray27(stack.objects)) {
5078
+ for (const obj of asArray29(stack.objects)) {
4413
5079
  const name = typeof obj.name === "string" ? obj.name : "";
4414
5080
  if (!name) continue;
4415
5081
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
4416
5082
  if (typeof owd === "string") owdByObject.set(name, owd);
4417
5083
  }
4418
- for (const ps of asArray27(stack.permissions)) {
5084
+ for (const ps of asArray29(stack.permissions)) {
4419
5085
  const psName = typeof ps.name === "string" ? ps.name : "";
4420
5086
  if (!psName) continue;
4421
5087
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -4488,34 +5154,34 @@ function diffAccessMatrix(before, after) {
4488
5154
 
4489
5155
  // src/validate-nav-access.ts
4490
5156
  var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
4491
- function asArray28(v) {
5157
+ function asArray30(v) {
4492
5158
  if (Array.isArray(v)) return v;
4493
5159
  if (v && typeof v === "object") {
4494
5160
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
4495
5161
  }
4496
5162
  return [];
4497
5163
  }
4498
- function strName9(v) {
5164
+ function strName11(v) {
4499
5165
  return typeof v === "string" && v.length > 0 ? v : void 0;
4500
5166
  }
4501
5167
  function collectNavExposures(stack) {
4502
5168
  const out = [];
4503
- const apps = asArray28(stack.apps);
5169
+ const apps = asArray30(stack.apps);
4504
5170
  for (let ai = 0; ai < apps.length; ai++) {
4505
5171
  const app = apps[ai];
4506
5172
  if (!app || typeof app !== "object") continue;
4507
- const appName = strName9(app.name) ?? `#${ai}`;
5173
+ const appName = strName11(app.name) ?? `#${ai}`;
4508
5174
  const walk = (items, basePath) => {
4509
- const navItems = asArray28(items);
5175
+ const navItems = asArray30(items);
4510
5176
  for (let ni = 0; ni < navItems.length; ni++) {
4511
5177
  const nav = navItems[ni];
4512
5178
  if (!nav || typeof nav !== "object") continue;
4513
5179
  const navPath = `${basePath}[${ni}]`;
4514
- const objectName = strName9(nav.objectName);
5180
+ const objectName = strName11(nav.objectName);
4515
5181
  if (nav.type === "object" && objectName) {
4516
5182
  out.push({
4517
5183
  objectName,
4518
- where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
5184
+ where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
4519
5185
  path: `${navPath}.objectName`
4520
5186
  });
4521
5187
  }
@@ -4523,7 +5189,7 @@ function collectNavExposures(stack) {
4523
5189
  }
4524
5190
  };
4525
5191
  walk(app.navigation, `apps[${ai}].navigation`);
4526
- const areas = asArray28(app.areas);
5192
+ const areas = asArray30(app.areas);
4527
5193
  for (let ri = 0; ri < areas.length; ri++) {
4528
5194
  walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
4529
5195
  }
@@ -4533,13 +5199,13 @@ function collectNavExposures(stack) {
4533
5199
  function validateNavAccess(stack) {
4534
5200
  const findings = [];
4535
5201
  if (!stack || typeof stack !== "object") return findings;
4536
- const permissionSets = asArray28(stack.permissions);
5202
+ const permissionSets = asArray30(stack.permissions);
4537
5203
  if (permissionSets.length === 0) return findings;
4538
5204
  const exposures = collectNavExposures(stack);
4539
5205
  if (exposures.length === 0) return findings;
4540
5206
  const ownObjects = /* @__PURE__ */ new Set();
4541
- for (const obj of asArray28(stack.objects)) {
4542
- const n = strName9(obj.name);
5207
+ for (const obj of asArray30(stack.objects)) {
5208
+ const n = strName11(obj.name);
4543
5209
  if (n) ownObjects.add(n);
4544
5210
  }
4545
5211
  const readable = /* @__PURE__ */ new Set();
@@ -4571,15 +5237,15 @@ function validateNavAccess(stack) {
4571
5237
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
4572
5238
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
4573
5239
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
4574
- function isRec7(v) {
5240
+ function isRec9(v) {
4575
5241
  return !!v && typeof v === "object" && !Array.isArray(v);
4576
5242
  }
4577
- function asArray29(v) {
5243
+ function asArray31(v) {
4578
5244
  if (Array.isArray(v)) return v;
4579
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
5245
+ if (isRec9(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec9(def) ? def : {} }));
4580
5246
  return [];
4581
5247
  }
4582
- function strName10(v) {
5248
+ function strName12(v) {
4583
5249
  return typeof v === "string" && v.length > 0 ? v : void 0;
4584
5250
  }
4585
5251
  function distance5(a, b) {
@@ -4637,20 +5303,20 @@ function collectViewRecord(view, factsFor) {
4637
5303
  const addView = (objectName, name) => {
4638
5304
  if (objectName && name) factsFor(objectName).views.add(name);
4639
5305
  };
4640
- const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
4641
- if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
4642
- addView(recordObject ?? listBinding, strName10(view.name));
5306
+ const listBinding = isRec9(view.list) ? bindingOf(view.list) : void 0;
5307
+ if (isRec9(view.list)) addView(listBinding, strName12(view.list.name));
5308
+ addView(recordObject ?? listBinding, strName12(view.name));
4643
5309
  for (const key of ["listViews", "formViews"]) {
4644
5310
  const container = view[key];
4645
- if (!isRec7(container)) continue;
5311
+ if (!isRec9(container)) continue;
4646
5312
  for (const [subKey, sub] of Object.entries(container)) {
4647
- if (!isRec7(sub)) continue;
5313
+ if (!isRec9(sub)) continue;
4648
5314
  const binding = bindingOf(sub) ?? listBinding;
4649
5315
  addView(binding, subKey);
4650
- addView(binding, strName10(sub.name));
5316
+ addView(binding, strName12(sub.name));
4651
5317
  if (binding) {
4652
- for (const section of asArray29(sub.sections)) {
4653
- const sectionName = strName10(section.name);
5318
+ for (const section of asArray31(sub.sections)) {
5319
+ const sectionName = strName12(section.name);
4654
5320
  if (sectionName) factsFor(binding).sections.add(sectionName);
4655
5321
  }
4656
5322
  }
@@ -4658,14 +5324,14 @@ function collectViewRecord(view, factsFor) {
4658
5324
  }
4659
5325
  const sectionBinding = recordObject ?? listBinding;
4660
5326
  if (sectionBinding) {
4661
- for (const section of asArray29(view.sections)) {
4662
- const sectionName = strName10(section.name);
5327
+ for (const section of asArray31(view.sections)) {
5328
+ const sectionName = strName12(section.name);
4663
5329
  if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
4664
5330
  }
4665
5331
  }
4666
5332
  }
4667
5333
  function viewObjectName(view) {
4668
- return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
5334
+ return strName12(view.objectName) ?? strName12(view.object) ?? (isRec9(view.data) ? strName12(view.data.object) : void 0);
4669
5335
  }
4670
5336
  function readOptions(field) {
4671
5337
  const raw = field.options;
@@ -4677,14 +5343,14 @@ function readOptions(field) {
4677
5343
  values.add(opt);
4678
5344
  continue;
4679
5345
  }
4680
- if (!isRec7(opt)) continue;
4681
- const value = strName10(opt.value);
5346
+ if (!isRec9(opt)) continue;
5347
+ const value = strName12(opt.value);
4682
5348
  if (!value) continue;
4683
5349
  values.add(value);
4684
- const label2 = strName10(opt.label);
5350
+ const label2 = strName12(opt.label);
4685
5351
  if (label2) byLabel.set(label2.toLowerCase(), value);
4686
5352
  }
4687
- } else if (isRec7(raw)) {
5353
+ } else if (isRec9(raw)) {
4688
5354
  for (const [value, label2] of Object.entries(raw)) {
4689
5355
  values.add(value);
4690
5356
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -4704,48 +5370,48 @@ function buildUniverse(stack) {
4704
5370
  }
4705
5371
  return facts;
4706
5372
  };
4707
- for (const obj of asArray29(stack.objects)) {
4708
- const objectName = strName10(obj.name);
5373
+ for (const obj of asArray31(stack.objects)) {
5374
+ const objectName = strName12(obj.name);
4709
5375
  if (!objectName) continue;
4710
5376
  const facts = factsFor(objectName);
4711
- for (const field of asArray29(obj.fields)) {
4712
- const fieldName = strName10(field.name);
5377
+ for (const field of asArray31(obj.fields)) {
5378
+ const fieldName = strName12(field.name);
4713
5379
  if (fieldName) facts.fields.set(fieldName, field);
4714
5380
  }
4715
- for (const action of asArray29(obj.actions)) {
4716
- const actionName = strName10(action.name);
5381
+ for (const action of asArray31(obj.actions)) {
5382
+ const actionName = strName12(action.name);
4717
5383
  if (actionName) facts.actions.set(actionName, action);
4718
5384
  }
4719
- for (const view of asArray29(obj.views)) {
4720
- collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
5385
+ for (const view of asArray31(obj.views)) {
5386
+ collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
4721
5387
  }
4722
5388
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
4723
- for (const group of asArray29(obj.fieldGroups)) {
4724
- const key = strName10(group.key) ?? strName10(group.name);
5389
+ for (const group of asArray31(obj.fieldGroups)) {
5390
+ const key = strName12(group.key) ?? strName12(group.name);
4725
5391
  if (key) facts.sections.add(key);
4726
5392
  }
4727
5393
  }
4728
- for (const view of asArray29(stack.views)) {
5394
+ for (const view of asArray31(stack.views)) {
4729
5395
  collectViewRecord(view, factsFor);
4730
5396
  }
4731
- const pages = asArray29(stack.pages);
5397
+ const pages = asArray31(stack.pages);
4732
5398
  for (let pi = 0; pi < pages.length; pi++) {
4733
5399
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
4734
5400
  if (!walked.objectName) continue;
4735
- const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
5401
+ const props = isRec9(walked.component.properties) ? walked.component.properties : void 0;
4736
5402
  if (!props) continue;
4737
- for (const section of asArray29(props.sections)) {
4738
- const sectionName = strName10(section.name);
5403
+ for (const section of asArray31(props.sections)) {
5404
+ const sectionName = strName12(section.name);
4739
5405
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
4740
5406
  }
4741
5407
  }
4742
5408
  }
4743
5409
  const globalActions = /* @__PURE__ */ new Map();
4744
5410
  const actionOwners = /* @__PURE__ */ new Map();
4745
- for (const action of asArray29(stack.actions)) {
4746
- const actionName = strName10(action.name);
5411
+ for (const action of asArray31(stack.actions)) {
5412
+ const actionName = strName12(action.name);
4747
5413
  if (!actionName) continue;
4748
- const owner = strName10(action.objectName) ?? strName10(action.object);
5414
+ const owner = strName12(action.objectName) ?? strName12(action.object);
4749
5415
  if (owner) {
4750
5416
  factsFor(owner).actions.set(actionName, action);
4751
5417
  actionOwners.set(actionName, owner);
@@ -4759,41 +5425,41 @@ function buildUniverse(stack) {
4759
5425
  }
4760
5426
  }
4761
5427
  const apps = /* @__PURE__ */ new Map();
4762
- for (const app of asArray29(stack.apps)) {
4763
- const appName = strName10(app.name);
5428
+ for (const app of asArray31(stack.apps)) {
5429
+ const appName = strName12(app.name);
4764
5430
  if (!appName) continue;
4765
5431
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
4766
5432
  const walkNav = (items) => {
4767
- for (const item of asArray29(items)) {
4768
- const id = strName10(item.id);
5433
+ for (const item of asArray31(items)) {
5434
+ const id = strName12(item.id);
4769
5435
  if (id) navIds.add(id);
4770
5436
  if (item.children) walkNav(item.children);
4771
5437
  }
4772
5438
  };
4773
5439
  walkNav(app.navigation);
4774
- for (const area of asArray29(app.areas)) {
4775
- const areaId = strName10(area.id);
5440
+ for (const area of asArray31(app.areas)) {
5441
+ const areaId = strName12(area.id);
4776
5442
  if (areaId) navIds.add(areaId);
4777
5443
  walkNav(area.navigation);
4778
5444
  }
4779
5445
  apps.set(appName, navIds);
4780
5446
  }
4781
5447
  const dashboards = /* @__PURE__ */ new Map();
4782
- for (const dash of asArray29(stack.dashboards)) {
4783
- const dashName = strName10(dash.name);
5448
+ for (const dash of asArray31(stack.dashboards)) {
5449
+ const dashName = strName12(dash.name);
4784
5450
  if (!dashName) continue;
4785
5451
  const widgets = /* @__PURE__ */ new Set();
4786
- for (const widget of asArray29(dash.widgets)) {
4787
- const id = strName10(widget.id) ?? strName10(widget.name);
5452
+ for (const widget of asArray31(dash.widgets)) {
5453
+ const id = strName12(widget.id) ?? strName12(widget.name);
4788
5454
  if (id) widgets.add(id);
4789
5455
  }
4790
5456
  const actions = /* @__PURE__ */ new Set();
4791
5457
  const headerActions = [
4792
- ...asArray29(isRec7(dash.header) ? dash.header.actions : void 0),
4793
- ...asArray29(dash.actions)
5458
+ ...asArray31(isRec9(dash.header) ? dash.header.actions : void 0),
5459
+ ...asArray31(dash.actions)
4794
5460
  ];
4795
5461
  for (const action of headerActions) {
4796
- const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
5462
+ const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
4797
5463
  if (key) actions.add(key);
4798
5464
  }
4799
5465
  dashboards.set(dashName, { widgets, actions });
@@ -4805,7 +5471,7 @@ function localePath(bundleIndex, locale) {
4805
5471
  }
4806
5472
  function validateTranslationReferences(stack) {
4807
5473
  const findings = [];
4808
- if (!isRec7(stack)) return findings;
5474
+ if (!isRec9(stack)) return findings;
4809
5475
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
4810
5476
  if (bundles.length === 0) return findings;
4811
5477
  const universe = buildUniverse(stack);
@@ -4814,13 +5480,13 @@ function validateTranslationReferences(stack) {
4814
5480
  };
4815
5481
  for (let bi = 0; bi < bundles.length; bi++) {
4816
5482
  const bundle = bundles[bi];
4817
- if (!isRec7(bundle)) continue;
5483
+ if (!isRec9(bundle)) continue;
4818
5484
  for (const [locale, rawData] of Object.entries(bundle)) {
4819
- if (!isRec7(rawData)) continue;
5485
+ if (!isRec9(rawData)) continue;
4820
5486
  const base = localePath(bi, locale);
4821
5487
  const inLocale = `locale "${locale}"`;
4822
5488
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
4823
- if (!isRec7(rawNode)) continue;
5489
+ if (!isRec9(rawNode)) continue;
4824
5490
  const objPath = `${base}.objects.${objectName}`;
4825
5491
  const facts = universe.objects.get(objectName);
4826
5492
  if (!facts) {
@@ -4846,7 +5512,7 @@ function validateTranslationReferences(stack) {
4846
5512
  );
4847
5513
  continue;
4848
5514
  }
4849
- if (!isRec7(rawField)) continue;
5515
+ if (!isRec9(rawField)) continue;
4850
5516
  checkOptionKeys(findings, {
4851
5517
  optionMap: rawField.options,
4852
5518
  field,
@@ -4928,7 +5594,7 @@ function validateTranslationReferences(stack) {
4928
5594
  );
4929
5595
  continue;
4930
5596
  }
4931
- if (!isRec7(rawApp)) continue;
5597
+ if (!isRec9(rawApp)) continue;
4932
5598
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
4933
5599
  if (navIds.has(navId)) continue;
4934
5600
  orphan(
@@ -4951,7 +5617,7 @@ function validateTranslationReferences(stack) {
4951
5617
  );
4952
5618
  continue;
4953
5619
  }
4954
- if (!isRec7(rawDash)) continue;
5620
+ if (!isRec9(rawDash)) continue;
4955
5621
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
4956
5622
  if (dash.widgets.has(widgetId)) continue;
4957
5623
  orphan(
@@ -4976,7 +5642,7 @@ function validateTranslationReferences(stack) {
4976
5642
  return findings;
4977
5643
  }
4978
5644
  function asRecord(v) {
4979
- return isRec7(v) ? v : {};
5645
+ return isRec9(v) ? v : {};
4980
5646
  }
4981
5647
  function checkOptionKeys(findings, ctx) {
4982
5648
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -4988,7 +5654,7 @@ function checkOptionKeys(findings, ctx) {
4988
5654
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
4989
5655
  where: ctx.where,
4990
5656
  path: ctx.path,
4991
- 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.`,
5657
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
4992
5658
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
4993
5659
  });
4994
5660
  return;
@@ -5007,11 +5673,11 @@ function checkOptionKeys(findings, ctx) {
5007
5673
  }
5008
5674
  }
5009
5675
  function checkActionParams(findings, ctx) {
5010
- const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
5676
+ const rawParams = Object.keys(asRecord(isRec9(ctx.rawAction) ? ctx.rawAction.params : void 0));
5011
5677
  if (rawParams.length === 0) return;
5012
5678
  const declared = /* @__PURE__ */ new Set();
5013
- for (const param of asArray29(ctx.action.params)) {
5014
- const name = strName10(param.name) ?? strName10(param.field);
5679
+ for (const param of asArray31(ctx.action.params)) {
5680
+ const name = strName12(param.name) ?? strName12(param.field);
5015
5681
  if (name) declared.add(name);
5016
5682
  }
5017
5683
  for (const paramName of rawParams) {
@@ -5029,14 +5695,14 @@ function checkActionParams(findings, ctx) {
5029
5695
 
5030
5696
  // src/validate-ai-surface-affinity.ts
5031
5697
  var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
5032
- function asArray30(v) {
5698
+ function asArray32(v) {
5033
5699
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5034
5700
  if (v && typeof v === "object") {
5035
5701
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5036
5702
  }
5037
5703
  return [];
5038
5704
  }
5039
- function strName11(v) {
5705
+ function strName13(v) {
5040
5706
  return typeof v === "string" && v.length > 0 ? v : void 0;
5041
5707
  }
5042
5708
  function surfaceOf(v) {
@@ -5046,18 +5712,18 @@ function validateAiSurfaceAffinity(stack) {
5046
5712
  const findings = [];
5047
5713
  if (!stack || typeof stack !== "object") return findings;
5048
5714
  const skillsByName = /* @__PURE__ */ new Map();
5049
- for (const skill of asArray30(stack.skills)) {
5050
- const n = strName11(skill.name);
5715
+ for (const skill of asArray32(stack.skills)) {
5716
+ const n = strName13(skill.name);
5051
5717
  if (n) skillsByName.set(n, skill);
5052
5718
  }
5053
- const agents = asArray30(stack.agents);
5719
+ const agents = asArray32(stack.agents);
5054
5720
  for (let ai = 0; ai < agents.length; ai++) {
5055
5721
  const agent = agents[ai];
5056
- const agentName = strName11(agent.name) ?? `#${ai}`;
5722
+ const agentName = strName13(agent.name) ?? `#${ai}`;
5057
5723
  const agentSurface = surfaceOf(agent.surface);
5058
5724
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
5059
5725
  for (let si = 0; si < skillRefs.length; si++) {
5060
- const ref = strName11(skillRefs[si]);
5726
+ const ref = strName13(skillRefs[si]);
5061
5727
  if (!ref) continue;
5062
5728
  const skill = skillsByName.get(ref);
5063
5729
  if (!skill) continue;
@@ -5079,14 +5745,14 @@ function validateAiSurfaceAffinity(stack) {
5079
5745
  // src/validate-ai-tool-references.ts
5080
5746
  import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
5081
5747
  var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
5082
- function asArray31(v) {
5748
+ function asArray33(v) {
5083
5749
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5084
5750
  if (v && typeof v === "object") {
5085
5751
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5086
5752
  }
5087
5753
  return [];
5088
5754
  }
5089
- function strName12(v) {
5755
+ function strName14(v) {
5090
5756
  return typeof v === "string" && v.length > 0 ? v : void 0;
5091
5757
  }
5092
5758
  function distance6(a, b) {
@@ -5127,26 +5793,26 @@ function materialisesAsTool(action) {
5127
5793
  if (!ai || typeof ai !== "object") return false;
5128
5794
  const aiRec = ai;
5129
5795
  if (aiRec.exposed !== true) return false;
5130
- if (!strName12(aiRec.description)) return false;
5131
- const type = strName12(action.type);
5796
+ if (!strName14(aiRec.description)) return false;
5797
+ const type = strName14(action.type);
5132
5798
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
5133
5799
  if (type === "script") return Boolean(action.target || action.body);
5134
5800
  return Boolean(action.target);
5135
5801
  }
5136
5802
  function collectToolUniverse(stack) {
5137
5803
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
5138
- for (const tool of asArray31(stack.tools)) {
5139
- const n = strName12(tool.name);
5804
+ for (const tool of asArray33(stack.tools)) {
5805
+ const n = strName14(tool.name);
5140
5806
  if (n) universe.add(n);
5141
5807
  }
5142
5808
  const addActionFamily = (actions) => {
5143
- for (const action of asArray31(actions)) {
5144
- const n = strName12(action.name);
5809
+ for (const action of asArray33(actions)) {
5810
+ const n = strName14(action.name);
5145
5811
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
5146
5812
  }
5147
5813
  };
5148
5814
  addActionFamily(stack.actions);
5149
- for (const obj of asArray31(stack.objects)) {
5815
+ for (const obj of asArray33(stack.objects)) {
5150
5816
  addActionFamily(obj.actions);
5151
5817
  }
5152
5818
  return universe;
@@ -5154,13 +5820,13 @@ function collectToolUniverse(stack) {
5154
5820
  function collectUnexposedActionNames(stack) {
5155
5821
  const names = /* @__PURE__ */ new Set();
5156
5822
  const scan = (actions) => {
5157
- for (const action of asArray31(actions)) {
5158
- const n = strName12(action.name);
5823
+ for (const action of asArray33(actions)) {
5824
+ const n = strName14(action.name);
5159
5825
  if (n && !materialisesAsTool(action)) names.add(n);
5160
5826
  }
5161
5827
  };
5162
5828
  scan(stack.actions);
5163
- for (const obj of asArray31(stack.objects)) scan(obj.actions);
5829
+ for (const obj of asArray33(stack.objects)) scan(obj.actions);
5164
5830
  return names;
5165
5831
  }
5166
5832
  function validateAiToolReferences(stack) {
@@ -5178,13 +5844,13 @@ function validateAiToolReferences(stack) {
5178
5844
  }
5179
5845
  return universe.has(ref);
5180
5846
  };
5181
- const skills = asArray31(stack.skills);
5847
+ const skills = asArray33(stack.skills);
5182
5848
  for (let si = 0; si < skills.length; si++) {
5183
5849
  const skill = skills[si];
5184
- const skillName = strName12(skill.name) ?? `#${si}`;
5850
+ const skillName = strName14(skill.name) ?? `#${si}`;
5185
5851
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
5186
5852
  for (let ti = 0; ti < refs.length; ti++) {
5187
- const ref = strName12(refs[ti]);
5853
+ const ref = strName14(refs[ti]);
5188
5854
  if (!ref || resolves(ref)) continue;
5189
5855
  const isPattern = ref.endsWith("*");
5190
5856
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -5203,24 +5869,24 @@ function validateAiToolReferences(stack) {
5203
5869
 
5204
5870
  // src/validate-ai-agent-authoring.ts
5205
5871
  var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
5206
- function asArray32(v) {
5872
+ function asArray34(v) {
5207
5873
  if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
5208
5874
  if (v && typeof v === "object") {
5209
5875
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
5210
5876
  }
5211
5877
  return [];
5212
5878
  }
5213
- function strName13(v) {
5879
+ function strName15(v) {
5214
5880
  return typeof v === "string" && v.length > 0 ? v : void 0;
5215
5881
  }
5216
5882
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
5217
5883
  function validateAiAgentAuthoring(stack) {
5218
5884
  const findings = [];
5219
5885
  if (!stack || typeof stack !== "object") return findings;
5220
- const agents = asArray32(stack.agents);
5886
+ const agents = asArray34(stack.agents);
5221
5887
  for (let ai = 0; ai < agents.length; ai++) {
5222
5888
  const agent = agents[ai];
5223
- const name = strName13(agent.name) ?? `#${ai}`;
5889
+ const name = strName15(agent.name) ?? `#${ai}`;
5224
5890
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
5225
5891
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
5226
5892
  findings.push({
@@ -5324,24 +5990,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
5324
5990
  "owner",
5325
5991
  "record_type"
5326
5992
  ]);
5327
- var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5328
- function asArray33(v) {
5329
- if (Array.isArray(v)) return v.filter((x) => isRec8(x));
5330
- if (isRec8(v)) {
5993
+ var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5994
+ function asArray35(v) {
5995
+ if (Array.isArray(v)) return v.filter((x) => isRec10(x));
5996
+ if (isRec10(v)) {
5331
5997
  return Object.entries(v).map(([name, def]) => ({
5332
5998
  name,
5333
- ...isRec8(def) ? def : {}
5999
+ ...isRec10(def) ? def : {}
5334
6000
  }));
5335
6001
  }
5336
6002
  return [];
5337
6003
  }
5338
6004
  function indexObjectFields2(stack) {
5339
6005
  const out = /* @__PURE__ */ new Map();
5340
- for (const obj of asArray33(stack.objects)) {
6006
+ for (const obj of asArray35(stack.objects)) {
5341
6007
  const name = typeof obj.name === "string" ? obj.name : void 0;
5342
6008
  if (!name) continue;
5343
6009
  const names = /* @__PURE__ */ new Set();
5344
- for (const f of asArray33(obj.fields)) {
6010
+ for (const f of asArray35(obj.fields)) {
5345
6011
  if (typeof f.name === "string" && f.name) names.add(f.name);
5346
6012
  }
5347
6013
  out.set(name, names);
@@ -5471,12 +6137,12 @@ ${source}
5471
6137
  }
5472
6138
  function validateHookBodyWrites(stack) {
5473
6139
  const findings = [];
5474
- const hooks = asArray33(stack.hooks);
6140
+ const hooks = asArray35(stack.hooks);
5475
6141
  if (hooks.length === 0) return findings;
5476
6142
  let objectFields = null;
5477
6143
  hooks.forEach((hook, hookIndex) => {
5478
6144
  const body = hook.body;
5479
- if (!isRec8(body) || body.language !== "js") return;
6145
+ if (!isRec10(body) || body.language !== "js") return;
5480
6146
  const source = body.source;
5481
6147
  if (typeof source !== "string" || source.trim() === "") return;
5482
6148
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
@@ -5557,13 +6223,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
5557
6223
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
5558
6224
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
5559
6225
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
5560
- var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5561
- function asArray34(v) {
5562
- if (Array.isArray(v)) return v.filter((x) => isRec9(x));
5563
- if (isRec9(v)) {
6226
+ var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6227
+ function asArray36(v) {
6228
+ if (Array.isArray(v)) return v.filter((x) => isRec11(x));
6229
+ if (isRec11(v)) {
5564
6230
  return Object.entries(v).map(([name, def]) => ({
5565
6231
  name,
5566
- ...isRec9(def) ? def : {}
6232
+ ...isRec11(def) ? def : {}
5567
6233
  }));
5568
6234
  }
5569
6235
  return [];
@@ -5577,9 +6243,11 @@ function collectActionBodies(stack) {
5577
6243
  const sites = [];
5578
6244
  const seen = /* @__PURE__ */ new Set();
5579
6245
  const collect = (actions, pathPrefix, parentObject) => {
5580
- asArray34(actions).forEach((action, index) => {
6246
+ asArray36(actions).forEach((action, index) => {
6247
+ const type = typeof action.type === "string" ? action.type : "script";
6248
+ if (type !== "script") return;
5581
6249
  const body = action.body;
5582
- if (!isRec9(body) || body.language !== "js") return;
6250
+ if (!isRec11(body) || body.language !== "js") return;
5583
6251
  const source = body.source;
5584
6252
  if (typeof source !== "string" || source.trim() === "") return;
5585
6253
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -5590,7 +6258,7 @@ function collectActionBodies(stack) {
5590
6258
  });
5591
6259
  };
5592
6260
  collect(stack.actions, "actions");
5593
- asArray34(stack.objects).forEach((obj, objIndex) => {
6261
+ asArray36(stack.objects).forEach((obj, objIndex) => {
5594
6262
  const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
5595
6263
  collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
5596
6264
  });
@@ -5598,7 +6266,7 @@ function collectActionBodies(stack) {
5598
6266
  }
5599
6267
  function validateActionBodyWrites(stack) {
5600
6268
  const findings = [];
5601
- if (!isRec9(stack)) return findings;
6269
+ if (!isRec11(stack)) return findings;
5602
6270
  const sites = collectActionBodies(stack);
5603
6271
  if (sites.length === 0) return findings;
5604
6272
  let objectFields = null;
@@ -5657,13 +6325,13 @@ import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSu
5657
6325
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
5658
6326
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
5659
6327
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
5660
- var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5661
- function asArray35(v) {
5662
- if (Array.isArray(v)) return v.filter((x) => isRec10(x));
5663
- if (isRec10(v)) {
6328
+ var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6329
+ function asArray37(v) {
6330
+ if (Array.isArray(v)) return v.filter((x) => isRec12(x));
6331
+ if (isRec12(v)) {
5664
6332
  return Object.entries(v).map(([name, def]) => ({
5665
6333
  name,
5666
- ...isRec10(def) ? def : {}
6334
+ ...isRec12(def) ? def : {}
5667
6335
  }));
5668
6336
  }
5669
6337
  return [];
@@ -5676,8 +6344,8 @@ function readLiteralObjectName2(config) {
5676
6344
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
5677
6345
  function validateFlowNodeWrites(stack) {
5678
6346
  const findings = [];
5679
- if (!isRec10(stack)) return findings;
5680
- const flows = asArray35(stack.flows);
6347
+ if (!isRec12(stack)) return findings;
6348
+ const flows = asArray37(stack.flows);
5681
6349
  if (flows.length === 0) return findings;
5682
6350
  let objectFields = null;
5683
6351
  flows.forEach((flow, flowIndex) => {
@@ -5685,10 +6353,10 @@ function validateFlowNodeWrites(stack) {
5685
6353
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
5686
6354
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
5687
6355
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
5688
- const config = isRec10(node.config) ? node.config : void 0;
6356
+ const config = isRec12(node.config) ? node.config : void 0;
5689
6357
  if (!config) return;
5690
6358
  const fields = config.fields;
5691
- if (!isRec10(fields)) return;
6359
+ if (!isRec12(fields)) return;
5692
6360
  const written = Object.keys(fields);
5693
6361
  if (written.length === 0) return;
5694
6362
  const objectName = readLiteralObjectName2(config);
@@ -5727,6 +6395,13 @@ var REFERENCE_INTEGRITY_RULES = [
5727
6395
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5728
6396
  { name: "validateChartBindings", run: validateChartBindings },
5729
6397
  { name: "validateNavAccess", run: validateNavAccess },
6398
+ // Nav targets that are NOT object names — page/report/dashboard. Restores the
6399
+ // coverage `defineStack`'s own cross-reference block switches off whenever the
6400
+ // stack declares none of that collection (`pageNames.size > 0 && …`), which is
6401
+ // exactly the state a stack is in when the target was never written.
6402
+ // `action` is deliberately absent (validateActionNameRefs owns it) and so is
6403
+ // `component` (an unregistered ref renders a named diagnostic, not silence).
6404
+ { name: "validateNavTargetRefs", run: validateNavTargetRefs },
5730
6405
  { name: "validateTranslationReferences", run: validateTranslationReferences },
5731
6406
  { name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
5732
6407
  { name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
@@ -5774,12 +6449,20 @@ var REFERENCE_INTEGRITY_RULES = [
5774
6449
  { name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
5775
6450
  // The `kind:'react'` page surface. Every prop a react block binds BY FIELD
5776
6451
  // NAME is resolved against the object it names (#4340) — `<ListView columns>`,
5777
- // `<ObjectForm fields>`, the `record:*` family through the SAME
6452
+ // `<ObjectForm fields>`, `<Block type="element:…">` through the SAME
5778
6453
  // `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
5779
6454
  // over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
5780
6455
  // `searchableFields` (#4329). Squarely the charter's question, on the surface
5781
6456
  // where it had no answer at all.
5782
6457
  //
6458
+ // It also carries `react-block-needs-record-context` (#4413) — a BINDING
6459
+ // question rather than a resolution one: the `record:*` family reads its
6460
+ // record from a record page's context, so on THIS surface the binding does
6461
+ // not exist at all and the props the contract published for it were read by
6462
+ // no renderer. This rule used to resolve those props' field names against
6463
+ // the object they named — lint standing guard over a binding that never ran.
6464
+ // It rejects the blocks now, out of the same parse.
6465
+ //
5783
6466
  // It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
5784
6467
  // accepted a react page whose every field binding was stale — including the
5785
6468
  // gating ones (a missing required binding, a filter position naming no field:
@@ -5803,12 +6486,1494 @@ function validateReferenceIntegrity(stack) {
5803
6486
  }
5804
6487
  return findings;
5805
6488
  }
6489
+
6490
+ // src/lint-flow-patterns.ts
6491
+ function asArray38(v) {
6492
+ if (Array.isArray(v)) return v;
6493
+ if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6494
+ return [];
6495
+ }
6496
+ function conditionSource(raw) {
6497
+ if (typeof raw === "string") return raw;
6498
+ if (raw && typeof raw === "object" && typeof raw.source === "string") return raw.source;
6499
+ return "";
6500
+ }
6501
+ var TIME_FNS = "daysFromNow|daysAgo|today|now|date|datetime";
6502
+ var TIME_FN_RE = new RegExp(`\\b(?:${TIME_FNS})\\s*\\(`);
6503
+ var DATE_EQ = new RegExp(
6504
+ `(?:(?:${TIME_FNS})\\s*\\([^)]*\\)\\s*(?:==|!=))|(?:(?:==|!=)\\s*(?:${TIME_FNS})\\s*\\()`
6505
+ );
6506
+ var FLOW_TIME_RELATIVE_ANTIPATTERN = "flow-time-relative-antipattern";
6507
+ var FLOW_DATE_EQUALITY_FILTER = "flow-date-equality-filter";
6508
+ var FLOW_PHANTOM_AGGREGATION = "flow-phantom-aggregation";
6509
+ var FLOW_DOUBLE_BRACE_INTERP = "flow-double-brace-interpolation";
6510
+ var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
6511
+ var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
6512
+ var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
6513
+ var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
6514
+ var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
6515
+ var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
6516
+ var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
6517
+ var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
6518
+ var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
6519
+ var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
6520
+ var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
6521
+ var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
6522
+ "decision",
6523
+ "assignment",
6524
+ "loop",
6525
+ "parallel",
6526
+ "try_catch",
6527
+ "create_record",
6528
+ "update_record",
6529
+ "delete_record",
6530
+ "get_record",
6531
+ "http",
6532
+ "notify",
6533
+ "script",
6534
+ "screen",
6535
+ "wait",
6536
+ "subflow",
6537
+ "map",
6538
+ "connector_action",
6539
+ "approval",
6540
+ "end"
6541
+ ]);
6542
+ var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
6543
+ var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
6544
+ var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
6545
+ function isScheduleTriggered(flow, startCfg) {
6546
+ if (flow.type === "schedule") return true;
6547
+ if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "schedule") return true;
6548
+ return startCfg.schedule != null;
6549
+ }
6550
+ function userLessTriggerKind(flow, startCfg) {
6551
+ if (isScheduleTriggered(flow, startCfg)) return "schedule";
6552
+ if (startCfg.timeRelative != null) return "time-relative";
6553
+ if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "time_relative") return "time-relative";
6554
+ if (flow.type === "api") return "api";
6555
+ if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "api") return "api";
6556
+ return null;
6557
+ }
6558
+ var PHANTOM_AGG_KEYS = /* @__PURE__ */ new Set(["aggregations", "aggregate", "groupBy", "rollup", "having"]);
6559
+ function celTimeSource(v) {
6560
+ if (v && typeof v === "object" && v.dialect === "cel") {
6561
+ const src = v.source;
6562
+ if (typeof src === "string" && TIME_FN_RE.test(src)) return src;
6563
+ }
6564
+ return null;
6565
+ }
6566
+ var RANGE_OPS = /* @__PURE__ */ new Set(["$gte", "$gt", "$lte", "$lt", "$ne"]);
6567
+ function scanFilterForDateEquality(filter, where, findings) {
6568
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) return;
6569
+ for (const [key, val] of Object.entries(filter)) {
6570
+ if (key === "$or" || key === "$and") {
6571
+ if (Array.isArray(val)) for (const sub of val) scanFilterForDateEquality(sub, where, findings);
6572
+ continue;
6573
+ }
6574
+ const direct = celTimeSource(val);
6575
+ let hit = direct ? { op: "==", src: direct } : null;
6576
+ if (!hit && val && typeof val === "object" && val.dialect !== "cel") {
6577
+ for (const [op, operand] of Object.entries(val)) {
6578
+ if (RANGE_OPS.has(op)) continue;
6579
+ if (op === "$eq") {
6580
+ const s = celTimeSource(operand);
6581
+ if (s) {
6582
+ hit = { op: "$eq", src: s };
6583
+ break;
6584
+ }
6585
+ } else if (op === "$in" && Array.isArray(operand)) {
6586
+ for (const item of operand) {
6587
+ const s = celTimeSource(item);
6588
+ if (s) {
6589
+ hit = { op: "$in", src: s };
6590
+ break;
6591
+ }
6592
+ }
6593
+ if (hit) break;
6594
+ }
6595
+ }
6596
+ }
6597
+ if (hit) {
6598
+ findings.push({
6599
+ where,
6600
+ message: `filter matches \`${key}\` by ${hit.op} against a time value (\`${hit.src}\`) \u2014 a date field carries a time component, so exact equality against \`${hit.src}\` (re-computed each run) silently matches nothing.`,
6601
+ hint: `Use a one-day window instead: \`${key}: { $gte: daysFromNow(N), $lt: daysFromNow(N+1) }\` (wrap multiple tiers in \`$or\`). The abutting windows tile the timeline so each row matches exactly once. (#1874)`,
6602
+ rule: FLOW_DATE_EQUALITY_FILTER
6603
+ });
6604
+ }
6605
+ }
6606
+ }
6607
+ var DOUBLE_BRACE = /\{\{\s*[\w$][\w$.\s]*\}\}/;
6608
+ var BARE_DOLLAR_REF = /(?:^|[^{])\$[A-Za-z_]\w*\.[A-Za-z_]/;
6609
+ var CEL_KEYS = /* @__PURE__ */ new Set(["condition", "expression", "conditions"]);
6610
+ function collectTemplateStrings(value, key, out) {
6611
+ if (key && CEL_KEYS.has(key)) return;
6612
+ if (typeof value === "string") {
6613
+ out.push(value);
6614
+ return;
6615
+ }
6616
+ if (Array.isArray(value)) {
6617
+ for (const v of value) collectTemplateStrings(v, key, out);
6618
+ return;
6619
+ }
6620
+ if (value && typeof value === "object") {
6621
+ for (const [k, v] of Object.entries(value)) collectTemplateStrings(v, k, out);
6622
+ }
6623
+ }
6624
+ function edgeLabelOf(e) {
6625
+ return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
6626
+ }
6627
+ function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
6628
+ const typeById = /* @__PURE__ */ new Map();
6629
+ for (const n of nodes) {
6630
+ if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
6631
+ }
6632
+ for (const e of edges) {
6633
+ const label2 = typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
6634
+ if (!ERROR_LABELS.has(label2)) continue;
6635
+ if (e.type === "fault") continue;
6636
+ if (e.condition) continue;
6637
+ const src = typeof e.source === "string" ? e.source : "";
6638
+ if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
6639
+ findings.push({
6640
+ where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6641
+ message: `edge is labelled '${String(e.label)}' but its type is '${String(e.type ?? "default")}', not 'fault' \u2014 so it is an ORDINARY out-edge. Unconditional out-edges all run in parallel, so '${String(e.target)}' executes on every SUCCESSFUL run of '${src}' and never on a failure. The error path the label describes does not exist, and the run still aborts when '${src}' fails.`,
6642
+ hint: `Add \`type: 'fault'\` to this edge. Only runtime failures route \u2014 a guard refusal (a filter token that resolved to nothing, a missing required config key, an unscoped run) stays fatal by design and must be fixed in the metadata, not handled. (#3863)`,
6643
+ rule: FLOW_ERROR_LABEL_NOT_FAULT
6644
+ });
6645
+ }
6646
+ }
6647
+ function scanBranchRouting(flowName, nodes, edges, findings) {
6648
+ const outEdgesBySource = /* @__PURE__ */ new Map();
6649
+ for (const e of edges) {
6650
+ if (e.type === "fault") continue;
6651
+ const src = typeof e.source === "string" ? e.source : "";
6652
+ if (!src) continue;
6653
+ if (!outEdgesBySource.has(src)) outEdgesBySource.set(src, []);
6654
+ outEdgesBySource.get(src).push(e);
6655
+ }
6656
+ for (const [src, outs] of outEdgesBySource) {
6657
+ for (const e of outs) {
6658
+ if (e.isDefault === true && e.condition) {
6659
+ findings.push({
6660
+ where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
6661
+ message: `edge sets \`isDefault: true\` AND a \`condition\` \u2014 contradictory. \`isDefault\` means "take this edge when NO sibling condition matched"; a condition makes it an ordinary guarded branch. The condition wins and the default marker routes nothing.`,
6662
+ hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
6663
+ rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
6664
+ // Gating: the two keys contradict, the condition always wins, and the
6665
+ // marker never routes. No reading makes it do what it says.
6666
+ severity: "error"
6667
+ });
6668
+ }
6669
+ }
6670
+ const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
6671
+ if (defaults.length > 1) {
6672
+ findings.push({
6673
+ where: `flow '${flowName}' \xB7 node '${src}'`,
6674
+ message: `${defaults.length} out-edges are marked \`isDefault: true\` (${defaults.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 a node has at most ONE default path. All of them are traversed together when no condition matches, which is a parallel fan-out, not an "otherwise".`,
6675
+ hint: `Keep \`isDefault: true\` on the single fallback edge and give the others a \`condition\` (or leave them unconditional if the fan-out really is intended). (#4414)`,
6676
+ rule: FLOW_MULTIPLE_DEFAULT_EDGES
6677
+ });
6678
+ }
6679
+ }
6680
+ for (const node of nodes) {
6681
+ const nodeType = typeof node.type === "string" ? node.type : "";
6682
+ if (!INERT_CONDITION_NODE_TYPES.has(nodeType)) continue;
6683
+ const cfg = node.config ?? {};
6684
+ if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
6685
+ findings.push({
6686
+ where: `flow '${flowName}' \xB7 node '${String(node.id)}' (${nodeType})`,
6687
+ message: `\`config.condition\` is set but nothing reads it \u2014 the key is the trigger gate on a \`start\` node and is ignored on every other node type, so this predicate never gates anything. (It is still parse-validated at registration, which is why a malformed one is caught and an inert one is not.)`,
6688
+ hint: nodeType === "decision" ? `Branching lives on the OUT-EDGES: give each branch its own \`condition\` and mark the fallback \`isDefault: true\`. If the edges already carry the predicate, delete this copy. (#4414)` : `Delete it, or move the predicate to the incoming edge's \`condition\` if this step was meant to be conditional. (#4414)`,
6689
+ rule: FLOW_INERT_NODE_CONDITION
6690
+ });
6691
+ }
6692
+ for (const node of nodes) {
6693
+ if (node.type !== "decision") continue;
6694
+ const nid = typeof node.id === "string" ? node.id : "";
6695
+ if (!nid) continue;
6696
+ const outs = outEdgesBySource.get(nid) ?? [];
6697
+ if (outs.length === 0) continue;
6698
+ const cfg = node.config ?? {};
6699
+ const declaredLabels = new Set(
6700
+ (Array.isArray(cfg.conditions) ? cfg.conditions : []).map((c) => typeof c?.label === "string" ? c.label.trim().toLowerCase() : "").filter(Boolean)
6701
+ );
6702
+ const edgeLabels = new Set(outs.map(edgeLabelOf).filter(Boolean));
6703
+ const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
6704
+ if (unclaimed.length > 0) {
6705
+ findings.push({
6706
+ where: `flow '${flowName}' \xB7 decision '${nid}'`,
6707
+ message: `declares branch label(s) ${unclaimed.map((l) => `'${l}'`).join(", ")} that no out-edge carries \u2014 out-edge labels are [${[...edgeLabels].map((l) => `'${l}'`).join(", ") || "none"}]. Traversal cannot honour a label nothing claims, so it falls back to considering EVERY out-edge and the branch the decision computed is ignored.`,
6708
+ hint: `Make an out-edge's \`label\` match the declared branch exactly, or drop \`config.conditions\` and branch on the edges instead (\`condition\` per branch + \`isDefault: true\` on the fallback) \u2014 one mechanism per decision, never both. (#4414)`,
6709
+ rule: FLOW_BRANCH_LABEL_UNMATCHED,
6710
+ // Gating: a label nothing claims cannot route under ANY reading, on
6711
+ // every run. See the severity policy at the top of this file.
6712
+ severity: "error"
6713
+ });
6714
+ }
6715
+ const gated = outs.filter((e) => e.condition || e.isDefault === true);
6716
+ if (gated.length === 0) continue;
6717
+ const ungated = outs.filter(
6718
+ (e) => !e.condition && e.isDefault !== true && !declaredLabels.has(edgeLabelOf(e))
6719
+ );
6720
+ if (ungated.length > 0) {
6721
+ findings.push({
6722
+ where: `flow '${flowName}' \xB7 decision '${nid}'`,
6723
+ message: `has guarded out-edge(s) alongside unconditional one(s) (${ungated.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 an unconditional out-edge is traversed on EVERY pass, in parallel with whichever guarded branch matched, so the decision does not actually exclude it. A \`label\` alone does not select a path unless the decision declares a matching \`conditions[].label\`.`,
6724
+ hint: `Mark the fallback \`isDefault: true\` so it is taken only when no sibling condition matched (BPMN default flow), or give it its own \`condition\`. (#4414)`,
6725
+ rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
6726
+ });
6727
+ }
6728
+ }
6729
+ }
6730
+ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
6731
+ const approvals = nodes.filter((n) => n.type === "approval");
6732
+ if (approvals.length === 0) return;
6733
+ const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
6734
+ const outEdges = /* @__PURE__ */ new Map();
6735
+ for (const e of edges) {
6736
+ const src = typeof e.source === "string" ? e.source : "";
6737
+ if (!src) continue;
6738
+ if (!outEdges.has(src)) outEdges.set(src, []);
6739
+ outEdges.get(src).push(e);
6740
+ }
6741
+ for (const a of approvals) {
6742
+ const aid = typeof a.id === "string" ? a.id : "";
6743
+ if (!aid) continue;
6744
+ const reviseTargets = edges.filter((e) => e.source === aid && edgeLabelOf(e) === "revise").map((e) => typeof e.target === "string" ? e.target : "").filter((t) => t && nodeIds.has(t));
6745
+ if (reviseTargets.length === 0) continue;
6746
+ const where = `flow '${flowName}' \xB7 approval '${aid}'`;
6747
+ const cfg = a.config ?? {};
6748
+ if (cfg.maxRevisions === 0) {
6749
+ findings.push({
6750
+ where,
6751
+ message: `declares a 'revise' out-edge but \`maxRevisions: 0\` disables send-back \u2014 every revise auto-rejects, so the revise branch never runs.`,
6752
+ hint: `Set \`maxRevisions\` >= 1 to allow N send-backs before auto-reject, or drop the 'revise' edge if send-back isn't intended (ADR-0044).`,
6753
+ rule: FLOW_APPROVAL_REVISE_DISABLED
6754
+ });
6755
+ }
6756
+ const seen = new Set(reviseTargets);
6757
+ const queue = [...reviseTargets];
6758
+ const returnEdges = [];
6759
+ while (queue.length) {
6760
+ const cur = queue.shift();
6761
+ for (const e of outEdges.get(cur) ?? []) {
6762
+ if (e.target === aid) returnEdges.push(e);
6763
+ const t = typeof e.target === "string" ? e.target : "";
6764
+ if (t && nodeIds.has(t) && !seen.has(t)) {
6765
+ seen.add(t);
6766
+ queue.push(t);
6767
+ }
6768
+ }
6769
+ }
6770
+ if (returnEdges.length === 0) {
6771
+ findings.push({
6772
+ where,
6773
+ message: `has a 'revise' out-edge but no path loops back to it \u2014 the submitter reworks the record with nowhere to resubmit, so the revise branch dead-ends. (registerFlow accepts this \u2014 it's a valid DAG.)`,
6774
+ hint: `Close the loop: the 'revise' edge should reach a wait node whose resubmit edge returns to '${aid}' marked \`type: 'back'\` (ADR-0044). See examples/app-showcase showcase_budget_approval.`,
6775
+ rule: FLOW_APPROVAL_REVISE_DEAD_END
6776
+ });
6777
+ } else if (!returnEdges.some((e) => e.type === "back")) {
6778
+ findings.push({
6779
+ where,
6780
+ message: `has a 'revise' loop that returns to it, but the closing edge isn't declared \`type: 'back'\` \u2014 registerFlow rejects this as an un-declared cycle.`,
6781
+ hint: `Mark the resubmit edge (whose target is '${aid}') \`type: 'back'\` so cycle validation skips it while it still traverses at runtime; \`maxRevisions\` guards the loop (ADR-0044).`,
6782
+ rule: FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE
6783
+ });
6784
+ }
6785
+ }
6786
+ }
6787
+ function lintFlowPatterns(stack) {
6788
+ const findings = [];
6789
+ for (const flow of asArray38(stack.flows)) {
6790
+ const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
6791
+ const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
6792
+ const edges = Array.isArray(flow.edges) ? flow.edges : [];
6793
+ const start = nodes.find((n) => n.type === "start");
6794
+ const startCfg = start?.config ?? {};
6795
+ const triggerType = typeof startCfg.triggerType === "string" ? startCfg.triggerType : "";
6796
+ if (triggerType.startsWith("record-")) {
6797
+ const src = conditionSource(startCfg.condition).trim();
6798
+ if (src && DATE_EQ.test(src)) {
6799
+ findings.push({
6800
+ where: `flow '${flowName}' \xB7 start condition`,
6801
+ message: `record-change trigger uses a date-EQUALITY time condition (\`${src}\`) \u2014 it only fires if the record happens to be written on that exact day, so unattended "N days before" rules never run.`,
6802
+ hint: `Use a SCHEDULE trigger (daily cron) + a range query instead \u2014 e.g. a scheduled flow whose get_record filters \`end_date\` BETWEEN {TODAY()} and {TODAY()+N}. (#1874)`,
6803
+ rule: FLOW_TIME_RELATIVE_ANTIPATTERN
6804
+ });
6805
+ }
6806
+ }
6807
+ const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
6808
+ const userLessKind = userLessTriggerKind(flow, startCfg);
6809
+ if (userLessKind && runAs !== "system") {
6810
+ const dataNode = nodes.find((n) => DATA_NODE_TYPES.has(typeof n.type === "string" ? n.type : ""));
6811
+ if (dataNode) {
6812
+ const declared = typeof flow.runAs === "string" ? `\`runAs:'${runAs}'\`` : `the default \`runAs:'user'\``;
6813
+ findings.push({
6814
+ where: `flow '${flowName}' \xB7 runAs`,
6815
+ message: `${userLessKind}-triggered flow runs as ${declared}, but a ${userLessKind} run has no trigger user \u2014 so its data node '${dataNode.id}' (${dataNode.type}) has no identity to scope to and will be REFUSED at run time.`,
6816
+ hint: `Declare \`runAs:'system'\` to make the elevation explicit and intended (the run reads/writes every record). A ${userLessKind} flow cannot scope to a user \u2014 there is none. (ADR-0049, ADR-0073 D5, #1888, #3760)`,
6817
+ rule: FLOW_RUNAS_UNSCOPED,
6818
+ severity: "error"
6819
+ });
6820
+ }
6821
+ }
6822
+ for (const node of nodes) {
6823
+ const nodeWhere = `flow '${flowName}' \xB7 node '${node.id}' (${node.type})`;
6824
+ const cfg = node.config ?? {};
6825
+ if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
6826
+ for (const key of Object.keys(cfg)) {
6827
+ if (PHANTOM_AGG_KEYS.has(key)) {
6828
+ findings.push({
6829
+ where: nodeWhere,
6830
+ message: `node config has \`${key}\` \u2014 the automation engine has no aggregate node, so \`${key}\` is silently ignored and this node computes nothing at runtime.`,
6831
+ hint: `Aggregation belongs in the data layer: use \`Field.summary\` for a cross-object rollup (sum/count of children), or \`Field.formula\` for a per-record computed value. (#1870)`,
6832
+ rule: FLOW_PHANTOM_AGGREGATION
6833
+ });
6834
+ }
6835
+ }
6836
+ const strings = [];
6837
+ collectTemplateStrings(node.config, void 0, strings);
6838
+ for (const str2 of strings) {
6839
+ if (DOUBLE_BRACE.test(str2)) {
6840
+ findings.push({
6841
+ where: nodeWhere,
6842
+ message: `double-brace interpolation \`${str2.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
6843
+ hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
6844
+ rule: FLOW_DOUBLE_BRACE_INTERP
6845
+ });
6846
+ }
6847
+ if (BARE_DOLLAR_REF.test(str2)) {
6848
+ findings.push({
6849
+ where: nodeWhere,
6850
+ message: `\`${str2.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
6851
+ hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
6852
+ rule: FLOW_BARE_DOLLAR_REF
6853
+ });
6854
+ }
6855
+ }
6856
+ }
6857
+ scanApprovalReviseLoops(flowName, nodes, edges, findings);
6858
+ scanErrorLabelledEdges(flowName, nodes, edges, findings);
6859
+ scanBranchRouting(flowName, nodes, edges, findings);
6860
+ }
6861
+ return findings;
6862
+ }
6863
+
6864
+ // src/lint-liveness-properties.ts
6865
+ import { createRequire as createRequire4 } from "module";
6866
+ import { dirname, join } from "path";
6867
+ import { existsSync, readFileSync } from "fs";
6868
+ var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
6869
+ var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
6870
+ function asArray39(v) {
6871
+ if (Array.isArray(v)) return v;
6872
+ if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
6873
+ return [];
6874
+ }
6875
+ function resolveLivenessDir() {
6876
+ try {
6877
+ const require2 = createRequire4(import.meta.url);
6878
+ const pkgJson = require2.resolve("@objectstack/spec/package.json");
6879
+ const dir = join(dirname(pkgJson), "liveness");
6880
+ return existsSync(dir) ? dir : null;
6881
+ } catch {
6882
+ return null;
6883
+ }
6884
+ }
6885
+ function loadWarnMap(dir, type) {
6886
+ const map = /* @__PURE__ */ new Map();
6887
+ const file = join(dir, `${type}.json`);
6888
+ if (!existsSync(file)) return map;
6889
+ let ledger;
6890
+ try {
6891
+ ledger = JSON.parse(readFileSync(file, "utf8"));
6892
+ } catch {
6893
+ return map;
6894
+ }
6895
+ const props = ledger.props || {};
6896
+ for (const [key, entry] of Object.entries(props)) {
6897
+ if (entry?.children) {
6898
+ for (const [ck, centry] of Object.entries(entry.children)) {
6899
+ if (shouldWarn(centry)) map.set(`${key}.${ck}`, centry);
6900
+ }
6901
+ }
6902
+ if (shouldWarn(entry)) map.set(key, entry);
6903
+ }
6904
+ return map;
6905
+ }
6906
+ function shouldWarn(entry) {
6907
+ if (!entry) return false;
6908
+ return entry.authorWarn === true || entry.status === "experimental";
6909
+ }
6910
+ function isAuthored(value) {
6911
+ if (value === void 0 || value === null) return false;
6912
+ if (typeof value === "boolean") return value === true;
6913
+ return true;
6914
+ }
6915
+ function describe(entry) {
6916
+ if (entry.status === "experimental") {
6917
+ return { kind: "is experimental \u2014 declared but NOT enforced at runtime", rule: LIVENESS_EXPERIMENTAL_PROPERTY };
6918
+ }
6919
+ return { kind: "has no runtime effect (liveness: dead)", rule: LIVENESS_DEAD_PROPERTY };
6920
+ }
6921
+ function checkItem(type, item, whereBase, warnMap, findings) {
6922
+ for (const [path, entry] of warnMap) {
6923
+ const values = path.includes(".") ? getNested(item, path) : [item[path]];
6924
+ for (const value of values instanceof Array ? values : [values]) {
6925
+ if (!isAuthored(value)) continue;
6926
+ const { kind, rule } = describe(entry);
6927
+ const hint = entry.authorHint ?? entry.note ?? "Remove it \u2014 it is declared in the spec but not consumed at runtime.";
6928
+ findings.push({
6929
+ where: whereBase,
6930
+ message: `sets \`${path}\` but this ${type} property ${kind}.`,
6931
+ hint,
6932
+ rule
6933
+ });
6934
+ break;
6935
+ }
6936
+ }
6937
+ }
6938
+ function getNested(obj, path) {
6939
+ let cur = [obj];
6940
+ for (const seg of path.split(".")) {
6941
+ const next = [];
6942
+ for (const c of cur) {
6943
+ if (c === null || typeof c !== "object") continue;
6944
+ const v = Array.isArray(c) ? void 0 : c[seg];
6945
+ if (Array.isArray(c)) {
6946
+ for (const el of c) {
6947
+ if (el && typeof el === "object") next.push(el[seg]);
6948
+ }
6949
+ } else {
6950
+ next.push(v);
6951
+ }
6952
+ }
6953
+ cur = next;
6954
+ }
6955
+ return cur.flatMap((v) => Array.isArray(v) ? v : [v]);
6956
+ }
6957
+ var TYPE_COLLECTIONS = [
6958
+ { type: "flow", key: "flows" },
6959
+ { type: "action", key: "actions" },
6960
+ { type: "agent", key: "agents" },
6961
+ { type: "tool", key: "tools" },
6962
+ { type: "skill", key: "skills" },
6963
+ { type: "dataset", key: "datasets" },
6964
+ { type: "permission", key: "permissions" },
6965
+ { type: "hook", key: "hooks" },
6966
+ { type: "page", key: "pages" },
6967
+ { type: "view", key: "views" },
6968
+ { type: "webhook", key: "webhooks" },
6969
+ // #4487. Note what adding a TYPE costs versus adding a warned property: the
6970
+ // doc below is right that coverage grows by marking entries `authorWarn` —
6971
+ // but only WITHIN a type already listed here. A newly governed type needs its
6972
+ // collection registered or its ledger warns nobody, which would leave the
6973
+ // ledger correct and silent: the exact shape this lint exists to prevent.
6974
+ { type: "datasource", key: "datasources" },
6975
+ // #4488 — the six newly governed types that carry `authorWarn` entries.
6976
+ // (doc / seed / validation are governed too but warn on nothing today, so
6977
+ // they are not listed; add them here the day one of their entries warns.)
6978
+ { type: "app", key: "apps" },
6979
+ { type: "book", key: "books" },
6980
+ { type: "job", key: "jobs" },
6981
+ { type: "email_template", key: "emailTemplates" },
6982
+ { type: "mapping", key: "mappings" },
6983
+ { type: "translation", key: "translations" }
6984
+ ];
6985
+ function lintLivenessProperties(stack) {
6986
+ const dir = resolveLivenessDir();
6987
+ if (!dir) return [];
6988
+ const findings = [];
6989
+ const objectWarn = loadWarnMap(dir, "object");
6990
+ const fieldWarn = loadWarnMap(dir, "field");
6991
+ for (const obj of asArray39(stack.objects)) {
6992
+ const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6993
+ if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
6994
+ if (fieldWarn.size > 0) {
6995
+ for (const field of asArray39(obj.fields)) {
6996
+ const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
6997
+ checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
6998
+ }
6999
+ }
7000
+ }
7001
+ for (const { type, key } of TYPE_COLLECTIONS) {
7002
+ const warnMap = loadWarnMap(dir, type);
7003
+ if (warnMap.size === 0) continue;
7004
+ for (const item of asArray39(stack[key])) {
7005
+ const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
7006
+ checkItem(type, item, `${type} '${name}'`, warnMap, findings);
7007
+ }
7008
+ }
7009
+ return findings;
7010
+ }
7011
+
7012
+ // src/lint-autonumber-formats.ts
7013
+ import { parseAutonumberFormat, referencedFields } from "@objectstack/spec/data";
7014
+ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
7015
+ var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
7016
+ var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
7017
+ var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
7018
+ function asArray40(v) {
7019
+ if (Array.isArray(v)) return v;
7020
+ if (v && typeof v === "object") {
7021
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7022
+ }
7023
+ return [];
7024
+ }
7025
+ function lintAutonumberFormats(stack) {
7026
+ const findings = [];
7027
+ for (const obj of asArray40(stack.objects)) {
7028
+ const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
7029
+ const fields = asArray40(obj.fields);
7030
+ const fieldMeta = /* @__PURE__ */ new Map();
7031
+ for (const f of fields) {
7032
+ if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
7033
+ }
7034
+ for (const f of fields) {
7035
+ if (f.type !== "autonumber") continue;
7036
+ const name = typeof f.name === "string" ? f.name : "(unnamed field)";
7037
+ const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
7038
+ if (!fmt) continue;
7039
+ const tokens = parseAutonumberFormat(fmt);
7040
+ const refs = referencedFields(tokens);
7041
+ const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
7042
+ for (const t of tokens) {
7043
+ if (t.kind !== "literal") continue;
7044
+ const braced = t.text.match(/\{[^{}]*\}/g);
7045
+ if (!braced) continue;
7046
+ for (const tok of braced) {
7047
+ const body = tok.slice(1, -1);
7048
+ const isExtraSeq = /^0+$/.test(body);
7049
+ findings.push({
7050
+ where,
7051
+ message: isExtraSeq ? `format has a second sequence slot \`${tok}\` \u2014 only the first \`{0..0}\` counts; this one renders literally as "${tok}".` : `format has an unrecognized token \`${tok}\` \u2014 it is not a counter/date/{field} token, so it renders literally as "${tok}" in every record number.`,
7052
+ hint: isExtraSeq ? `Use a single \`{0000}\` slot; fold any second number into a literal or a {field} token.` : `Date tokens are case-sensitive and exact: {YYYY} {YY} {MM} {DD} {YYYYMMDD} (no spaces/punctuation inside). For a field value use {field_name}.`,
7053
+ rule: AUTONUMBER_LITERAL_TOKEN,
7054
+ severity: "warning"
7055
+ });
7056
+ }
7057
+ }
7058
+ for (const ref of refs) {
7059
+ if (ref === name) {
7060
+ findings.push({
7061
+ where,
7062
+ message: `format interpolates \`{${ref}}\` \u2014 its own value, which does not exist yet when the number is generated.`,
7063
+ hint: `Reference a DIFFERENT field that is set before create (e.g. \`{plan_no}{000}\`), or drop the token.`,
7064
+ rule: AUTONUMBER_SELF_REFERENCE,
7065
+ severity: "error"
7066
+ });
7067
+ continue;
7068
+ }
7069
+ const meta = fieldMeta.get(ref);
7070
+ if (!meta) {
7071
+ findings.push({
7072
+ where,
7073
+ message: `format interpolates \`{${ref}}\`, but object '${objectName}' has no field named '${ref}' \u2014 generation will always throw.`,
7074
+ hint: `Reference an existing field, or remove the \`{${ref}}\` token from the format.`,
7075
+ rule: AUTONUMBER_UNKNOWN_FIELD,
7076
+ severity: "error"
7077
+ });
7078
+ } else if (!meta.required) {
7079
+ findings.push({
7080
+ where,
7081
+ message: `format interpolates \`{${ref}}\`, but '${ref}' is optional \u2014 any record left blank fails autonumber generation at create time.`,
7082
+ hint: `Mark '${ref}' as \`required: true\` so it is always set before the record number is rendered.`,
7083
+ rule: AUTONUMBER_OPTIONAL_FIELD,
7084
+ severity: "warning"
7085
+ });
7086
+ }
7087
+ }
7088
+ }
7089
+ }
7090
+ return findings;
7091
+ }
7092
+
7093
+ // src/lint-view-refs.ts
7094
+ import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
7095
+ function asArray41(v) {
7096
+ if (Array.isArray(v)) return v;
7097
+ if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
7098
+ return [];
7099
+ }
7100
+ var VIEW_KEY_COLLISION = "view-key-collision";
7101
+ var VIEW_REF_FORM_TARGET_MISSING = "view-ref-form-target-missing";
7102
+ var VIEW_REF_FORM_TARGET_KIND = "view-ref-form-target-kind";
7103
+ function containerFromObject(obj) {
7104
+ return { list: obj.list, form: obj.form, listViews: obj.listViews, formViews: obj.formViews };
7105
+ }
7106
+ function viewContainerObjectName(item) {
7107
+ if (typeof item.name === "string" && item.name) return item.name;
7108
+ if (typeof item.id === "string" && item.id) return item.id;
7109
+ if (typeof item.object === "string" && item.object) return item.object;
7110
+ if (typeof item.list?.data?.object === "string") return item.list.data.object;
7111
+ if (typeof item.form?.data?.object === "string") return item.form.data.object;
7112
+ return void 0;
7113
+ }
7114
+ function lintViewRefs(stack) {
7115
+ const findings = [];
7116
+ const viewKinds = /* @__PURE__ */ new Map();
7117
+ const indexKind = (name, kind) => {
7118
+ let s = viewKinds.get(name);
7119
+ if (!s) viewKinds.set(name, s = /* @__PURE__ */ new Set());
7120
+ s.add(kind);
7121
+ };
7122
+ const containers = [];
7123
+ for (const v of asArray41(stack.views)) {
7124
+ if (v.viewKind) {
7125
+ if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
7126
+ continue;
7127
+ }
7128
+ if (!isAggregatedViewContainer(v)) continue;
7129
+ const object = viewContainerObjectName(v);
7130
+ if (object) containers.push({ object, container: v });
7131
+ }
7132
+ for (const obj of asArray41(stack.objects)) {
7133
+ const object = typeof obj.name === "string" ? obj.name : void 0;
7134
+ if (!object) continue;
7135
+ if (obj.list || obj.form || obj.listViews || obj.formViews) {
7136
+ containers.push({ object, container: containerFromObject(obj) });
7137
+ }
7138
+ }
7139
+ for (const { object, container } of containers) {
7140
+ const { items, collisions } = expandViewContainerWithDiagnostics(object, container);
7141
+ for (const it of items) indexKind(it.name, it.viewKind);
7142
+ for (const col of collisions) {
7143
+ findings.push({
7144
+ where: `object '${object}' \xB7 view key '${col.key}'`,
7145
+ message: `View key collision: the ${col.viewKind} view '${col.requested}' clashes with another view in the same container and was renamed to '${col.renamedTo}'. Anything referencing '${col.requested}' (a form action target, a navigation viewName) resolves to the OTHER view, not this one.`,
7146
+ hint: `Give the ${col.viewKind} view a unique key \u2014 the default list implicitly claims '<object>.default'. Renaming key '${col.key}' fixes both this collision and any reference that targets it.`,
7147
+ rule: VIEW_KEY_COLLISION,
7148
+ severity: "warning"
7149
+ });
7150
+ }
7151
+ }
7152
+ const seenFormTargets = /* @__PURE__ */ new Set();
7153
+ const checkAction = (action, ownerObject) => {
7154
+ if (!action || action.type !== "form") return;
7155
+ const target = action.target;
7156
+ if (typeof target !== "string" || !target) return;
7157
+ if (target.includes("${")) return;
7158
+ if (!target.includes(".")) return;
7159
+ const actionName = typeof action.name === "string" ? action.name : "(unnamed)";
7160
+ const dedupeKey = `${actionName}\0${target}`;
7161
+ if (seenFormTargets.has(dedupeKey)) return;
7162
+ seenFormTargets.add(dedupeKey);
7163
+ const where = ownerObject ? `action '${actionName}' on object '${ownerObject}'` : `action '${actionName}'`;
7164
+ const kinds = viewKinds.get(target);
7165
+ if (!kinds) {
7166
+ findings.push({
7167
+ where,
7168
+ message: `Form action target '${target}' does not resolve to any view. A type:'form' action must point at an existing form view; at runtime this opens a blank/broken form.`,
7169
+ hint: `Check for a typo, or a form view renamed by a key collision. Expected a form view named '<object>.<formViewKey>'.`,
7170
+ rule: VIEW_REF_FORM_TARGET_MISSING,
7171
+ severity: "warning"
7172
+ });
7173
+ return;
7174
+ }
7175
+ if (!kinds.has("form")) {
7176
+ const actual = [...kinds].join("/");
7177
+ findings.push({
7178
+ where,
7179
+ message: `Form action target '${target}' resolves to a ${actual} view, not a form view. Opening a ${actual} view through a form action renders an empty form (and a submit can silently no-op) at runtime.`,
7180
+ hint: `Point target at a form view (viewKind 'form'). If the form view was renamed by a key collision, fix the colliding key so '${target}' names the form again.`,
7181
+ rule: VIEW_REF_FORM_TARGET_KIND,
7182
+ severity: "error"
7183
+ });
7184
+ }
7185
+ };
7186
+ for (const obj of asArray41(stack.objects)) {
7187
+ const object = typeof obj.name === "string" ? obj.name : void 0;
7188
+ for (const action of asArray41(obj.actions)) checkAction(action, object);
7189
+ }
7190
+ for (const action of asArray41(stack.actions)) checkAction(action);
7191
+ return findings;
7192
+ }
7193
+
7194
+ // src/data-model-rules.ts
7195
+ var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
7196
+ var NUMERIC_TYPES = /* @__PURE__ */ new Set([
7197
+ "number",
7198
+ "currency",
7199
+ "integer",
7200
+ "decimal",
7201
+ "percent",
7202
+ "float",
7203
+ "double"
7204
+ ]);
7205
+ var OPTION_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multiselect", "radio", "enum"]);
7206
+ var NAME_LIKE_FIELDS = ["name", "title", "subject", "label", "full_name", "display_name", "code"];
7207
+ var LINE_ITEM_RE = /_(line|lines|line_item|line_items|item|items|detail|details|entry|entries)$/;
7208
+ var ASSOCIATION_TOKENS = [
7209
+ "comment",
7210
+ "attachment",
7211
+ "note",
7212
+ "log",
7213
+ "audit",
7214
+ "activity",
7215
+ "activities",
7216
+ "history",
7217
+ "event",
7218
+ "reaction",
7219
+ "like",
7220
+ "mention",
7221
+ "notification",
7222
+ "message"
7223
+ ];
7224
+ function isLineItemName(name) {
7225
+ return LINE_ITEM_RE.test(name);
7226
+ }
7227
+ function isAssociationName(name) {
7228
+ const lc = name.toLowerCase();
7229
+ return ASSOCIATION_TOKENS.some((t) => lc === t || lc.endsWith(`_${t}`) || lc.endsWith(`_${t}s`));
7230
+ }
7231
+ function fieldEntries2(fields) {
7232
+ if (!fields) return [];
7233
+ if (Array.isArray(fields)) {
7234
+ return fields.filter((f) => f && f.name != null).map((f) => ({ name: String(f.name), def: f }));
7235
+ }
7236
+ return Object.entries(fields).map(([name, def]) => ({ name, def }));
7237
+ }
7238
+ function refOf2(def) {
7239
+ return def?.reference || def?.reference_to;
7240
+ }
7241
+ var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
7242
+ function uniqueDeclared(u) {
7243
+ return u === true || u === "global";
7244
+ }
7245
+ function lintUniqueDeclarations(objects) {
7246
+ const issues = [];
7247
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
7248
+ for (let i = 0; i < objects.length; i++) {
7249
+ const obj = objects[i];
7250
+ if (!obj?.name) continue;
7251
+ const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
7252
+ if (declaredIndexes.length === 0) continue;
7253
+ const singleColumnUniqueIndexes = /* @__PURE__ */ new Map();
7254
+ for (const idx of declaredIndexes) {
7255
+ if (!uniqueDeclared(idx?.unique)) continue;
7256
+ const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
7257
+ if (cols.length !== 1) continue;
7258
+ if (!singleColumnUniqueIndexes.has(cols[0])) singleColumnUniqueIndexes.set(cols[0], idx);
7259
+ }
7260
+ if (singleColumnUniqueIndexes.size === 0) continue;
7261
+ for (const { name, def } of fieldEntries2(obj.fields)) {
7262
+ if (!uniqueDeclared(def?.unique)) continue;
7263
+ if (def.unique === "global") continue;
7264
+ const idx = singleColumnUniqueIndexes.get(name);
7265
+ if (!idx) continue;
7266
+ const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
7267
+ issues.push({
7268
+ severity: "warning",
7269
+ rule: UNIQUE_DOUBLE_DECLARATION,
7270
+ message: `"${obj.name}.${name}" declares field-level \`unique: true\` AND a single-column unique index${indexLabel} on the same column. Since #3696 the field-level form is scoped per tenant \u2014 \`(tenant, ${name})\` \u2014 while a declared index is materialized over exactly its \`fields\`, i.e. platform-wide. On a tenant-scoped object the global index wins and the per-tenant constraint can never be reached; on a tenancy-less object the two are the same index declared twice. Either way one of the two declarations has no effect.`,
7271
+ path: `objects[${i}]`,
7272
+ fix: `Pick the intent: for platform-wide uniqueness set \`unique: 'global'\` on '${name}' and drop the duplicate index; for per-tenant uniqueness drop the index (the field-level declaration already builds the tenant composite), or spell the index out as \`fields: ['organization_id', '${name}']\` if you want it explicit.`
7273
+ });
7274
+ }
7275
+ }
7276
+ return issues;
7277
+ }
7278
+ function lintDataModel(objects) {
7279
+ const issues = lintUniqueDeclarations(objects);
7280
+ if (!Array.isArray(objects) || objects.length === 0) return issues;
7281
+ const childrenByParent = {};
7282
+ for (const child of objects) {
7283
+ if (!child?.name) continue;
7284
+ for (const { name: fieldName, def } of fieldEntries2(child.fields)) {
7285
+ if (!RELATIONSHIP_TYPES.has(def?.type)) continue;
7286
+ const parent = refOf2(def);
7287
+ if (!parent) continue;
7288
+ (childrenByParent[parent] || (childrenByParent[parent] = [])).push({ child, fieldName, def });
7289
+ }
7290
+ }
7291
+ for (let i = 0; i < objects.length; i++) {
7292
+ const obj = objects[i];
7293
+ if (!obj?.name) continue;
7294
+ const objPath = `objects[${i}]`;
7295
+ const fields = fieldEntries2(obj.fields);
7296
+ const hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
7297
+ if (fields.length > 0 && !hasNameField) {
7298
+ issues.push({
7299
+ severity: "suggestion",
7300
+ rule: "object/missing-name-field",
7301
+ message: `Object "${obj.name}" has no name/title field or primaryField \u2014 records will display as raw IDs`,
7302
+ path: `${objPath}.fields`
7303
+ });
7304
+ }
7305
+ for (const { name: fieldName, def } of fields) {
7306
+ if (!def || typeof def !== "object") continue;
7307
+ const fieldPath = `${objPath}.fields.${fieldName}`;
7308
+ const type = def.type;
7309
+ if (OPTION_FIELD_TYPES.has(type)) {
7310
+ const hasOptions = Array.isArray(def.options) && def.options.length > 0 || !!def.optionsFrom || !!def.dataSource || !!def.reference;
7311
+ if (!hasOptions) {
7312
+ issues.push({
7313
+ severity: "warning",
7314
+ rule: "field/select-missing-options",
7315
+ message: `${type} field "${obj.name}.${fieldName}" has no options`,
7316
+ path: `${fieldPath}.options`
7317
+ });
7318
+ }
7319
+ }
7320
+ if (!RELATIONSHIP_TYPES.has(type)) continue;
7321
+ const parent = refOf2(def);
7322
+ if (!parent) {
7323
+ issues.push({
7324
+ severity: "error",
7325
+ rule: "relationship/missing-reference",
7326
+ message: `${type} field "${obj.name}.${fieldName}" is missing a reference target`,
7327
+ path: `${fieldPath}.reference`
7328
+ });
7329
+ continue;
7330
+ }
7331
+ if (type === "master_detail") {
7332
+ if (def.required !== true) {
7333
+ issues.push({
7334
+ severity: "warning",
7335
+ rule: "relationship/master-detail-required",
7336
+ message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should be required (a detail record cannot exist without its master)`,
7337
+ path: `${fieldPath}.required`,
7338
+ fix: "required: true"
7339
+ });
7340
+ }
7341
+ if (def.deleteBehavior === void 0) {
7342
+ issues.push({
7343
+ severity: "suggestion",
7344
+ rule: "relationship/delete-behavior",
7345
+ message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should declare deleteBehavior (cascade/restrict/set_null)`,
7346
+ path: `${fieldPath}.deleteBehavior`,
7347
+ fix: "deleteBehavior: 'cascade'"
7348
+ });
7349
+ }
7350
+ if (isLineItemName(obj.name) && def.inlineEdit !== true) {
7351
+ issues.push({
7352
+ severity: "suggestion",
7353
+ rule: "relationship/line-items-inline-edit",
7354
+ message: `"${obj.name}" looks like line items of ${parent}; consider inlineEdit: true on "${fieldName}" so it is entered inline within the ${parent} form`,
7355
+ path: `${fieldPath}.inlineEdit`,
7356
+ fix: "inlineEdit: true"
7357
+ });
7358
+ }
7359
+ }
7360
+ if (type === "lookup" && isLineItemName(obj.name)) {
7361
+ issues.push({
7362
+ severity: "suggestion",
7363
+ rule: "relationship/line-item-should-be-master-detail",
7364
+ message: `"${obj.name}" looks like line items of ${parent} but uses lookup; master_detail gives ownership + cascade + roll-ups`,
7365
+ path: `${fieldPath}.type`,
7366
+ fix: "type: 'master_detail'"
7367
+ });
7368
+ }
7369
+ if (def.inlineEdit === true && isAssociationName(obj.name)) {
7370
+ issues.push({
7371
+ severity: "warning",
7372
+ rule: "relationship/association-inline-edit",
7373
+ message: `"${obj.name}" is an association (comments/audit/activity), not line items \u2014 inlineEdit clutters the ${parent} entry form; surface it as a detail-page related list instead`,
7374
+ path: `${fieldPath}.inlineEdit`,
7375
+ fix: "remove inlineEdit (use relatedList on the detail page)"
7376
+ });
7377
+ }
7378
+ }
7379
+ const children = childrenByParent[obj.name] || [];
7380
+ const summaryChildObjects = new Set(
7381
+ fields.filter((f) => f.def?.type === "summary").map((f) => f.def?.summaryOperations?.object || f.def?.reference).filter(Boolean)
7382
+ );
7383
+ const seenSuggestedChild = /* @__PURE__ */ new Set();
7384
+ for (const { child, def } of children) {
7385
+ if (def?.type !== "master_detail") continue;
7386
+ if (!child?.name || seenSuggestedChild.has(child.name)) continue;
7387
+ if (summaryChildObjects.has(child.name)) continue;
7388
+ const numericChildField = fieldEntries2(child.fields).find((f) => NUMERIC_TYPES.has(f.def?.type));
7389
+ if (!numericChildField) continue;
7390
+ seenSuggestedChild.add(child.name);
7391
+ issues.push({
7392
+ severity: "suggestion",
7393
+ rule: "rollup/missing-summary",
7394
+ message: `"${obj.name}" owns "${child.name}" (master_detail) with numeric field "${numericChildField.name}" but has no roll-up summary; consider a summary field (count/sum) on ${obj.name}`,
7395
+ path: `${objPath}.fields`,
7396
+ fix: `summary field aggregating ${child.name}.${numericChildField.name}`
7397
+ });
7398
+ }
7399
+ }
7400
+ return issues;
7401
+ }
7402
+
7403
+ // src/authoring-rules.ts
7404
+ var AUTHORING_COMMANDS = ["validate", "build", "lint"];
7405
+ var AUTHORING_SURFACES = ["cli", "runtime-publish"];
7406
+ var ALL = AUTHORING_COMMANDS;
7407
+ var CLI_ONLY = ["cli"];
7408
+ var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
7409
+ 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.";
7410
+ 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.";
7411
+ 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.";
7412
+ var EXPRESSION_INVALID = "expression-invalid";
7413
+ var AUTHORING_RULES = [
7414
+ // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
7415
+ // are parsed for syntax AND checked that each `record.<field>` resolves. This
7416
+ // is what catches a BARE field ref (`done` instead of `record.done`) that
7417
+ // would otherwise silently hide an action on every record (#2183/#2185).
7418
+ {
7419
+ name: "validateStackExpressions",
7420
+ tier: "gating",
7421
+ input: "parsed",
7422
+ commands: ALL,
7423
+ source: "packages/lint/src/validate-expressions.ts",
7424
+ // Runtime publish gate (#4463): the EXPRESSION family. On a flow write it
7425
+ // checks every script-node callable and every declared predicate the flow
7426
+ // carries, against the live object universe — the same parse `os build`
7427
+ // runs, now at the door Studio/REST/MCP authors actually use.
7428
+ surfaces: CLI_AND_RUNTIME,
7429
+ runtimeTypes: ["flow"],
7430
+ run: (stack) => validateStackExpressions(stack).map((i) => ({
7431
+ severity: i.severity ?? "error",
7432
+ rule: EXPRESSION_INVALID,
7433
+ where: i.where,
7434
+ path: i.where,
7435
+ message: i.message,
7436
+ hint: `source: \`${i.source}\``
7437
+ }))
7438
+ },
7439
+ // ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
7440
+ // mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
7441
+ // read the pre-parse tier or the evidence is already gone.
7442
+ {
7443
+ name: "validateListViewMode",
7444
+ tier: "gating",
7445
+ input: "normalized",
7446
+ commands: ALL,
7447
+ source: "packages/lint/src/validate-list-view-mode.ts",
7448
+ surfaces: CLI_ONLY,
7449
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7450
+ run: (stack) => validateListViewMode(stack)
7451
+ },
7452
+ // [ADR-0078] A Zod-VALID instance that silently does nothing: a `summary`
7453
+ // with no `summaryOperations`, a `lookup` with no `reference`, a `select`
7454
+ // with no `options`. Every key is one we know, so #4001's unknown-key
7455
+ // rejection cannot see it, and the liveness ledger cannot either (it is
7456
+ // per-property; the properties ARE live). This is the gate between them.
7457
+ //
7458
+ // `gating` because the error-severity shapes are fully inert — the field
7459
+ // reads 0 forever while authoring reports success, which is the failure the
7460
+ // ADR was written for (cloud#687). Pre-parse so the findings survive an
7461
+ // unrelated schema error elsewhere in the stack.
7462
+ {
7463
+ name: "validateFunctionalCompleteness",
7464
+ tier: "gating",
7465
+ input: "normalized",
7466
+ commands: ALL,
7467
+ source: "packages/lint/src/validate-functional-completeness.ts",
7468
+ surfaces: CLI_ONLY,
7469
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7470
+ run: (stack) => validateFunctionalCompleteness(stack)
7471
+ },
7472
+ // A flat list-view object in `views: []` parses to an EMPTY container
7473
+ // (ViewSchema strips unknown keys): the schema step passes, zero views
7474
+ // register, and the Console renders nothing. Pre-parse for the same reason.
7475
+ {
7476
+ name: "validateViewContainers",
7477
+ tier: "gating",
7478
+ input: "normalized",
7479
+ commands: ALL,
7480
+ source: "packages/lint/src/validate-view-containers.ts",
7481
+ surfaces: CLI_ONLY,
7482
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7483
+ run: (stack) => validateViewContainers(stack)
7484
+ },
7485
+ // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
7486
+ // chartConfig axis/series must resolve against the declared datasets.
7487
+ {
7488
+ name: "validateWidgetBindings",
7489
+ tier: "gating",
7490
+ input: "parsed",
7491
+ commands: ALL,
7492
+ source: "packages/lint/src/validate-widget-bindings.ts",
7493
+ surfaces: CLI_ONLY,
7494
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7495
+ run: (stack) => validateWidgetBindings(stack)
7496
+ },
7497
+ // ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
7498
+ // target that resolves to no defined action ships a button that renders and
7499
+ // silently does nothing on click. Unresolved `url` routes stay advisory.
7500
+ {
7501
+ name: "validateDashboardActionRefs",
7502
+ tier: "gating",
7503
+ input: "parsed",
7504
+ commands: ALL,
7505
+ source: "packages/lint/src/validate-dashboard-action-refs.ts",
7506
+ surfaces: CLI_ONLY,
7507
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7508
+ run: (stack) => validateDashboardActionRefs(stack)
7509
+ },
7510
+ // #3574 — a filter value like `{current_user}` resolves in no vocabulary,
7511
+ // reaches the data engine as a literal and matches nothing. The surface
7512
+ // renders empty with no error, and a silent zero is indistinguishable from a
7513
+ // genuine one at review time.
7514
+ {
7515
+ name: "validateFilterTokens",
7516
+ tier: "gating",
7517
+ input: "parsed",
7518
+ commands: ALL,
7519
+ source: "packages/lint/src/validate-filter-tokens.ts",
7520
+ surfaces: CLI_ONLY,
7521
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7522
+ run: (stack) => validateFilterTokens(stack)
7523
+ },
7524
+ // The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
7525
+ // rules that answer "does this name resolve to anything?". It reached all
7526
+ // three commands before this file existed; it is an entry here so the two
7527
+ // registries compose instead of competing, and so its members are covered by
7528
+ // the same guard as everything else.
7529
+ {
7530
+ name: "validateReferenceIntegrity",
7531
+ tier: "gating",
7532
+ input: "parsed",
7533
+ commands: ALL,
7534
+ source: "packages/lint/src/reference-integrity-suite.ts",
7535
+ // Runtime publish gate (#4463): the REFERENCE family. On a flow snapshot the
7536
+ // members that answer a flow question run (`validateFlowTemplatePaths`,
7537
+ // `validateFlowNodeWrites`, `validateReadonlyFlowWrites`,
7538
+ // `validateObjectReferences`); the page/nav/AI members see no such
7539
+ // collection in the snapshot and return nothing, and the two that would
7540
+ // load `typescript` need a hook/action/react body the snapshot never
7541
+ // carries — which is what `runtime-lazy-deps.test.ts` pins.
7542
+ surfaces: CLI_AND_RUNTIME,
7543
+ runtimeTypes: ["flow"],
7544
+ run: (stack) => validateReferenceIntegrity(stack)
7545
+ },
7546
+ // ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
7547
+ // `id`), name real CSS properties + design tokens, and carry a `large` base.
7548
+ {
7549
+ name: "validateResponsiveStyles",
7550
+ tier: "gating",
7551
+ input: "parsed",
7552
+ commands: ALL,
7553
+ source: "packages/lint/src/validate-responsive-styles.ts",
7554
+ surfaces: CLI_ONLY,
7555
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7556
+ run: (stack) => validateResponsiveStyles(stack)
7557
+ },
7558
+ // ADR-0080 — a `kind:'jsx'` page's `source` is parsed (never executed) and
7559
+ // compiled to the SDUI tree at save time, so malformed source must fail loudly
7560
+ // here (ADR-0078) instead of being stored and breaking only at render.
7561
+ {
7562
+ name: "validateJsxPages",
7563
+ tier: "gating",
7564
+ input: "parsed",
7565
+ commands: ALL,
7566
+ source: "packages/lint/src/validate-jsx-pages.ts",
7567
+ surfaces: CLI_ONLY,
7568
+ surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
7569
+ run: (stack, ctx) => validateJsxPages(stack, ctx.sduiManifest ? { manifest: ctx.sduiManifest } : {})
7570
+ },
7571
+ // ADR-0081 — a `kind:'react'` page's `source` is real React executed at
7572
+ // render. Transpiled here (Sucrase, never executed) so a syntax error fails at
7573
+ // author time, not at render. Lazy: only a stack with such a page pays.
7574
+ {
7575
+ name: "validateReactPages",
7576
+ tier: "gating",
7577
+ input: "parsed",
7578
+ commands: ALL,
7579
+ source: "packages/lint/src/validate-react-pages.ts",
7580
+ surfaces: CLI_ONLY,
7581
+ surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
7582
+ run: (stack) => validateReactPages(stack)
7583
+ },
7584
+ // ADR-0065, source tier — Tailwind `className` in a `kind:'html'`/`'react'`
7585
+ // page silently no-ops (the build never scans authored metadata).
7586
+ {
7587
+ name: "validatePageSourceStyling",
7588
+ tier: "advisory",
7589
+ input: "parsed",
7590
+ commands: ALL,
7591
+ source: "packages/lint/src/validate-page-source-styling.ts",
7592
+ surfaces: CLI_ONLY,
7593
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7594
+ run: (stack) => validatePageSourceStyling(stack)
7595
+ },
7596
+ // ADR-0066 ⑨ — a `requiredPermissions` entry naming a capability registered
7597
+ // nowhere fails closed at runtime. Advisory: another installed package may
7598
+ // legitimately provide it.
7599
+ {
7600
+ name: "validateCapabilityReferences",
7601
+ tier: "advisory",
7602
+ input: "parsed",
7603
+ commands: ALL,
7604
+ source: "packages/lint/src/validate-capability-references.ts",
7605
+ surfaces: CLI_ONLY,
7606
+ surfaceReason: 'P2 (#4463): the ONE rule the runtime universe makes strictly stronger \u2014 the advisory hedge ("another installed package may provide it") is decidable against the live capability registry, so it graduates from advisory to gating there rather than merely being ported. That promotion is a severity change on a published rule id and belongs in its own PR, not riding a wiring change.',
7607
+ run: (stack) => validateCapabilityReferences(stack)
7608
+ },
7609
+ // A record-change flow whose start-node objectName matches nothing never
7610
+ // fires — silently. Reads the pre-parse tier so an author sees what they
7611
+ // wrote. Advisory: the object may come from another installed package.
7612
+ {
7613
+ name: "validateFlowTriggerReadiness",
7614
+ tier: "advisory",
7615
+ input: "normalized",
7616
+ commands: ALL,
7617
+ source: "packages/lint/src/validate-flow-trigger-readiness.ts",
7618
+ // Runtime publish gate (#4463): the FLOW family. Advisory at this surface
7619
+ // too — its findings are logged, not thrown (P1 gates on `error` only; P2
7620
+ // puts advisories on the response for Studio to render).
7621
+ surfaces: CLI_AND_RUNTIME,
7622
+ runtimeTypes: ["flow"],
7623
+ run: (stack) => validateFlowTriggerReadiness(stack)
7624
+ },
7625
+ // ADR-0090 D3 fallout — an approval `{ type: 'role' }` resolves against the
7626
+ // better-auth org-membership tier, not positions, so a position name authored
7627
+ // there routes the approval to nobody; and an expression approver that does
7628
+ // not parse can never resolve. The rule whose absence from `os build` and
7629
+ // `os validate` was #4409's worked example: it gates, and it ran on `os lint`
7630
+ // alone, so a broken approval flow built and published green.
7631
+ {
7632
+ name: "validateApprovalApprovers",
7633
+ tier: "gating",
7634
+ input: "parsed",
7635
+ commands: ALL,
7636
+ source: "packages/lint/src/validate-approval-approvers.ts",
7637
+ // Runtime publish gate (#4463): the APPROVAL family, and the issue's worked
7638
+ // example both times. #4409 fixed it for `os build`; a tenant saving the
7639
+ // same broken expression approver from Studio still sailed through, because
7640
+ // `approver.value` is just a string to Zod. It is not just a string here.
7641
+ surfaces: CLI_AND_RUNTIME,
7642
+ runtimeTypes: ["flow"],
7643
+ run: (stack) => validateApprovalApprovers(stack)
7644
+ },
7645
+ // ADR-0079 — `titleFormat` is retired in favour of `nameField`, and an object
7646
+ // with no resolvable title ships records with no meaningful name. Advisory:
7647
+ // auto-provision and the `Record #<id>` floor keep it from ever being fatal.
7648
+ {
7649
+ name: "validateRecordTitle",
7650
+ tier: "advisory",
7651
+ input: "parsed",
7652
+ commands: ALL,
7653
+ source: "packages/lint/src/validate-record-title.ts",
7654
+ surfaces: CLI_ONLY,
7655
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7656
+ run: (stack) => validateRecordTitle(stack)
7657
+ },
7658
+ // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
7659
+ // into the object's field map; a dangling one is Zod-valid and silently inert
7660
+ // at render. Advisory: every consumer degrades gracefully.
7661
+ {
7662
+ name: "validateSemanticRoles",
7663
+ tier: "advisory",
7664
+ input: "parsed",
7665
+ commands: ALL,
7666
+ source: "packages/lint/src/validate-semantic-roles.ts",
7667
+ surfaces: CLI_ONLY,
7668
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7669
+ run: (stack) => validateSemanticRoles(stack)
7670
+ },
7671
+ // #2578 / #4449 — a form section's field reference that resolves to nothing
7672
+ // (silently not rendered) and an absolute `colSpan` under a per-surface
7673
+ // derived column count. Advisory: the renderer skips the unknown field and
7674
+ // clamps the span, so nothing is broken — but each is almost certainly an
7675
+ // authoring mistake, and until #4449 this rule ran on no command at all.
7676
+ // Pure structured-metadata walk (no lazy dependency), so wiring it to all
7677
+ // three costs nothing measurable.
7678
+ {
7679
+ name: "validateFormLayout",
7680
+ tier: "advisory",
7681
+ input: "parsed",
7682
+ commands: ALL,
7683
+ source: "packages/lint/src/validate-form-layout.ts",
7684
+ surfaces: CLI_ONLY,
7685
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7686
+ run: (stack) => validateFormLayout(stack)
7687
+ },
7688
+ // ADR-0078 Phase 3 (Tier-A `action-locations`) — an action that declares no
7689
+ // `locations` and that no view places by name renders on no surface at all.
7690
+ // objectui#3142 made that measurable: four renderers used to show an
7691
+ // undeclared action anyway, and now none does. Advisory: a view in another
7692
+ // installed package may be the one placing it, and `locations: []` (the
7693
+ // documented headless shape) is deliberately never flagged.
7694
+ {
7695
+ name: "validateActionLocations",
7696
+ tier: "advisory",
7697
+ input: "parsed",
7698
+ commands: ALL,
7699
+ source: "packages/lint/src/validate-action-locations.ts",
7700
+ surfaces: CLI_ONLY,
7701
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7702
+ run: (stack) => validateActionLocations(stack)
7703
+ },
7704
+ // framework#3434 — seeds replay on every boot, so a `mode: 'insert'` dataset
7705
+ // duplicates its table on every restart.
7706
+ {
7707
+ name: "validateSeedReplaySafety",
7708
+ tier: "advisory",
7709
+ input: "parsed",
7710
+ commands: ALL,
7711
+ source: "packages/lint/src/validate-seed-replay-safety.ts",
7712
+ surfaces: CLI_ONLY,
7713
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7714
+ run: (stack) => validateSeedReplaySafety(stack)
7715
+ },
7716
+ // framework#3433 follow-up — #3433 exempts seed writes from the
7717
+ // `state_machine` rule, so a seeded status the FSM does not declare is no
7718
+ // longer rejected at write time. Re-added at author time; advisory, because
7719
+ // the exemption itself is legitimate.
7720
+ {
7721
+ name: "validateSeedStateMachine",
7722
+ tier: "advisory",
7723
+ input: "parsed",
7724
+ commands: ALL,
7725
+ source: "packages/lint/src/validate-seed-state-machine.ts",
7726
+ surfaces: CLI_ONLY,
7727
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7728
+ run: (stack) => validateSeedStateMachine(stack)
7729
+ },
7730
+ // ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
7731
+ // Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
7732
+ // during parse, so the alias the author wrote is gone from `result.data`.
7733
+ {
7734
+ name: "validateVisibilityPredicates",
7735
+ tier: "advisory",
7736
+ input: "normalized",
7737
+ commands: ALL,
7738
+ source: "packages/lint/src/validate-visibility-predicates.ts",
7739
+ surfaces: CLI_ONLY,
7740
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7741
+ run: (stack) => validateVisibilityPredicates(stack)
7742
+ },
7743
+ // #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
7744
+ // `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
7745
+ // runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
7746
+ // `flow-default-edge-with-condition` (#4414 — a declaration that is inert, so
7747
+ // the route silently differs from what the author wrote). The bar for
7748
+ // promoting one is stated at the top of `lint-flow-patterns.ts`.
7749
+ {
7750
+ name: "lintFlowPatterns",
7751
+ tier: "gating",
7752
+ input: "parsed",
7753
+ commands: ALL,
7754
+ source: "packages/lint/src/lint-flow-patterns.ts",
7755
+ // Runtime publish gate (#4463): the FLOW family's anti-pattern half. Its
7756
+ // three `error` rules are the sharpest fit for a publish gate that exists —
7757
+ // `flow-runas-unscoped` is metadata the automation engine REFUSES to
7758
+ // execute, so publishing it can only ever produce a broken flow.
7759
+ surfaces: CLI_AND_RUNTIME,
7760
+ runtimeTypes: ["flow"],
7761
+ run: (stack) => lintFlowPatterns(stack).map((f) => ({
7762
+ severity: f.severity ?? "warning",
7763
+ rule: f.rule,
7764
+ where: f.where,
7765
+ path: f.where,
7766
+ message: f.message,
7767
+ hint: f.hint
7768
+ }))
7769
+ },
7770
+ // The spec-liveness loop on the author side: a property the ledger marks
7771
+ // dead-and-misleading or experimental is set hopefully and does nothing.
7772
+ // Ledger-driven (entries opt in via `authorWarn`), so it is high-signal and
7773
+ // never fatal.
7774
+ {
7775
+ name: "lintLivenessProperties",
7776
+ tier: "advisory",
7777
+ input: "parsed",
7778
+ commands: ALL,
7779
+ source: "packages/lint/src/lint-liveness-properties.ts",
7780
+ surfaces: CLI_ONLY,
7781
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7782
+ run: (stack) => lintLivenessProperties(stack).map((f) => ({
7783
+ severity: "warning",
7784
+ rule: f.rule,
7785
+ where: f.where,
7786
+ path: f.where,
7787
+ message: f.message,
7788
+ hint: f.hint
7789
+ }))
7790
+ },
7791
+ // A format like `{plan_no}{000}` makes the referenced field part of the
7792
+ // counter scope, so it must exist and be set at create time. Unknown field →
7793
+ // broken (error); optional field → fragile (warning).
7794
+ {
7795
+ name: "lintAutonumberFormats",
7796
+ tier: "gating",
7797
+ input: "parsed",
7798
+ commands: ALL,
7799
+ source: "packages/lint/src/lint-autonumber-formats.ts",
7800
+ surfaces: CLI_ONLY,
7801
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7802
+ run: (stack) => lintAutonumberFormats(stack).map((f) => ({
7803
+ severity: f.severity,
7804
+ rule: f.rule,
7805
+ where: f.where,
7806
+ path: f.where,
7807
+ message: f.message,
7808
+ hint: f.hint
7809
+ }))
7810
+ },
7811
+ // #2554 — a `type:'form'` action target naming a missing or LIST view opens a
7812
+ // broken form at runtime; a list/form view-key collision silently renames one
7813
+ // view so references resolve to the OTHER. Both are broken.
7814
+ {
7815
+ name: "lintViewRefs",
7816
+ tier: "gating",
7817
+ input: "parsed",
7818
+ commands: ALL,
7819
+ source: "packages/lint/src/lint-view-refs.ts",
7820
+ surfaces: CLI_ONLY,
7821
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7822
+ run: (stack) => lintViewRefs(stack).map((f) => ({
7823
+ severity: f.severity,
7824
+ rule: f.rule,
7825
+ where: f.where,
7826
+ path: f.where,
7827
+ message: f.message,
7828
+ hint: f.hint
7829
+ }))
7830
+ },
7831
+ // #3991 — a column carrying BOTH a field-level `unique: true` and a
7832
+ // single-column declared unique index has two intents, of which exactly one
7833
+ // takes effect (the global index wins; the tenant composite is unreachable).
7834
+ {
7835
+ name: "lintUniqueDeclarations",
7836
+ tier: "advisory",
7837
+ input: "parsed",
7838
+ commands: ["validate", "build"],
7839
+ source: "packages/lint/src/data-model-rules.ts",
7840
+ surfaces: CLI_ONLY,
7841
+ surfaceReason: RUNTIME_OBJECT_WRITES_P2,
7842
+ scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly as R10 of its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
7843
+ run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
7844
+ severity: f.severity === "suggestion" ? "info" : f.severity,
7845
+ rule: f.rule,
7846
+ where: f.path,
7847
+ path: f.path,
7848
+ message: f.message,
7849
+ hint: f.fix ?? ""
7850
+ }))
7851
+ },
7852
+ // ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
7853
+ // a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
7854
+ // binding gate, vocabulary freeze), moving the failure from a runtime deny to
7855
+ // an author-time fix-it. Per ADR-0049 this is not advisory security.
7856
+ {
7857
+ name: "validateSecurityPosture",
7858
+ tier: "gating",
7859
+ input: "parsed",
7860
+ commands: ALL,
7861
+ source: "packages/lint/src/validate-security-posture.ts",
7862
+ surfaces: CLI_ONLY,
7863
+ surfaceReason: "Already gated at this surface by a DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on `object` (`registerAuthoringGate`) that enforces the same OWD posture rules on every runtime write. Running the linter here as well would double-report one refusal in two vocabularies. Consolidating the two onto this table is P2 (#4463), and is a merge, not a hole.",
7864
+ run: (stack) => validateSecurityPosture(stack)
7865
+ },
7866
+ // ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
7867
+ // sharing rule that walks it builds a second permission hierarchy (the
7868
+ // dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
7869
+ // so it grants nothing it appears to.
7870
+ {
7871
+ name: "validateOrgAxisRedLines",
7872
+ tier: "gating",
7873
+ input: "parsed",
7874
+ commands: ALL,
7875
+ source: "packages/lint/src/validate-org-axis-red-lines.ts",
7876
+ surfaces: CLI_ONLY,
7877
+ surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
7878
+ run: (stack) => validateOrgAxisRedLines(stack)
7879
+ }
7880
+ ];
7881
+ function authoringRulesFor(command) {
7882
+ return AUTHORING_RULES.filter((r) => r.commands.includes(command));
7883
+ }
7884
+ function runAuthoringRules(command, run) {
7885
+ const findings = [];
7886
+ const ctx = { sduiManifest: run.sduiManifest };
7887
+ for (const rule of authoringRulesFor(command)) {
7888
+ const stack = rule.input === "normalized" ? run.normalized : run.parsed ?? run.normalized;
7889
+ findings.push(...rule.run(stack, ctx));
7890
+ }
7891
+ return findings;
7892
+ }
7893
+ function splitBySeverity(findings) {
7894
+ return {
7895
+ errors: findings.filter((f) => f.severity === "error"),
7896
+ advisories: findings.filter((f) => f.severity !== "error")
7897
+ };
7898
+ }
7899
+
7900
+ // src/runtime-gate.ts
7901
+ var TYPE_TO_STACK_KEY = {
7902
+ flow: "flows",
7903
+ object: "objects",
7904
+ view: "views",
7905
+ action: "actions",
7906
+ page: "pages",
7907
+ dashboard: "dashboards",
7908
+ agent: "agents",
7909
+ hook: "hooks",
7910
+ seed: "seeds"
7911
+ };
7912
+ function runtimeAuthoringRulesFor(type) {
7913
+ return AUTHORING_RULES.filter(
7914
+ (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
7915
+ );
7916
+ }
7917
+ function runtimeGatedTypes() {
7918
+ const types = /* @__PURE__ */ new Set();
7919
+ for (const rule of AUTHORING_RULES) {
7920
+ if (!rule.surfaces.includes("runtime-publish")) continue;
7921
+ for (const t of rule.runtimeTypes ?? []) types.add(t);
7922
+ }
7923
+ return [...types].sort();
7924
+ }
7925
+ function stackKeyForType(type) {
7926
+ return TYPE_TO_STACK_KEY[type] ?? null;
7927
+ }
7928
+ var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
7929
+ function runRules(rules, stack, ctx) {
7930
+ const findings = [];
7931
+ for (const rule of rules) {
7932
+ try {
7933
+ findings.push(...rule.run(stack, ctx));
7934
+ } catch (err) {
7935
+ findings.push({
7936
+ severity: "warning",
7937
+ rule: "authoring-rule-threw",
7938
+ where: rule.name,
7939
+ path: rule.source,
7940
+ message: `rule ${rule.name} threw while judging this write: ${err instanceof Error ? err.message : String(err)}`,
7941
+ hint: "This is a bug in the rule, not in the metadata \u2014 the write was not blocked by it. Please report it with the body that triggered it."
7942
+ });
7943
+ }
7944
+ }
7945
+ return findings;
7946
+ }
7947
+ function runRuntimeAuthoringRules(args) {
7948
+ const rules = runtimeAuthoringRulesFor(args.type);
7949
+ const empty = { errors: [], advisories: [], rulesRun: [] };
7950
+ if (rules.length === 0) return empty;
7951
+ const stackKey = stackKeyForType(args.type);
7952
+ if (!stackKey) return empty;
7953
+ if (!args.item || typeof args.item !== "object") return empty;
7954
+ const item = args.item;
7955
+ const itemName = typeof item.name === "string" ? item.name : void 0;
7956
+ const contextObjects = args.context?.objects ?? [];
7957
+ const ctx = { sduiManifest: args.sduiManifest };
7958
+ const writesIntoContext = stackKey === "objects";
7959
+ const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
7960
+ const baseline = { objects: baselineObjects };
7961
+ const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
7962
+ const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
7963
+ const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
7964
+ return {
7965
+ errors: added.filter((f) => f.severity === "error"),
7966
+ advisories: added.filter((f) => f.severity !== "error"),
7967
+ rulesRun: rules.map((r) => r.name)
7968
+ };
7969
+ }
5806
7970
  export {
5807
7971
  ACTION_BODY_WRITE_EXCLUSIONS,
5808
7972
  ACTION_BODY_WRITE_PATTERNS,
5809
7973
  ACTION_BODY_WRITE_PATTERN_IDS,
5810
7974
  ACTION_BODY_WRITE_UNKNOWN_FIELD,
5811
7975
  ACTION_NAME_UNDEFINED,
7976
+ ACTION_NO_PLACEMENT,
5812
7977
  ACTION_RECORD_WRITE_DISCARDED,
5813
7978
  ACTION_RECORD_WRITE_PATTERNS,
5814
7979
  ACTION_RECORD_WRITE_PATTERN_IDS,
@@ -5824,6 +7989,13 @@ export {
5824
7989
  APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
5825
7990
  APPROVAL_EXPRESSION_INVALID,
5826
7991
  APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
7992
+ AUTHORING_COMMANDS,
7993
+ AUTHORING_RULES,
7994
+ AUTHORING_SURFACES,
7995
+ AUTONUMBER_LITERAL_TOKEN,
7996
+ AUTONUMBER_OPTIONAL_FIELD,
7997
+ AUTONUMBER_SELF_REFERENCE,
7998
+ AUTONUMBER_UNKNOWN_FIELD,
5827
7999
  CAPABILITY_REFERENCE_UNKNOWN,
5828
8000
  CHART_AXIS_NOT_SELECTED,
5829
8001
  CHART_CONFIG_MISSING,
@@ -5834,13 +8006,29 @@ export {
5834
8006
  DASHBOARD_ACTION_ROUTE_UNRESOLVED,
5835
8007
  DASHBOARD_ACTION_TARGET_UNDEFINED,
5836
8008
  DASHBOARD_FILTER_FIELD_UNKNOWN,
8009
+ EXPRESSION_INVALID,
5837
8010
  FIELD_GROUP_EMPTY,
5838
8011
  FIELD_GROUP_UNDECLARED,
5839
8012
  FILTER_TOKEN_UNKNOWN,
8013
+ FLOW_APPROVAL_REVISE_DEAD_END,
8014
+ FLOW_APPROVAL_REVISE_DISABLED,
8015
+ FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
8016
+ FLOW_BARE_DOLLAR_REF,
8017
+ FLOW_BRANCH_LABEL_UNMATCHED,
8018
+ FLOW_DATE_EQUALITY_FILTER,
8019
+ FLOW_DECISION_UNCONDITIONAL_BRANCH,
8020
+ FLOW_DEFAULT_EDGE_WITH_CONDITION,
8021
+ FLOW_DOUBLE_BRACE_INTERP,
5840
8022
  FLOW_DRAFT_STATUS_AMBIGUOUS,
8023
+ FLOW_ERROR_LABEL_NOT_FAULT,
8024
+ FLOW_INERT_NODE_CONDITION,
8025
+ FLOW_MULTIPLE_DEFAULT_EDGES,
5841
8026
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
8027
+ FLOW_PHANTOM_AGGREGATION,
8028
+ FLOW_RUNAS_UNSCOPED,
5842
8029
  FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
5843
8030
  FLOW_TEMPLATE_UNKNOWN_FIELD,
8031
+ FLOW_TIME_RELATIVE_ANTIPATTERN,
5844
8032
  FLOW_TRIGGER_UNKNOWN_OBJECT,
5845
8033
  FLOW_UPDATE_READONLY_FIELD,
5846
8034
  FLOW_UPDATE_READONLY_WHEN_FIELD,
@@ -5853,19 +8041,25 @@ export {
5853
8041
  HOOK_BODY_WRITE_PATTERN_IDS,
5854
8042
  HOOK_BODY_WRITE_UNKNOWN_FIELD,
5855
8043
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
8044
+ LIVENESS_DEAD_PROPERTY,
8045
+ LIVENESS_EXPERIMENTAL_PROPERTY,
5856
8046
  MEASURE_AGGREGATE_INCOHERENT,
5857
8047
  NAV_OBJECT_UNGRANTED,
8048
+ NAV_TARGET_UNRESOLVED,
8049
+ NULL_GUARD_HINT,
5858
8050
  OBJECT_REFERENCE_UNKNOWN,
5859
8051
  OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
5860
8052
  ORG_AXIS_CROSS_ORG_BU_GRANT,
5861
8053
  ORG_AXIS_PERMISSION_INHERITANCE,
5862
8054
  PAGE_FIELD_UNKNOWN,
5863
8055
  PAGE_SOURCE_CLASSNAME,
8056
+ REACT_BLOCK_NEEDS_RECORD_CONTEXT,
5864
8057
  REACT_CHART_AGGREGATE_INVALID,
5865
8058
  REACT_CHART_AXIS_UNKNOWN,
5866
8059
  REACT_CHART_FIELD_UNKNOWN,
5867
8060
  REFERENCE_INTEGRITY_RULES,
5868
8061
  SEARCHABLE_FIELD_UNKNOWN,
8062
+ SEARCHABLE_FIELD_UNSEARCHABLE,
5869
8063
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
5870
8064
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
5871
8065
  SECURITY_DELEGATION_MISSING_REASON,
@@ -5890,17 +8084,37 @@ export {
5890
8084
  TITLE_UNRESOLVABLE,
5891
8085
  TRANSLATION_OPTION_KEY_UNKNOWN,
5892
8086
  TRANSLATION_TARGET_UNKNOWN,
8087
+ UNIQUE_DOUBLE_DECLARATION,
5893
8088
  VIEW_CONTAINER_SHAPE,
8089
+ VIEW_KEY_COLLISION,
8090
+ VIEW_REF_FORM_TARGET_KIND,
8091
+ VIEW_REF_FORM_TARGET_MISSING,
5894
8092
  VISIBILITY_ALIAS_DEPRECATED,
5895
8093
  VISIBILITY_ROOT_MISLAYERED,
5896
8094
  WIDGET_DATASET_UNKNOWN,
5897
8095
  WIDGET_DIMENSION_UNKNOWN,
5898
8096
  WIDGET_MEASURE_UNKNOWN,
8097
+ authoringRulesFor,
5899
8098
  buildAccessMatrix,
5900
8099
  diffAccessMatrix,
5901
8100
  extractHookBodyWriteSet,
5902
8101
  extractHookBodyWrites,
8102
+ findUnguardedNullableOperands,
8103
+ lintAutonumberFormats,
8104
+ lintDataModel,
8105
+ lintFlowPatterns,
8106
+ lintLivenessProperties,
8107
+ lintUniqueDeclarations,
8108
+ lintViewRefs,
8109
+ nullGuardMessage,
8110
+ runAuthoringRules,
8111
+ runRuntimeAuthoringRules,
8112
+ runtimeAuthoringRulesFor,
8113
+ runtimeGatedTypes,
8114
+ splitBySeverity,
8115
+ stackKeyForType,
5903
8116
  validateActionBodyWrites,
8117
+ validateActionLocations,
5904
8118
  validateActionNameRefs,
5905
8119
  validateAiAgentAuthoring,
5906
8120
  validateAiSurfaceAffinity,
@@ -5914,10 +8128,12 @@ export {
5914
8128
  validateFlowTemplatePaths,
5915
8129
  validateFlowTriggerReadiness,
5916
8130
  validateFormLayout,
8131
+ validateFunctionalCompleteness,
5917
8132
  validateHookBodyWrites,
5918
8133
  validateJsxPages,
5919
8134
  validateListViewMode,
5920
8135
  validateNavAccess,
8136
+ validateNavTargetRefs,
5921
8137
  validateObjectReferences,
5922
8138
  validateOrgAxisRedLines,
5923
8139
  validatePageFieldBindings,