@objectstack/lint 17.0.0 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -250,10 +250,10 @@ var OUTCOME_CLAUSE = {
250
250
  "fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
251
251
  "fail-open": "so the predicate is SKIPPED fail-open \u2014 the field is never actually required, the write proceeds unchecked, and the only trace is a `requiredWhen \u2026 failed to evaluate \u2014 skipped` log line (#4649/#4811)"
252
252
  };
253
- function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
253
+ function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
254
254
  const owner = objectName ? `'${objectName}'` : "this object";
255
- const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
256
- 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 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
255
+ const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
256
+ return `${subject} applies \`${finding2.operator}\` to \`${finding2.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding2.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
257
257
  }
258
258
 
259
259
  // src/validate-expressions.ts
@@ -436,10 +436,10 @@ function validateStackExpressions(stack) {
436
436
  if (!nullableFields || nullableFields.size === 0) return;
437
437
  const source = celSourceOf(raw);
438
438
  if (!source) return;
439
- for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
439
+ for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
440
440
  issues.push({
441
441
  where,
442
- message: nullGuardMessage(subject, objectName, finding, outcome),
442
+ message: nullGuardMessage(subject, objectName, finding2, outcome),
443
443
  source,
444
444
  severity: "error"
445
445
  });
@@ -463,7 +463,7 @@ function validateStackExpressions(stack) {
463
463
  const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
464
464
  const FIELD_RULE_SLOT_CONSEQUENCE = {
465
465
  visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
466
- readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). Per ADR-0057 D10 the server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
466
+ readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). The server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
467
467
  requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
468
468
  // Listed rather than left to the `??` below, so the map covers every slot
469
469
  // the field walk passes and the default stays unreachable. `FieldSchema`
@@ -522,14 +522,15 @@ function validateStackExpressions(stack) {
522
522
  if (retired.length > 0) {
523
523
  issues.push({
524
524
  where: `${at} \xB7 node '${node.id}' (script) callable`,
525
- message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's
526
- // behaviour, never the retired key's fate — "rewrite it" reads two ways
527
- // over a branch that DELETES the key (template/recipients/variables/script),
528
- // "rewrite existing sources" only one. Plain-quoted (not a template literal)
525
+ message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled), reworded under #9529: the house
526
+ // sentence names the TOOL's behaviour, never the retired key's fate —
527
+ // "rewrite it" read two ways over a branch that DELETES the key
528
+ // (template/recipients/variables/script), and the tool never rewrote a
529
+ // source file at all. Plain-quoted (not a template literal)
529
530
  // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
530
531
  // widened scan (#7030) on the same textual shape as the spec corpus — no
531
532
  // interpolation lives in this clause, so nothing is lost switching quote style.
532
- "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
533
+ "Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
533
534
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
534
535
  });
535
536
  } else if (!fn) {
@@ -600,10 +601,10 @@ function validateStackExpressions(stack) {
600
601
  f.expression,
601
602
  objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
602
603
  );
603
- const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
604
- for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
605
- for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
606
- warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
604
+ const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
605
+ for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
606
+ for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
607
+ warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
607
608
  }
608
609
  }
609
610
  }
@@ -1259,7 +1260,6 @@ function asArray4(v) {
1259
1260
  function strName(v) {
1260
1261
  return typeof v === "string" && v.length > 0 ? v : void 0;
1261
1262
  }
1262
- var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
1263
1263
  var URL_COLLECTION_TO_STACK_KEY = {
1264
1264
  object: "objects",
1265
1265
  objects: "objects",
@@ -1304,13 +1304,10 @@ function collectKnownTargets(stack) {
1304
1304
  return { actions, objects, reports, dashboards, pages, views };
1305
1305
  }
1306
1306
  function resolveActionTarget(actionType, target, known) {
1307
- if (known.actions.has(target)) return true;
1308
1307
  if (actionType === "modal") {
1309
- if (known.objects.has(target)) return true;
1310
- const m = MODAL_VERB_RE.exec(target);
1311
- if (m && known.objects.has(m[1])) return true;
1308
+ return known.pages.has(target);
1312
1309
  }
1313
- return false;
1310
+ return known.actions.has(target);
1314
1311
  }
1315
1312
  function resolveUrlRoute(target, known) {
1316
1313
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
@@ -1340,14 +1337,13 @@ function validateDashboardActionRefs(stack) {
1340
1337
  const actionType = strName(action.actionType) ?? "url";
1341
1338
  if (actionType === "script" || actionType === "modal") {
1342
1339
  if (resolveActionTarget(actionType, target, known)) return;
1343
- const kindWord = actionType === "script" ? "script" : "modal";
1344
1340
  findings.push({
1345
1341
  severity: "error",
1346
1342
  rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
1347
1343
  where,
1348
1344
  path,
1349
- message: `${kindWord} action target "${target}" resolves to no defined action` + (actionType === "modal" ? " or object" : "") + `. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
1350
- hint: actionType === "modal" ? `Define an action named "${target}" (stack.actions or the object's actions), use the "<verb>_<object>" convention against a real object (e.g. "create_<object>"), point actionUrl at an existing object, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
1345
+ message: actionType === "modal" ? `modal action target "${target}" names no declared page \u2014 a modal target names a PAGE, only (objectstack#6739). The button renders but the runtime refuses the dispatch when clicked \u2014 a dangling reference (ADR-0049: a declared reference must resolve).` : `script action target "${target}" resolves to no defined action. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
1346
+ hint: actionType === "modal" ? `Point actionUrl at a declared page (stack.pages), or use actionType: 'form' with an "<object>.<view>" form-view target to open an object's form, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
1351
1347
  });
1352
1348
  return;
1353
1349
  }
@@ -1389,7 +1385,7 @@ function validateDashboardActionRefs(stack) {
1389
1385
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
1390
1386
 
1391
1387
  // src/filter-walk.ts
1392
- var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
1388
+ var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
1393
1389
  function asArray5(v) {
1394
1390
  if (Array.isArray(v)) return v;
1395
1391
  if (v && typeof v === "object") {
@@ -1493,6 +1489,149 @@ function validateFilterTokens(stack) {
1493
1489
  return out;
1494
1490
  }
1495
1491
 
1492
+ // src/validate-preset-comparands.ts
1493
+ import {
1494
+ VALID_AST_OPERATORS,
1495
+ bareDateRangePresetComparandMessage,
1496
+ canonicalAstOperator,
1497
+ isDateRangePresetName
1498
+ } from "@objectstack/spec/data";
1499
+ import { normalizeFilterOperator } from "@objectstack/spec/ui";
1500
+ var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
1501
+ var PRESET_COMPARAND_SURFACES = [
1502
+ { key: "dashboards", kind: "dashboard" },
1503
+ { key: "objects", kind: "object" },
1504
+ { key: "views", kind: "view" },
1505
+ { key: "reports", kind: "report" },
1506
+ { key: "datasets", kind: "dataset" },
1507
+ { key: "pages", kind: "page" },
1508
+ { key: "apps", kind: "app" },
1509
+ { key: "flows", kind: "flow" }
1510
+ ];
1511
+ var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
1512
+ var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
1513
+ var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
1514
+ "greater_than",
1515
+ "greater_than_or_equal",
1516
+ "less_than",
1517
+ "less_than_or_equal",
1518
+ "before",
1519
+ "after"
1520
+ ]);
1521
+ var MAX_DEPTH = 32;
1522
+ function isPlainObject(v) {
1523
+ return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
1524
+ }
1525
+ function finding(where, path, preset, operator) {
1526
+ return {
1527
+ severity: "error",
1528
+ rule: FILTER_PRESET_COMPARAND,
1529
+ where,
1530
+ path,
1531
+ message: bareDateRangePresetComparandMessage(preset, operator),
1532
+ hint: "Presets belong to the dashboard date-filter bar (dateRange.defaultRange, a date global filter's defaultValue). In a filter comparand, write the {date-macro} window the message names, or an ISO date."
1533
+ };
1534
+ }
1535
+ function judgeConditionNode(node, path, where, out, depth) {
1536
+ if (depth > MAX_DEPTH) return;
1537
+ for (const [key, value] of Object.entries(node)) {
1538
+ const here = `${path}.${key}`;
1539
+ if (key === "$and" || key === "$or") {
1540
+ if (Array.isArray(value)) {
1541
+ value.forEach((arm, i) => {
1542
+ if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
1543
+ });
1544
+ }
1545
+ continue;
1546
+ }
1547
+ if (key === "$not") {
1548
+ if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
1549
+ continue;
1550
+ }
1551
+ if (key.startsWith("$")) continue;
1552
+ if (!isPlainObject(value)) continue;
1553
+ const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
1554
+ if (!hasOps) {
1555
+ judgeConditionNode(value, here, where, out, depth + 1);
1556
+ continue;
1557
+ }
1558
+ for (const [op, comparand] of Object.entries(value)) {
1559
+ if (ORDERING_DOLLAR_OPS.has(op) && isDateRangePresetName(comparand)) {
1560
+ out.push(finding(where, `${here}.${op}`, comparand, op));
1561
+ continue;
1562
+ }
1563
+ if (op === "$between" && Array.isArray(comparand)) {
1564
+ comparand.forEach((endpoint, i) => {
1565
+ if (isDateRangePresetName(endpoint)) {
1566
+ out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
1567
+ }
1568
+ });
1569
+ }
1570
+ }
1571
+ }
1572
+ }
1573
+ function judgeFilterRule(rule, path, where, out) {
1574
+ const operator = normalizeFilterOperator(rule.operator);
1575
+ if (typeof operator !== "string") return;
1576
+ const value = rule.value;
1577
+ if (ORDERING_RULE_OPS.has(operator) && isDateRangePresetName(value)) {
1578
+ out.push(finding(where, `${path}.value`, value, operator));
1579
+ return;
1580
+ }
1581
+ if (operator === "between" && Array.isArray(value)) {
1582
+ value.forEach((endpoint, i) => {
1583
+ if (isDateRangePresetName(endpoint)) {
1584
+ out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
1585
+ }
1586
+ });
1587
+ }
1588
+ }
1589
+ function judgeTriple(triple, path, where, out) {
1590
+ const op = triple[1];
1591
+ if (typeof op !== "string") return;
1592
+ const canonical = canonicalAstOperator(op);
1593
+ const value = triple[2];
1594
+ if (ORDERING_INFIX_OPS.has(canonical) && isDateRangePresetName(value)) {
1595
+ out.push(finding(where, `${path}[2]`, value, op));
1596
+ return;
1597
+ }
1598
+ if (canonical === "between" && Array.isArray(value)) {
1599
+ value.forEach((endpoint, i) => {
1600
+ if (isDateRangePresetName(endpoint)) {
1601
+ out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
1602
+ }
1603
+ });
1604
+ }
1605
+ }
1606
+ function judgeFilterValue(node, path, where, out, depth) {
1607
+ if (depth > MAX_DEPTH) return;
1608
+ if (Array.isArray(node)) {
1609
+ if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
1610
+ judgeTriple(node, path, where, out);
1611
+ return;
1612
+ }
1613
+ node.forEach((member, i) => {
1614
+ if (typeof member === "string") return;
1615
+ judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
1616
+ });
1617
+ return;
1618
+ }
1619
+ if (!isPlainObject(node)) return;
1620
+ if (typeof node.field === "string" && typeof node.operator === "string") {
1621
+ judgeFilterRule(node, path, where, out);
1622
+ return;
1623
+ }
1624
+ judgeConditionNode(node, path, where, out, depth);
1625
+ }
1626
+ function validatePresetComparands(stack) {
1627
+ if (!stack || typeof stack !== "object") return [];
1628
+ const out = [];
1629
+ walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
1630
+ judgeFilterValue(value, path, where, out, 0);
1631
+ });
1632
+ return out;
1633
+ }
1634
+
1496
1635
  // src/validate-empty-combinators.ts
1497
1636
  import { reduceFilterVerdict } from "@objectstack/spec/data";
1498
1637
  var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
@@ -1816,6 +1955,7 @@ import {
1816
1955
  } from "@objectstack/spec/data";
1817
1956
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
1818
1957
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
1958
+ var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
1819
1959
  function asArray7(v) {
1820
1960
  if (Array.isArray(v)) return v;
1821
1961
  if (v && typeof v === "object") {
@@ -1906,7 +2046,7 @@ function indexObjectSearchTargets(stack) {
1906
2046
  }
1907
2047
  return fieldsByObject;
1908
2048
  }
1909
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
2049
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
1910
2050
  const findings = [];
1911
2051
  if (!Array.isArray(declared) || declared.length === 0) return findings;
1912
2052
  if (!objectName) return findings;
@@ -1914,6 +2054,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1914
2054
  const target = fieldsByObject.get(objectName);
1915
2055
  if (!target) return findings;
1916
2056
  const known = target.names;
2057
+ const anchors = unprovisionedAnchors?.get(objectName);
1917
2058
  const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
1918
2059
  for (let i = 0; i < declared.length; i++) {
1919
2060
  const entry = declared[i];
@@ -1931,6 +2072,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
1931
2072
  });
1932
2073
  continue;
1933
2074
  }
2075
+ if (anchors?.has(name)) {
2076
+ findings.push({
2077
+ severity: "warning",
2078
+ rule: SEARCHABLE_FIELD_UNPROVISIONED,
2079
+ where,
2080
+ path: `${path}[${i}]`,
2081
+ message: `${subject} entry "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)}` + (role === "narrowing" ? ` \u2014 clients echo this declaration verbatim as the '$searchFields' override, so every toolbar search on this list scans a column that is empty on every record: it reads as search coverage and matches nothing.` : ` \u2014 'search' scans it on every record and it can never match, so the object's searchable set is narrower than it declares. Should it be the ONLY entry that resolves, the set scans nothing at all.`),
2082
+ hint: unprovisionedAnchorHint(objectName, name)
2083
+ });
2084
+ }
1934
2085
  if (isVirtualSearchField(target.fields[name])) {
1935
2086
  const vtype = target.fields[name]?.type;
1936
2087
  findings.push({
@@ -1984,9 +2135,19 @@ function validateSearchableFields(stack) {
1984
2135
  if (!isRec3(stack)) return findings;
1985
2136
  const objects = asArray7(stack.objects);
1986
2137
  const fieldsByObject = indexObjectSearchTargets(stack);
2138
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
1987
2139
  const check = (declared, objectName, where, path, subject, role) => {
1988
2140
  findings.push(
1989
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
2141
+ ...checkSearchableFieldList(
2142
+ declared,
2143
+ objectName,
2144
+ fieldsByObject,
2145
+ where,
2146
+ path,
2147
+ subject,
2148
+ role,
2149
+ unprovisionedAnchors
2150
+ )
1990
2151
  );
1991
2152
  };
1992
2153
  for (let oi = 0; oi < objects.length; oi++) {
@@ -2055,33 +2216,203 @@ function listViewObject(listView) {
2055
2216
  return isRec3(data) ? strName3(data.object) : void 0;
2056
2217
  }
2057
2218
 
2058
- // src/page-walk.ts
2219
+ // src/validate-sortable-fields.ts
2220
+ import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
2221
+ var SORT_FIELD_UNKNOWN = "sort-field-unknown";
2222
+ var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
2059
2223
  function isRec4(v) {
2060
2224
  return !!v && typeof v === "object" && !Array.isArray(v);
2061
2225
  }
2062
2226
  function strName4(v) {
2063
2227
  return typeof v === "string" && v.length > 0 ? v : void 0;
2064
2228
  }
2229
+ function readSortKeys(declared) {
2230
+ const fromShorthand = (raw, at) => {
2231
+ const trimmed = raw.trim();
2232
+ if (!trimmed) return void 0;
2233
+ const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
2234
+ return bare ? { field: bare, at } : void 0;
2235
+ };
2236
+ if (typeof declared === "string") {
2237
+ return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
2238
+ }
2239
+ if (Array.isArray(declared)) {
2240
+ const keys = [];
2241
+ for (let i = 0; i < declared.length; i++) {
2242
+ const el = declared[i];
2243
+ if (typeof el === "string") {
2244
+ const k = fromShorthand(el, `[${i}]`);
2245
+ if (k) keys.push(k);
2246
+ continue;
2247
+ }
2248
+ if (!isRec4(el)) continue;
2249
+ const field = strName4(el.field);
2250
+ if (field) keys.push({ field: field.trim(), at: `[${i}]` });
2251
+ }
2252
+ return keys;
2253
+ }
2254
+ return [];
2255
+ }
2256
+ function suggest4(target, known) {
2257
+ let best;
2258
+ let bestScore = Infinity;
2259
+ for (const candidate of known) {
2260
+ const d = distance3(target, candidate);
2261
+ if (d < bestScore) {
2262
+ bestScore = d;
2263
+ best = candidate;
2264
+ }
2265
+ }
2266
+ const limit = Math.max(2, Math.floor(target.length / 3));
2267
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
2268
+ }
2269
+ function distance3(a, b) {
2270
+ const m = a.length;
2271
+ const n = b.length;
2272
+ if (m === 0) return n;
2273
+ if (n === 0) return m;
2274
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
2275
+ for (let i = 1; i <= m; i++) {
2276
+ const curr = [i, ...new Array(n).fill(0)];
2277
+ for (let j = 1; j <= n; j++) {
2278
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2279
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
2280
+ }
2281
+ prev = curr;
2282
+ }
2283
+ return prev[n];
2284
+ }
2285
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
2286
+ const findings = [];
2287
+ if (declared === void 0 || declared === null) return findings;
2288
+ if (!objectName) return findings;
2289
+ if (!fieldsByObject.has(objectName)) return findings;
2290
+ const target = fieldsByObject.get(objectName);
2291
+ if (!target) return findings;
2292
+ const known = target.names;
2293
+ for (const key of readSortKeys(declared)) {
2294
+ const name = key.field;
2295
+ const head = name.split(".")[0];
2296
+ if (SYSTEM_FIELDS.has(head)) continue;
2297
+ if (!known.has(head)) {
2298
+ const dotted = name.includes(".");
2299
+ findings.push({
2300
+ severity: "error",
2301
+ rule: SORT_FIELD_UNKNOWN,
2302
+ where,
2303
+ path: `${path}${key.at}`,
2304
+ message: `${subject} orders by "${name}", which is not a field on object "${objectName}". The runtime refuses the sort rather than dropping it: every load of this view answers 400 INVALID_SORT (#6994), because a sort is the view's FIRST fetch and not an optional interaction.` + (dotted ? "" : suggest4(head, known)),
2305
+ hint: (dotted ? `'sort' reaches only whole columns of "${objectName}" itself, never a related record's column \u2014 denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + (known.size > 0 ? `Object fields: ${[...known].sort().join(", ")}.` : "")
2306
+ });
2307
+ continue;
2308
+ }
2309
+ const meta = target.fields[name];
2310
+ if (isVirtualSearchField2(meta)) {
2311
+ const vtype = meta?.type;
2312
+ findings.push({
2313
+ severity: "error",
2314
+ rule: SORT_FIELD_UNSORTABLE,
2315
+ where,
2316
+ path: `${path}${key.at}`,
2317
+ message: `${subject} orders by "${name}" on object "${objectName}", a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materialises a column to ORDER BY. Measured, an unrefused sort on one returns 'asc' and 'desc' in byte-identical order \u2014 the rows carry the values they were asked to be ordered by, unordered, under a success.`,
2318
+ hint: `Denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that, or drop "${name}" from this sort. At runtime both doors now refuse it with 400 INVALID_SORT \u2014 the REST ingress (#6994) and the engine itself (#7095) \u2014 so the declaration breaks the view's first fetch, and every fetch after it.`
2319
+ });
2320
+ }
2321
+ }
2322
+ return findings;
2323
+ }
2324
+ function validateSortableFields(stack) {
2325
+ const findings = [];
2326
+ if (!isRec4(stack)) return findings;
2327
+ const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
2328
+ const fieldsByObject = indexObjectSearchTargets(stack);
2329
+ const check = (declared, objectName, where, path, subject) => {
2330
+ findings.push(
2331
+ ...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
2332
+ );
2333
+ };
2334
+ for (let oi = 0; oi < objects.length; oi++) {
2335
+ const obj = objects[oi];
2336
+ if (!isRec4(obj)) continue;
2337
+ const objName = strName4(obj.name);
2338
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
2339
+ if (isRec4(obj.listViews)) {
2340
+ for (const [key, lv] of Object.entries(obj.listViews)) {
2341
+ if (!isRec4(lv)) continue;
2342
+ check(
2343
+ lv.sort,
2344
+ // A built-in list view belongs to its object; an inline `data.object`
2345
+ // may still retarget it (ADR-0047 allows the explicit binding).
2346
+ listViewObject2(lv) ?? objName,
2347
+ `${label2} \u203A listViews.${key}`,
2348
+ `objects[${oi}].listViews.${key}.sort`,
2349
+ "list-view sort"
2350
+ );
2351
+ }
2352
+ }
2353
+ }
2354
+ const views = Array.isArray(stack.views) ? stack.views : [];
2355
+ for (let vi = 0; vi < views.length; vi++) {
2356
+ const view = views[vi];
2357
+ if (!isRec4(view)) continue;
2358
+ const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
2359
+ const viewObject = strName4(view.objectName) ?? strName4(view.object);
2360
+ if (isRec4(view.list)) {
2361
+ check(
2362
+ view.list.sort,
2363
+ listViewObject2(view.list) ?? viewObject,
2364
+ `view "${viewLabel2}" \u203A list`,
2365
+ `views[${vi}].list.sort`,
2366
+ "list-view sort"
2367
+ );
2368
+ }
2369
+ if (isRec4(view.listViews)) {
2370
+ for (const [key, lv] of Object.entries(view.listViews)) {
2371
+ if (!isRec4(lv)) continue;
2372
+ check(
2373
+ lv.sort,
2374
+ listViewObject2(lv) ?? viewObject,
2375
+ `view "${viewLabel2}" \u203A listViews.${key}`,
2376
+ `views[${vi}].listViews.${key}.sort`,
2377
+ "list-view sort"
2378
+ );
2379
+ }
2380
+ }
2381
+ }
2382
+ return findings;
2383
+ }
2384
+ function listViewObject2(listView) {
2385
+ const data = listView.data;
2386
+ return isRec4(data) ? strName4(data.object) : void 0;
2387
+ }
2388
+
2389
+ // src/page-walk.ts
2390
+ function isRec5(v) {
2391
+ return !!v && typeof v === "object" && !Array.isArray(v);
2392
+ }
2393
+ function strName5(v) {
2394
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2395
+ }
2065
2396
  var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
2066
2397
  function isSourceAuthoredPage(page) {
2067
- const kind = strName4(page.kind);
2398
+ const kind = strName5(page.kind);
2068
2399
  return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
2069
2400
  }
2070
2401
  function walkPageComponents(page, pagePath) {
2071
2402
  const out = [];
2072
- if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
2073
- const pageObject = strName4(page.object);
2403
+ if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
2404
+ const pageObject = strName5(page.object);
2074
2405
  const visit = (node, path, inheritedObject) => {
2075
- if (!isRec4(node)) return;
2076
- const props = isRec4(node.properties) ? node.properties : void 0;
2077
- const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
2078
- const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
2406
+ if (!isRec5(node)) return;
2407
+ const props = isRec5(node.properties) ? node.properties : void 0;
2408
+ const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
2409
+ const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
2079
2410
  out.push({ component: node, path, objectName });
2080
2411
  if (!props) return;
2081
2412
  if (Array.isArray(props.items)) {
2082
2413
  for (let i = 0; i < props.items.length; i++) {
2083
2414
  const item = props.items[i];
2084
- if (!isRec4(item) || !Array.isArray(item.children)) continue;
2415
+ if (!isRec5(item) || !Array.isArray(item.children)) continue;
2085
2416
  for (let c = 0; c < item.children.length; c++) {
2086
2417
  visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
2087
2418
  }
@@ -2103,12 +2434,12 @@ function walkPageComponents(page, pagePath) {
2103
2434
  const regions = Array.isArray(page.regions) ? page.regions : [];
2104
2435
  for (let r = 0; r < regions.length; r++) {
2105
2436
  const region = regions[r];
2106
- if (!isRec4(region) || !Array.isArray(region.components)) continue;
2437
+ if (!isRec5(region) || !Array.isArray(region.components)) continue;
2107
2438
  for (let c = 0; c < region.components.length; c++) {
2108
2439
  visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
2109
2440
  }
2110
2441
  }
2111
- const slots = isRec4(page.slots) ? page.slots : void 0;
2442
+ const slots = isRec5(page.slots) ? page.slots : void 0;
2112
2443
  if (slots) {
2113
2444
  for (const [slot, value] of Object.entries(slots)) {
2114
2445
  const list3 = Array.isArray(value) ? value : [value];
@@ -2130,13 +2461,13 @@ function asArray8(v) {
2130
2461
  }
2131
2462
  return [];
2132
2463
  }
2133
- function strName5(v) {
2464
+ function strName6(v) {
2134
2465
  return typeof v === "string" && v.length > 0 ? v : void 0;
2135
2466
  }
2136
2467
  function strList(v) {
2137
2468
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2138
2469
  }
2139
- function distance3(a, b) {
2470
+ function distance4(a, b) {
2140
2471
  const m = a.length;
2141
2472
  const n = b.length;
2142
2473
  if (m === 0) return n;
@@ -2152,11 +2483,11 @@ function distance3(a, b) {
2152
2483
  }
2153
2484
  return prev[n];
2154
2485
  }
2155
- function suggest4(target, known) {
2486
+ function suggest5(target, known) {
2156
2487
  let best;
2157
2488
  let bestScore = Infinity;
2158
2489
  for (const candidate of known) {
2159
- const d = distance3(target, candidate);
2490
+ const d = distance4(target, candidate);
2160
2491
  if (d < bestScore) {
2161
2492
  bestScore = d;
2162
2493
  best = candidate;
@@ -2168,13 +2499,13 @@ function suggest4(target, known) {
2168
2499
  function collectActionNames(stack) {
2169
2500
  const names = /* @__PURE__ */ new Set();
2170
2501
  for (const action of asArray8(stack.actions)) {
2171
- const n = strName5(action?.name);
2502
+ const n = strName6(action?.name);
2172
2503
  if (n) names.add(n);
2173
2504
  }
2174
2505
  for (const obj of asArray8(stack.objects)) {
2175
2506
  if (!obj || typeof obj !== "object") continue;
2176
2507
  for (const action of asArray8(obj.actions)) {
2177
- const n = strName5(action?.name);
2508
+ const n = strName6(action?.name);
2178
2509
  if (n) names.add(n);
2179
2510
  }
2180
2511
  }
@@ -2191,7 +2522,7 @@ function validateActionNameRefs(stack) {
2191
2522
  rule: ACTION_NAME_UNDEFINED,
2192
2523
  where,
2193
2524
  path,
2194
- 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),
2525
+ 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.` + suggest5(name, known),
2195
2526
  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(", ")}.` : "")
2196
2527
  });
2197
2528
  };
@@ -2217,7 +2548,7 @@ function validateActionNameRefs(stack) {
2217
2548
  if (!def || typeof def !== "object") continue;
2218
2549
  if (def.execution !== "aggregate") continue;
2219
2550
  if (def.actionDef !== void 0) continue;
2220
- const name = strName5(def.name);
2551
+ const name = strName6(def.name);
2221
2552
  if (!name) continue;
2222
2553
  check(
2223
2554
  name,
@@ -2232,7 +2563,7 @@ function validateActionNameRefs(stack) {
2232
2563
  for (let vi = 0; vi < views.length; vi++) {
2233
2564
  const view = views[vi];
2234
2565
  if (!view || typeof view !== "object") continue;
2235
- const viewName = strName5(view.name) ?? strName5(view.object) ?? `#${vi}`;
2566
+ const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
2236
2567
  const owner = `view "${viewName}"`;
2237
2568
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
2238
2569
  const listViews = view.listViews;
@@ -2248,7 +2579,7 @@ function validateActionNameRefs(stack) {
2248
2579
  if (!obj || typeof obj !== "object") continue;
2249
2580
  const objListViews = obj.listViews;
2250
2581
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
2251
- const owner = `object "${strName5(obj.name) ?? `#${oi}`}"`;
2582
+ const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
2252
2583
  for (const [key, lv] of Object.entries(objListViews)) {
2253
2584
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
2254
2585
  }
@@ -2257,7 +2588,7 @@ function validateActionNameRefs(stack) {
2257
2588
  for (let pi = 0; pi < pages.length; pi++) {
2258
2589
  const page = pages[pi];
2259
2590
  if (!page || typeof page !== "object") continue;
2260
- const pageName = strName5(page.name) ?? `#${pi}`;
2591
+ const pageName = strName6(page.name) ?? `#${pi}`;
2261
2592
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2262
2593
  const props = component.properties;
2263
2594
  if (!props || typeof props !== "object") continue;
@@ -2265,7 +2596,7 @@ function validateActionNameRefs(stack) {
2265
2596
  for (let ai = 0; ai < names.length; ai++) {
2266
2597
  check(
2267
2598
  names[ai],
2268
- `page "${pageName}" \xB7 component "${strName5(component.type) ?? "?"}"`,
2599
+ `page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
2269
2600
  `${path}.properties.actionNames[${ai}]`,
2270
2601
  "Quick-actions bar"
2271
2602
  );
@@ -2276,7 +2607,7 @@ function validateActionNameRefs(stack) {
2276
2607
  for (let ai = 0; ai < apps.length; ai++) {
2277
2608
  const app = apps[ai];
2278
2609
  if (!app || typeof app !== "object") continue;
2279
- const appName = strName5(app.name) ?? `#${ai}`;
2610
+ const appName = strName6(app.name) ?? `#${ai}`;
2280
2611
  const walkNav = (items, basePath) => {
2281
2612
  const navItems = asArray8(items);
2282
2613
  for (let ni = 0; ni < navItems.length; ni++) {
@@ -2284,20 +2615,20 @@ function validateActionNameRefs(stack) {
2284
2615
  if (!nav || typeof nav !== "object") continue;
2285
2616
  const navPath = `${basePath}[${ni}]`;
2286
2617
  const actionDef = nav.actionDef;
2287
- const actionName = strName5(actionDef?.actionName);
2618
+ const actionName = strName6(actionDef?.actionName);
2288
2619
  if (nav.type === "action" && actionName) {
2289
2620
  check(
2290
2621
  actionName,
2291
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2622
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2292
2623
  `${navPath}.actionDef.actionName`,
2293
2624
  "Navigation action item"
2294
2625
  );
2295
2626
  }
2296
- const runAction = strName5(nav.runAction);
2627
+ const runAction = strName6(nav.runAction);
2297
2628
  if (nav.type === "object" && runAction) {
2298
2629
  check(
2299
2630
  runAction,
2300
- `app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
2631
+ `app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
2301
2632
  `${navPath}.runAction`,
2302
2633
  "Navigation deep-link auto-run"
2303
2634
  );
@@ -2324,23 +2655,23 @@ function asArray9(v) {
2324
2655
  }
2325
2656
  return [];
2326
2657
  }
2327
- function strName6(v) {
2658
+ function strName7(v) {
2328
2659
  return typeof v === "string" && v.length > 0 ? v : void 0;
2329
2660
  }
2330
- function isRec5(v) {
2661
+ function isRec6(v) {
2331
2662
  return !!v && typeof v === "object" && !Array.isArray(v);
2332
2663
  }
2333
2664
  function fieldRefsFrom(value, basePath) {
2334
2665
  const out = [];
2335
2666
  const one = (v, path) => {
2336
- const bare = strName6(v);
2667
+ const bare = strName7(v);
2337
2668
  if (bare) {
2338
2669
  out.push({ name: bare, path });
2339
2670
  return;
2340
2671
  }
2341
- if (!isRec5(v)) return;
2342
- const named = strName6(v.field) ?? strName6(v.name);
2343
- if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
2672
+ if (!isRec6(v)) return;
2673
+ const named = strName7(v.field) ?? strName7(v.name);
2674
+ if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
2344
2675
  };
2345
2676
  if (Array.isArray(value)) {
2346
2677
  for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
@@ -2366,7 +2697,12 @@ var COMPONENT_FIELD_SPECS = {
2366
2697
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2367
2698
  "record:path": { props: ["statusField"] },
2368
2699
  "element:number": { props: ["field"] },
2369
- "element:filter": { props: ["fields"] },
2700
+ // `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
2701
+ // whole element at element grain (ADR-0049 — no renderer ever shipped for it).
2702
+ // Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
2703
+ // spec-conformant page carries `fields` on it, and the #5068 props gate
2704
+ // reports an authored one by name with the element-retirement prescription —
2705
+ // the same #5775/#6629 residue class as the record-picker entries below.
2370
2706
  "element:form": { props: ["fields"] },
2371
2707
  // `labelField` is the one field-bearing prop this element declares. Its former
2372
2708
  // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
@@ -2396,18 +2732,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
2396
2732
  const sections = Array.isArray(props[key]) ? props[key] : [];
2397
2733
  for (let si = 0; si < sections.length; si++) {
2398
2734
  const section = sections[si];
2399
- if (!isRec5(section)) continue;
2735
+ if (!isRec6(section)) continue;
2400
2736
  refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
2401
2737
  }
2402
2738
  }
2403
2739
  return refs;
2404
2740
  }
2405
2741
  function relatedListFieldRefs(props, basePath, sep = ".") {
2406
- const add = isRec5(props.add) ? props.add : void 0;
2407
- const picker = add && isRec5(add.picker) ? add.picker : void 0;
2742
+ const add = isRec6(props.add) ? props.add : void 0;
2743
+ const picker = add && isRec6(add.picker) ? add.picker : void 0;
2408
2744
  const at = (key) => `${basePath}${sep}${key}`;
2409
2745
  return {
2410
- relatedObject: strName6(props.objectName),
2746
+ relatedObject: strName7(props.objectName),
2411
2747
  related: [
2412
2748
  ...fieldRefsFrom(props.columns, at("columns")),
2413
2749
  ...sortFieldRefs(props.sort, at("sort")),
@@ -2416,7 +2752,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2416
2752
  ...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
2417
2753
  ],
2418
2754
  parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
2419
- pickerObject: picker ? strName6(picker.object) : void 0,
2755
+ pickerObject: picker ? strName7(picker.object) : void 0,
2420
2756
  picker: picker ? [
2421
2757
  ...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
2422
2758
  ...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
@@ -2425,13 +2761,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
2425
2761
  }
2426
2762
  function indexObjectFields(stack) {
2427
2763
  const objectFields = /* @__PURE__ */ new Map();
2428
- if (!isRec5(stack)) return objectFields;
2764
+ if (!isRec6(stack)) return objectFields;
2429
2765
  for (const obj of asArray9(stack.objects)) {
2430
- const name = strName6(obj.name);
2766
+ const name = strName7(obj.name);
2431
2767
  if (!name) continue;
2432
2768
  const names = /* @__PURE__ */ new Set();
2433
2769
  for (const f of asArray9(obj.fields)) {
2434
- const fn = strName6(f.name);
2770
+ const fn = strName7(f.name);
2435
2771
  if (fn) names.add(fn);
2436
2772
  }
2437
2773
  objectFields.set(name, names);
@@ -2479,15 +2815,15 @@ function validatePageFieldBindings(stack) {
2479
2815
  for (let pi = 0; pi < pages.length; pi++) {
2480
2816
  const page = pages[pi];
2481
2817
  if (!page || typeof page !== "object") continue;
2482
- const pageName = strName6(page.name) ?? `#${pi}`;
2818
+ const pageName = strName7(page.name) ?? `#${pi}`;
2483
2819
  const checkRefs = (refs, objectName, where) => {
2484
2820
  findings.push(
2485
2821
  ...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
2486
2822
  );
2487
2823
  };
2488
2824
  for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
2489
- const type = strName6(component.type);
2490
- const props = isRec5(component.properties) ? component.properties : void 0;
2825
+ const type = strName7(component.type);
2826
+ const props = isRec6(component.properties) ? component.properties : void 0;
2491
2827
  if (!type || !props) continue;
2492
2828
  const where = `page "${pageName}" \xB7 ${type}`;
2493
2829
  const base = `${path}.properties`;
@@ -2502,16 +2838,16 @@ function validatePageFieldBindings(stack) {
2502
2838
  if (!refs) continue;
2503
2839
  checkRefs(refs, objectName, where);
2504
2840
  }
2505
- const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
2841
+ const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
2506
2842
  if (cfg) {
2507
- const cfgObject = strName6(cfg.source) ?? strName6(page.object);
2843
+ const cfgObject = strName7(cfg.source) ?? strName7(page.object);
2508
2844
  const base = `pages[${pi}].interfaceConfig`;
2509
2845
  const refs = [
2510
2846
  ...fieldRefsFrom(cfg.columns, `${base}.columns`),
2511
2847
  ...sortFieldRefs(cfg.sort, `${base}.sort`),
2512
2848
  ...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
2513
2849
  ];
2514
- const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
2850
+ const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
2515
2851
  if (userFilters) {
2516
2852
  refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
2517
2853
  }
@@ -2533,16 +2869,16 @@ function asArray10(v) {
2533
2869
  }
2534
2870
  return [];
2535
2871
  }
2536
- function strName7(v) {
2872
+ function strName8(v) {
2537
2873
  return typeof v === "string" && v.length > 0 ? v : void 0;
2538
2874
  }
2539
2875
  function strList2(v) {
2540
2876
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
2541
2877
  }
2542
- function isRec6(v) {
2878
+ function isRec7(v) {
2543
2879
  return !!v && typeof v === "object" && !Array.isArray(v);
2544
2880
  }
2545
- function distance4(a, b) {
2881
+ function distance5(a, b) {
2546
2882
  const m = a.length;
2547
2883
  const n = b.length;
2548
2884
  if (m === 0) return n;
@@ -2558,11 +2894,11 @@ function distance4(a, b) {
2558
2894
  }
2559
2895
  return prev[n];
2560
2896
  }
2561
- function suggest5(target, known) {
2897
+ function suggest6(target, known) {
2562
2898
  let best;
2563
2899
  let bestScore = Infinity;
2564
2900
  for (const c of known) {
2565
- const d = distance4(target, c);
2901
+ const d = distance5(target, c);
2566
2902
  if (d < bestScore) {
2567
2903
  bestScore = d;
2568
2904
  best = c;
@@ -2578,16 +2914,16 @@ function list2(names) {
2578
2914
  function indexDatasets(stack) {
2579
2915
  const out = /* @__PURE__ */ new Map();
2580
2916
  for (const ds of asArray10(stack.datasets)) {
2581
- const name = strName7(ds.name);
2917
+ const name = strName8(ds.name);
2582
2918
  if (!name) continue;
2583
2919
  const dimensions = /* @__PURE__ */ new Set();
2584
2920
  for (const d of asArray10(ds.dimensions)) {
2585
- const n = strName7(d.name);
2921
+ const n = strName8(d.name);
2586
2922
  if (n) dimensions.add(n);
2587
2923
  }
2588
2924
  const measures = /* @__PURE__ */ new Set();
2589
2925
  for (const m of asArray10(ds.measures)) {
2590
- const n = strName7(m.name);
2926
+ const n = strName8(m.name);
2591
2927
  if (n) measures.add(n);
2592
2928
  }
2593
2929
  out.set(name, { dimensions, measures });
@@ -2610,7 +2946,7 @@ function validateChartBindings(stack) {
2610
2946
  where: binding.where,
2611
2947
  path: `${binding.path}.dataset`,
2612
2948
  message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
2613
- hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
2949
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
2614
2950
  });
2615
2951
  return;
2616
2952
  }
@@ -2622,7 +2958,7 @@ function validateChartBindings(stack) {
2622
2958
  where: binding.where,
2623
2959
  path,
2624
2960
  message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
2625
- hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
2961
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
2626
2962
  });
2627
2963
  };
2628
2964
  const measureRef = (name, path, selected2) => {
@@ -2633,7 +2969,7 @@ function validateChartBindings(stack) {
2633
2969
  where: binding.where,
2634
2970
  path,
2635
2971
  message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
2636
- hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
2972
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
2637
2973
  });
2638
2974
  return;
2639
2975
  }
@@ -2668,26 +3004,26 @@ function validateChartBindings(stack) {
2668
3004
  const reports = asArray10(stack.reports);
2669
3005
  for (let ri = 0; ri < reports.length; ri++) {
2670
3006
  const report = reports[ri];
2671
- if (!isRec6(report)) continue;
2672
- const reportName = strName7(report.name) ?? `#${ri}`;
3007
+ if (!isRec7(report)) continue;
3008
+ const reportName = strName8(report.name) ?? `#${ri}`;
2673
3009
  const checkReportChart = (chart, dataset, values, where, path) => {
2674
- if (!isRec6(chart)) return;
3010
+ if (!isRec7(chart)) return;
2675
3011
  check({
2676
3012
  dataset,
2677
3013
  // `values` is the report's measure SELECTION, not a chart ref; feeding
2678
3014
  // it in lets the yAxis "declared but not selected" check work without
2679
3015
  // reporting the selection itself twice.
2680
3016
  values: { names: values, path: `${path}.values` },
2681
- xAxis: strName7(chart.xAxis) ? { name: strName7(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
2682
- yAxis: strName7(chart.yAxis) ? { name: strName7(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
2683
- series: asArray10(chart.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
3017
+ xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
3018
+ yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
3019
+ series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
2684
3020
  where,
2685
3021
  path: `${path}.chart`
2686
3022
  });
2687
3023
  };
2688
3024
  checkReportChart(
2689
3025
  report.chart,
2690
- strName7(report.dataset),
3026
+ strName8(report.dataset),
2691
3027
  strList2(report.values),
2692
3028
  `report "${reportName}" \xB7 chart`,
2693
3029
  `reports[${ri}]`
@@ -2695,22 +3031,22 @@ function validateChartBindings(stack) {
2695
3031
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
2696
3032
  for (let bi = 0; bi < blocks.length; bi++) {
2697
3033
  const block = blocks[bi];
2698
- if (!isRec6(block)) continue;
3034
+ if (!isRec7(block)) continue;
2699
3035
  checkReportChart(
2700
3036
  block.chart,
2701
- strName7(block.dataset),
3037
+ strName8(block.dataset),
2702
3038
  strList2(block.values),
2703
- `report "${reportName}" \xB7 block "${strName7(block.name) ?? `#${bi}`}" chart`,
3039
+ `report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
2704
3040
  `reports[${ri}].blocks[${bi}]`
2705
3041
  );
2706
3042
  }
2707
3043
  }
2708
3044
  const checkListChart = (container, where, path) => {
2709
- if (!isRec6(container)) return;
3045
+ if (!isRec7(container)) return;
2710
3046
  const chart = container.chart;
2711
- if (!isRec6(chart)) return;
3047
+ if (!isRec7(chart)) return;
2712
3048
  check({
2713
- dataset: strName7(chart.dataset),
3049
+ dataset: strName8(chart.dataset),
2714
3050
  dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
2715
3051
  values: { names: strList2(chart.values), path: `${path}.chart.values` },
2716
3052
  where,
@@ -2720,10 +3056,10 @@ function validateChartBindings(stack) {
2720
3056
  const views = asArray10(stack.views);
2721
3057
  for (let vi = 0; vi < views.length; vi++) {
2722
3058
  const view = views[vi];
2723
- if (!isRec6(view)) continue;
2724
- const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
3059
+ if (!isRec7(view)) continue;
3060
+ const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
2725
3061
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
2726
- if (isRec6(view.listViews)) {
3062
+ if (isRec7(view.listViews)) {
2727
3063
  for (const [key, lv] of Object.entries(view.listViews)) {
2728
3064
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
2729
3065
  }
@@ -2732,8 +3068,8 @@ function validateChartBindings(stack) {
2732
3068
  const objects = asArray10(stack.objects);
2733
3069
  for (let oi = 0; oi < objects.length; oi++) {
2734
3070
  const obj = objects[oi];
2735
- if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
2736
- const objName = strName7(obj.name) ?? `#${oi}`;
3071
+ if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
3072
+ const objName = strName8(obj.name) ?? `#${oi}`;
2737
3073
  for (const [key, lv] of Object.entries(obj.listViews)) {
2738
3074
  checkListChart(
2739
3075
  lv,
@@ -2745,19 +3081,19 @@ function validateChartBindings(stack) {
2745
3081
  const pages = asArray10(stack.pages);
2746
3082
  for (let pi = 0; pi < pages.length; pi++) {
2747
3083
  const page = pages[pi];
2748
- if (!isRec6(page)) continue;
2749
- const pageName = strName7(page.name) ?? `#${pi}`;
3084
+ if (!isRec7(page)) continue;
3085
+ const pageName = strName8(page.name) ?? `#${pi}`;
2750
3086
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
2751
- const props = isRec6(component.properties) ? component.properties : void 0;
2752
- if (!props || !strName7(props.dataset)) continue;
2753
- const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
2754
- const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
3087
+ const props = isRec7(component.properties) ? component.properties : void 0;
3088
+ if (!props || !strName8(props.dataset)) continue;
3089
+ const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
3090
+ const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
2755
3091
  check({
2756
- dataset: strName7(props.dataset),
3092
+ dataset: strName8(props.dataset),
2757
3093
  dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
2758
3094
  values: { names: strList2(props.values), path: `${path}.properties.values` },
2759
3095
  series: [...axisRefs, ...seriesRefs],
2760
- where: `page "${pageName}" \xB7 ${strName7(component.type) ?? "chart"}`,
3096
+ where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
2761
3097
  path: `${path}.properties`
2762
3098
  });
2763
3099
  }
@@ -2824,7 +3160,7 @@ function asArray12(v) {
2824
3160
  }
2825
3161
  return [];
2826
3162
  }
2827
- function strName8(v) {
3163
+ function strName9(v) {
2828
3164
  return typeof v === "string" && v.length > 0 ? v : void 0;
2829
3165
  }
2830
3166
  function collectNavExposures(stack) {
@@ -2833,18 +3169,18 @@ function collectNavExposures(stack) {
2833
3169
  for (let ai = 0; ai < apps.length; ai++) {
2834
3170
  const app = apps[ai];
2835
3171
  if (!app || typeof app !== "object") continue;
2836
- const appName = strName8(app.name) ?? `#${ai}`;
3172
+ const appName = strName9(app.name) ?? `#${ai}`;
2837
3173
  const walk = (items, basePath) => {
2838
3174
  const navItems = asArray12(items);
2839
3175
  for (let ni = 0; ni < navItems.length; ni++) {
2840
3176
  const nav = navItems[ni];
2841
3177
  if (!nav || typeof nav !== "object") continue;
2842
3178
  const navPath = `${basePath}[${ni}]`;
2843
- const objectName = strName8(nav.objectName);
3179
+ const objectName = strName9(nav.objectName);
2844
3180
  if (nav.type === "object" && objectName) {
2845
3181
  out.push({
2846
3182
  objectName,
2847
- where: `app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
3183
+ where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
2848
3184
  path: `${navPath}.objectName`
2849
3185
  });
2850
3186
  }
@@ -2868,7 +3204,7 @@ function validateNavAccess(stack) {
2868
3204
  if (exposures.length === 0) return findings;
2869
3205
  const ownObjects = /* @__PURE__ */ new Set();
2870
3206
  for (const obj of asArray12(stack.objects)) {
2871
- const n = strName8(obj.name);
3207
+ const n = strName9(obj.name);
2872
3208
  if (n) ownObjects.add(n);
2873
3209
  }
2874
3210
  const readable = /* @__PURE__ */ new Set();
@@ -2898,13 +3234,13 @@ function validateNavAccess(stack) {
2898
3234
 
2899
3235
  // src/validate-nav-target-refs.ts
2900
3236
  var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
2901
- var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3237
+ var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2902
3238
  function asArray13(v) {
2903
- if (Array.isArray(v)) return v.filter(isRec7);
2904
- if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
3239
+ if (Array.isArray(v)) return v.filter(isRec8);
3240
+ if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
2905
3241
  return [];
2906
3242
  }
2907
- function strName9(v) {
3243
+ function strName10(v) {
2908
3244
  return typeof v === "string" && v.length > 0 ? v : void 0;
2909
3245
  }
2910
3246
  var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
@@ -2916,14 +3252,14 @@ var NAV_TARGETS = [
2916
3252
  function namesOf(collection) {
2917
3253
  const out = /* @__PURE__ */ new Set();
2918
3254
  for (const entry of asArray13(collection)) {
2919
- const n = strName9(entry.name);
3255
+ const n = strName10(entry.name);
2920
3256
  if (n) out.add(n);
2921
3257
  }
2922
3258
  return out;
2923
3259
  }
2924
3260
  function validateNavTargetRefs(stack) {
2925
3261
  const findings = [];
2926
- if (!isRec7(stack)) return findings;
3262
+ if (!isRec8(stack)) return findings;
2927
3263
  const apps = asArray13(stack.apps);
2928
3264
  if (apps.length === 0) return findings;
2929
3265
  const declared = /* @__PURE__ */ new Map();
@@ -2931,16 +3267,16 @@ function validateNavTargetRefs(stack) {
2931
3267
  declared.set(collection, namesOf(stack[collection]));
2932
3268
  }
2933
3269
  for (const [ai, app] of apps.entries()) {
2934
- const appName = strName9(app.name) ?? `#${ai}`;
3270
+ const appName = strName10(app.name) ?? `#${ai}`;
2935
3271
  const walk = (items, basePath) => {
2936
3272
  if (!Array.isArray(items)) return;
2937
3273
  for (const [ni, raw] of items.entries()) {
2938
- if (!isRec7(raw)) continue;
3274
+ if (!isRec8(raw)) continue;
2939
3275
  const nav = raw;
2940
3276
  const navPath = `${basePath}[${ni}]`;
2941
3277
  for (const [type, prop, collection, noun] of NAV_TARGETS) {
2942
3278
  if (nav.type !== type) continue;
2943
- const target = strName9(nav[prop]);
3279
+ const target = strName10(nav[prop]);
2944
3280
  if (!target || isInterpolated2(target)) continue;
2945
3281
  const known = declared.get(collection);
2946
3282
  if (known.has(target)) continue;
@@ -2948,7 +3284,7 @@ function validateNavTargetRefs(stack) {
2948
3284
  findings.push({
2949
3285
  severity: "warning",
2950
3286
  rule: NAV_TARGET_UNRESOLVED,
2951
- where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? strName9(nav.label) ?? `#${ni}`}"`,
3287
+ where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
2952
3288
  path: `${navPath}.${prop}`,
2953
3289
  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.`,
2954
3290
  hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
@@ -2969,42 +3305,42 @@ function validateNavTargetRefs(stack) {
2969
3305
  // src/validate-nav-object-servability.ts
2970
3306
  import { canServeApiOperation } from "@objectstack/spec/data";
2971
3307
  var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
2972
- var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3308
+ var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2973
3309
  function asArray14(v) {
2974
- if (Array.isArray(v)) return v.filter(isRec8);
2975
- if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
3310
+ if (Array.isArray(v)) return v.filter(isRec9);
3311
+ if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
2976
3312
  return [];
2977
3313
  }
2978
- function strName10(v) {
3314
+ function strName11(v) {
2979
3315
  return typeof v === "string" && v.length > 0 ? v : void 0;
2980
3316
  }
2981
3317
  var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
2982
3318
  function validateNavObjectServability(stack) {
2983
3319
  const findings = [];
2984
- if (!isRec8(stack)) return findings;
3320
+ if (!isRec9(stack)) return findings;
2985
3321
  const apps = asArray14(stack.apps);
2986
3322
  if (apps.length === 0) return findings;
2987
3323
  const ownEnable = /* @__PURE__ */ new Map();
2988
3324
  const objects = asArray14(stack.objects);
2989
3325
  for (const [oi, obj] of objects.entries()) {
2990
- const n = strName10(obj.name);
3326
+ const n = strName11(obj.name);
2991
3327
  if (!n) continue;
2992
3328
  ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
2993
3329
  }
2994
3330
  if (ownEnable.size === 0) return findings;
2995
3331
  for (const [ai, app] of apps.entries()) {
2996
- const appName = strName10(app.name) ?? `#${ai}`;
3332
+ const appName = strName11(app.name) ?? `#${ai}`;
2997
3333
  const walk = (items, basePath) => {
2998
3334
  if (!Array.isArray(items)) return;
2999
3335
  for (const [ni, raw] of items.entries()) {
3000
- if (!isRec8(raw)) continue;
3336
+ if (!isRec9(raw)) continue;
3001
3337
  const nav = raw;
3002
3338
  const navPath = `${basePath}[${ni}]`;
3003
3339
  if (nav.type === "object") {
3004
- const target = strName10(nav.objectName);
3340
+ const target = strName11(nav.objectName);
3005
3341
  const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
3006
3342
  if (target && declared && !canServeApiOperation(declared.enable, "list")) {
3007
- const enable = isRec8(declared.enable) ? declared.enable : {};
3343
+ const enable = isRec9(declared.enable) ? declared.enable : {};
3008
3344
  const apiDisabled = enable.apiEnabled === false;
3009
3345
  const condition = apiDisabled ? "`enable.apiEnabled: false`" : "`enable.apiMethods` does not grant `list`" + (Array.isArray(enable.apiMethods) ? ` (declared: ${enable.apiMethods.length === 0 ? "[] \u2014 deny-all" : enable.apiMethods.map((m) => `\`${String(m)}\``).join(", ")})` : "");
3010
3346
  const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
@@ -3012,7 +3348,7 @@ function validateNavObjectServability(stack) {
3012
3348
  findings.push({
3013
3349
  severity: "error",
3014
3350
  rule: NAV_OBJECT_UNSERVABLE,
3015
- where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
3351
+ where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
3016
3352
  // The nav entry is where the dead row is authored; the `enable`
3017
3353
  // key that condemns it is named in the message, because the fix
3018
3354
  // may belong at either end.
@@ -3039,27 +3375,27 @@ import { expandViewContainer } from "@objectstack/spec";
3039
3375
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
3040
3376
 
3041
3377
  // src/view-walk.ts
3042
- function isRec9(v) {
3378
+ function isRec10(v) {
3043
3379
  return !!v && typeof v === "object" && !Array.isArray(v);
3044
3380
  }
3045
- function strName11(v) {
3381
+ function strName12(v) {
3046
3382
  return typeof v === "string" && v.length > 0 ? v : void 0;
3047
3383
  }
3048
3384
  function viewObjectName(view) {
3049
- return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
3385
+ return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
3050
3386
  }
3051
3387
  function viewContainerSites(view, basePath) {
3052
- if (!isRec9(view)) return [];
3388
+ if (!isRec10(view)) return [];
3053
3389
  const sites = [{ view, path: basePath, surface: "", kind: "self" }];
3054
- if (isRec9(view.form)) {
3390
+ if (isRec10(view.form)) {
3055
3391
  sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
3056
3392
  }
3057
3393
  for (const key of ["listViews", "formViews"]) {
3058
3394
  const container = view[key];
3059
- if (!isRec9(container)) continue;
3395
+ if (!isRec10(container)) continue;
3060
3396
  const kind = key === "listViews" ? "listView" : "formView";
3061
3397
  for (const [subKey, sub] of Object.entries(container)) {
3062
- if (!isRec9(sub)) continue;
3398
+ if (!isRec10(sub)) continue;
3063
3399
  sites.push({
3064
3400
  view: sub,
3065
3401
  path: `${basePath}.${key}.${subKey}`,
@@ -3077,18 +3413,18 @@ function formViewSites(view, basePath) {
3077
3413
  // src/validate-translation-references.ts
3078
3414
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
3079
3415
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
3080
- function isRec10(v) {
3416
+ function isRec11(v) {
3081
3417
  return !!v && typeof v === "object" && !Array.isArray(v);
3082
3418
  }
3083
3419
  function asArray15(v) {
3084
3420
  if (Array.isArray(v)) return v;
3085
- if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
3421
+ if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
3086
3422
  return [];
3087
3423
  }
3088
- function strName12(v) {
3424
+ function strName13(v) {
3089
3425
  return typeof v === "string" && v.length > 0 ? v : void 0;
3090
3426
  }
3091
- function distance5(a, b) {
3427
+ function distance6(a, b) {
3092
3428
  const m = a.length;
3093
3429
  const n = b.length;
3094
3430
  if (m === 0) return n;
@@ -3104,7 +3440,7 @@ function distance5(a, b) {
3104
3440
  }
3105
3441
  return prev[n];
3106
3442
  }
3107
- function suggest6(target, known) {
3443
+ function suggest7(target, known) {
3108
3444
  const names = [...known];
3109
3445
  const segmentMatch = names.find(
3110
3446
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -3113,7 +3449,7 @@ function suggest6(target, known) {
3113
3449
  let best;
3114
3450
  let bestScore = Infinity;
3115
3451
  for (const candidate of names) {
3116
- const d = distance5(target, candidate);
3452
+ const d = distance6(target, candidate);
3117
3453
  if (d < bestScore) {
3118
3454
  bestScore = d;
3119
3455
  best = candidate;
@@ -3146,33 +3482,33 @@ function collectViewRecord(view, factsFor) {
3146
3482
  const addSections = (container, binding) => {
3147
3483
  if (!binding) return;
3148
3484
  for (const section of asArray15(container.sections)) {
3149
- const sectionName = strName12(section.name);
3485
+ const sectionName = strName13(section.name);
3150
3486
  if (sectionName) factsFor(binding).sections.add(sectionName);
3151
3487
  }
3152
3488
  };
3153
- const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
3154
- if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3155
- addView(recordObject ?? listBinding, strName12(view.name));
3489
+ const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
3490
+ if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
3491
+ addView(recordObject ?? listBinding, strName13(view.name));
3156
3492
  const named = namedViewKeys(view);
3157
3493
  for (const family of ["listViews", "formViews"]) {
3158
3494
  const container = view[family];
3159
- if (!isRec10(container)) continue;
3495
+ if (!isRec11(container)) continue;
3160
3496
  const registryKeys = family === "listViews" ? named.list : named.form;
3161
3497
  let at = 0;
3162
3498
  for (const sub of Object.values(container)) {
3163
3499
  if (!sub || typeof sub !== "object") continue;
3164
3500
  const registryKey = registryKeys[at++];
3165
- if (!isRec10(sub)) continue;
3501
+ if (!isRec11(sub)) continue;
3166
3502
  const binding = bindingOf(sub) ?? listBinding;
3167
3503
  addView(binding, registryKey);
3168
3504
  addSections(sub, binding);
3169
3505
  }
3170
3506
  }
3171
- if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3507
+ if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
3172
3508
  addSections(view, recordObject ?? listBinding);
3173
3509
  }
3174
3510
  function defaultListViewKey(object, container) {
3175
- if (!object || !isRec10(container.list)) return void 0;
3511
+ if (!object || !isRec11(container.list)) return void 0;
3176
3512
  const item = expandViewContainer(object, container).find(
3177
3513
  (i) => i.viewKind === "list" && i.isDefault
3178
3514
  );
@@ -3184,7 +3520,7 @@ function namedViewKeys(container) {
3184
3520
  const object = "probe";
3185
3521
  const prefix = `${object}.`;
3186
3522
  const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
3187
- const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3523
+ const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
3188
3524
  const listCount = countEntries(container.listViews);
3189
3525
  const formCount = countEntries(container.formViews);
3190
3526
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -3202,14 +3538,14 @@ function readOptions(field) {
3202
3538
  values.add(opt);
3203
3539
  continue;
3204
3540
  }
3205
- if (!isRec10(opt)) continue;
3206
- const value = strName12(opt.value);
3541
+ if (!isRec11(opt)) continue;
3542
+ const value = strName13(opt.value);
3207
3543
  if (!value) continue;
3208
3544
  values.add(value);
3209
- const label2 = strName12(opt.label);
3545
+ const label2 = strName13(opt.label);
3210
3546
  if (label2) byLabel.set(label2.toLowerCase(), value);
3211
3547
  }
3212
- } else if (isRec10(raw)) {
3548
+ } else if (isRec11(raw)) {
3213
3549
  for (const [value, label2] of Object.entries(raw)) {
3214
3550
  values.add(value);
3215
3551
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -3230,23 +3566,23 @@ function buildUniverse(stack) {
3230
3566
  return facts;
3231
3567
  };
3232
3568
  for (const obj of asArray15(stack.objects)) {
3233
- const objectName = strName12(obj.name);
3569
+ const objectName = strName13(obj.name);
3234
3570
  if (!objectName) continue;
3235
3571
  const facts = factsFor(objectName);
3236
3572
  for (const field of asArray15(obj.fields)) {
3237
- const fieldName = strName12(field.name);
3573
+ const fieldName = strName13(field.name);
3238
3574
  if (fieldName) facts.fields.set(fieldName, field);
3239
3575
  }
3240
3576
  for (const action of asArray15(obj.actions)) {
3241
- const actionName = strName12(action.name);
3577
+ const actionName = strName13(action.name);
3242
3578
  if (actionName) facts.actions.set(actionName, action);
3243
3579
  }
3244
3580
  for (const view of asArray15(obj.views)) {
3245
- collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
3581
+ collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
3246
3582
  }
3247
3583
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
3248
3584
  for (const group of asArray15(obj.fieldGroups)) {
3249
- const key = strName12(group.key) ?? strName12(group.name);
3585
+ const key = strName13(group.key) ?? strName13(group.name);
3250
3586
  if (key) facts.sections.add(key);
3251
3587
  }
3252
3588
  }
@@ -3257,10 +3593,10 @@ function buildUniverse(stack) {
3257
3593
  for (let pi = 0; pi < pages.length; pi++) {
3258
3594
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
3259
3595
  if (!walked.objectName) continue;
3260
- const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
3596
+ const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
3261
3597
  if (!props) continue;
3262
3598
  for (const section of asArray15(props.sections)) {
3263
- const sectionName = strName12(section.name);
3599
+ const sectionName = strName13(section.name);
3264
3600
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
3265
3601
  }
3266
3602
  }
@@ -3268,9 +3604,9 @@ function buildUniverse(stack) {
3268
3604
  const globalActions = /* @__PURE__ */ new Map();
3269
3605
  const actionOwners = /* @__PURE__ */ new Map();
3270
3606
  for (const action of asArray15(stack.actions)) {
3271
- const actionName = strName12(action.name);
3607
+ const actionName = strName13(action.name);
3272
3608
  if (!actionName) continue;
3273
- const owner = strName12(action.objectName) ?? strName12(action.object);
3609
+ const owner = strName13(action.objectName) ?? strName13(action.object);
3274
3610
  if (owner) {
3275
3611
  factsFor(owner).actions.set(actionName, action);
3276
3612
  actionOwners.set(actionName, owner);
@@ -3285,19 +3621,19 @@ function buildUniverse(stack) {
3285
3621
  }
3286
3622
  const apps = /* @__PURE__ */ new Map();
3287
3623
  for (const app of asArray15(stack.apps)) {
3288
- const appName = strName12(app.name);
3624
+ const appName = strName13(app.name);
3289
3625
  if (!appName) continue;
3290
3626
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
3291
3627
  const walkNav = (items) => {
3292
3628
  for (const item of asArray15(items)) {
3293
- const id = strName12(item.id);
3629
+ const id = strName13(item.id);
3294
3630
  if (id) navIds.add(id);
3295
3631
  if (item.children) walkNav(item.children);
3296
3632
  }
3297
3633
  };
3298
3634
  walkNav(app.navigation);
3299
3635
  for (const area of asArray15(app.areas)) {
3300
- const areaId = strName12(area.id);
3636
+ const areaId = strName13(area.id);
3301
3637
  if (areaId) navIds.add(areaId);
3302
3638
  walkNav(area.navigation);
3303
3639
  }
@@ -3305,20 +3641,20 @@ function buildUniverse(stack) {
3305
3641
  }
3306
3642
  const dashboards = /* @__PURE__ */ new Map();
3307
3643
  for (const dash of asArray15(stack.dashboards)) {
3308
- const dashName = strName12(dash.name);
3644
+ const dashName = strName13(dash.name);
3309
3645
  if (!dashName) continue;
3310
3646
  const widgets = /* @__PURE__ */ new Set();
3311
3647
  for (const widget of asArray15(dash.widgets)) {
3312
- const id = strName12(widget.id) ?? strName12(widget.name);
3648
+ const id = strName13(widget.id) ?? strName13(widget.name);
3313
3649
  if (id) widgets.add(id);
3314
3650
  }
3315
3651
  const actions = /* @__PURE__ */ new Set();
3316
3652
  const headerActions = [
3317
- ...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
3653
+ ...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
3318
3654
  ...asArray15(dash.actions)
3319
3655
  ];
3320
3656
  for (const action of headerActions) {
3321
- const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
3657
+ const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
3322
3658
  if (key) actions.add(key);
3323
3659
  }
3324
3660
  dashboards.set(dashName, { widgets, actions });
@@ -3330,7 +3666,7 @@ function localePath(bundleIndex, locale) {
3330
3666
  }
3331
3667
  function validateTranslationReferences(stack) {
3332
3668
  const findings = [];
3333
- if (!isRec10(stack)) return findings;
3669
+ if (!isRec11(stack)) return findings;
3334
3670
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3335
3671
  if (bundles.length === 0) return findings;
3336
3672
  const universe = buildUniverse(stack);
@@ -3339,13 +3675,13 @@ function validateTranslationReferences(stack) {
3339
3675
  };
3340
3676
  for (let bi = 0; bi < bundles.length; bi++) {
3341
3677
  const bundle = bundles[bi];
3342
- if (!isRec10(bundle)) continue;
3678
+ if (!isRec11(bundle)) continue;
3343
3679
  for (const [locale, rawData] of Object.entries(bundle)) {
3344
- if (!isRec10(rawData)) continue;
3680
+ if (!isRec11(rawData)) continue;
3345
3681
  const base = localePath(bi, locale);
3346
3682
  const inLocale = `locale "${locale}"`;
3347
3683
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
3348
- if (!isRec10(rawNode)) continue;
3684
+ if (!isRec11(rawNode)) continue;
3349
3685
  const objPath = `${base}.objects.${objectName}`;
3350
3686
  const facts = universe.objects.get(objectName);
3351
3687
  if (!facts) {
@@ -3353,7 +3689,7 @@ function validateTranslationReferences(stack) {
3353
3689
  orphan(
3354
3690
  `${inLocale} \xB7 object "${objectName}"`,
3355
3691
  objPath,
3356
- hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest6(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest6(objectName, universe.objects.keys()),
3692
+ hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest7(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest7(objectName, universe.objects.keys()),
3357
3693
  `Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
3358
3694
  );
3359
3695
  continue;
@@ -3366,12 +3702,12 @@ function validateTranslationReferences(stack) {
3366
3702
  orphan(
3367
3703
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
3368
3704
  fieldPath,
3369
- `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest6(fieldName, facts.fields.keys()),
3705
+ `Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest7(fieldName, facts.fields.keys()),
3370
3706
  `Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
3371
3707
  );
3372
3708
  continue;
3373
3709
  }
3374
- if (!isRec10(rawField)) continue;
3710
+ if (!isRec11(rawField)) continue;
3375
3711
  checkOptionKeys(findings, {
3376
3712
  optionMap: rawField.options,
3377
3713
  field,
@@ -3386,7 +3722,7 @@ function validateTranslationReferences(stack) {
3386
3722
  orphan(
3387
3723
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
3388
3724
  `${objPath}._views.${viewName}`,
3389
- `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest6(viewName, facts.views),
3725
+ `Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest7(viewName, facts.views),
3390
3726
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
3391
3727
  );
3392
3728
  }
@@ -3395,7 +3731,7 @@ function validateTranslationReferences(stack) {
3395
3731
  orphan(
3396
3732
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
3397
3733
  `${objPath}._sections.${sectionName}`,
3398
- `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest6(sectionName, facts.sections),
3734
+ `Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest7(sectionName, facts.sections),
3399
3735
  `Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
3400
3736
  );
3401
3737
  }
@@ -3406,7 +3742,7 @@ function validateTranslationReferences(stack) {
3406
3742
  orphan(
3407
3743
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
3408
3744
  actionPath,
3409
- `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest6(actionName, facts.actions.keys()),
3745
+ `Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest7(actionName, facts.actions.keys()),
3410
3746
  `Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
3411
3747
  );
3412
3748
  continue;
@@ -3428,7 +3764,7 @@ function validateTranslationReferences(stack) {
3428
3764
  orphan(
3429
3765
  `${inLocale} \xB7 global action "${actionName}"`,
3430
3766
  actionPath,
3431
- owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest6(actionName, universe.globalActions.keys()),
3767
+ owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest7(actionName, universe.globalActions.keys()),
3432
3768
  owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
3433
3769
  );
3434
3770
  continue;
@@ -3448,18 +3784,18 @@ function validateTranslationReferences(stack) {
3448
3784
  orphan(
3449
3785
  `${inLocale} \xB7 app "${appName}"`,
3450
3786
  appPath,
3451
- `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest6(appName, universe.apps.keys()),
3787
+ `Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest7(appName, universe.apps.keys()),
3452
3788
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
3453
3789
  );
3454
3790
  continue;
3455
3791
  }
3456
- if (!isRec10(rawApp)) continue;
3792
+ if (!isRec11(rawApp)) continue;
3457
3793
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
3458
3794
  if (navIds.has(navId)) continue;
3459
3795
  orphan(
3460
3796
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
3461
3797
  `${appPath}.navigation.${navId}`,
3462
- `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
3798
+ `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
3463
3799
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
3464
3800
  );
3465
3801
  }
@@ -3471,18 +3807,18 @@ function validateTranslationReferences(stack) {
3471
3807
  orphan(
3472
3808
  `${inLocale} \xB7 dashboard "${dashName}"`,
3473
3809
  dashPath,
3474
- `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest6(dashName, universe.dashboards.keys()),
3810
+ `Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest7(dashName, universe.dashboards.keys()),
3475
3811
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
3476
3812
  );
3477
3813
  continue;
3478
3814
  }
3479
- if (!isRec10(rawDash)) continue;
3815
+ if (!isRec11(rawDash)) continue;
3480
3816
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
3481
3817
  if (dash.widgets.has(widgetId)) continue;
3482
3818
  orphan(
3483
3819
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
3484
3820
  `${dashPath}.widgets.${widgetId}`,
3485
- `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
3821
+ `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
3486
3822
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
3487
3823
  );
3488
3824
  }
@@ -3491,7 +3827,7 @@ function validateTranslationReferences(stack) {
3491
3827
  orphan(
3492
3828
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
3493
3829
  `${dashPath}.actions.${actionKey}`,
3494
- `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
3830
+ `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
3495
3831
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
3496
3832
  );
3497
3833
  }
@@ -3501,7 +3837,7 @@ function validateTranslationReferences(stack) {
3501
3837
  return findings;
3502
3838
  }
3503
3839
  function asRecord(v) {
3504
- return isRec10(v) ? v : {};
3840
+ return isRec11(v) ? v : {};
3505
3841
  }
3506
3842
  function checkOptionKeys(findings, ctx) {
3507
3843
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -3513,7 +3849,7 @@ function checkOptionKeys(findings, ctx) {
3513
3849
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3514
3850
  where: ctx.where,
3515
3851
  path: ctx.path,
3516
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
3852
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName13(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
3517
3853
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
3518
3854
  });
3519
3855
  return;
@@ -3526,17 +3862,17 @@ function checkOptionKeys(findings, ctx) {
3526
3862
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
3527
3863
  where: ctx.where,
3528
3864
  path: `${ctx.path}.${key}`,
3529
- message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest6(key, declared.values),
3865
+ message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest7(key, declared.values),
3530
3866
  hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
3531
3867
  });
3532
3868
  }
3533
3869
  }
3534
3870
  function checkActionParams(findings, ctx) {
3535
- const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
3871
+ const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
3536
3872
  if (rawParams.length === 0) return;
3537
3873
  const declared = /* @__PURE__ */ new Set();
3538
3874
  for (const param of asArray15(ctx.action.params)) {
3539
- const name = strName12(param.name) ?? strName12(param.field);
3875
+ const name = strName13(param.name) ?? strName13(param.field);
3540
3876
  if (name) declared.add(name);
3541
3877
  }
3542
3878
  for (const paramName of rawParams) {
@@ -3546,40 +3882,40 @@ function checkActionParams(findings, ctx) {
3546
3882
  rule: TRANSLATION_TARGET_UNKNOWN,
3547
3883
  where: `${ctx.where} \xB7 param "${paramName}"`,
3548
3884
  path: `${ctx.path}.params.${paramName}`,
3549
- message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest6(paramName, declared),
3885
+ message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest7(paramName, declared),
3550
3886
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
3551
3887
  });
3552
3888
  }
3553
3889
  }
3554
3890
 
3555
3891
  // src/collection-entries.ts
3556
- function isRec11(v) {
3892
+ function isRec12(v) {
3557
3893
  return !!v && typeof v === "object" && !Array.isArray(v);
3558
3894
  }
3559
3895
  function collectionEntries(v, base) {
3560
3896
  if (Array.isArray(v)) {
3561
3897
  const out = [];
3562
3898
  for (let i = 0; i < v.length; i++) {
3563
- if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3899
+ if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
3564
3900
  }
3565
3901
  return out;
3566
3902
  }
3567
- if (isRec11(v)) {
3568
- return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3903
+ if (isRec12(v)) {
3904
+ return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
3569
3905
  }
3570
3906
  return [];
3571
3907
  }
3572
3908
 
3573
3909
  // src/validate-translatable-sections.ts
3574
3910
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
3575
- function isRec12(v) {
3911
+ function isRec13(v) {
3576
3912
  return !!v && typeof v === "object" && !Array.isArray(v);
3577
3913
  }
3578
- function strName13(v) {
3914
+ function strName14(v) {
3579
3915
  return typeof v === "string" && v.length > 0 ? v : void 0;
3580
3916
  }
3581
3917
  function viewLabel(view) {
3582
- const name = strName13(view.name);
3918
+ const name = strName14(view.name);
3583
3919
  return name ? `view "${name}"` : "";
3584
3920
  }
3585
3921
  function joinWhere(...parts) {
@@ -3587,7 +3923,7 @@ function joinWhere(...parts) {
3587
3923
  }
3588
3924
  function collectViewSites(view, basePath, label2, sites) {
3589
3925
  const recordObject = viewObjectName(view);
3590
- const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3926
+ const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
3591
3927
  for (const site of viewContainerSites(view, basePath)) {
3592
3928
  sites.push({
3593
3929
  path: `${site.path}.sections`,
@@ -3601,11 +3937,11 @@ function translatedObjectNames(stack) {
3601
3937
  const out = /* @__PURE__ */ new Set();
3602
3938
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
3603
3939
  for (const bundle of bundles) {
3604
- if (!isRec12(bundle)) continue;
3940
+ if (!isRec13(bundle)) continue;
3605
3941
  for (const data of Object.values(bundle)) {
3606
- if (!isRec12(data) || !isRec12(data.objects)) continue;
3942
+ if (!isRec13(data) || !isRec13(data.objects)) continue;
3607
3943
  for (const [objectName, node] of Object.entries(data.objects)) {
3608
- if (isRec12(node)) out.add(objectName);
3944
+ if (isRec13(node)) out.add(objectName);
3609
3945
  }
3610
3946
  }
3611
3947
  }
@@ -3617,22 +3953,22 @@ function suggestedName(label2) {
3617
3953
  }
3618
3954
  function validateTranslatableSections(stack) {
3619
3955
  const findings = [];
3620
- if (!isRec12(stack)) return findings;
3956
+ if (!isRec13(stack)) return findings;
3621
3957
  const translated = translatedObjectNames(stack);
3622
3958
  if (translated.size === 0) return findings;
3623
3959
  const sites = [];
3624
3960
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
3625
- const objectName = strName13(obj.name);
3961
+ const objectName = strName14(obj.name);
3626
3962
  if (!objectName) continue;
3627
3963
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
3628
3964
  collectViewSites(
3629
- { ...view, object: strName13(view.object) ?? objectName },
3965
+ { ...view, object: strName14(view.object) ?? objectName },
3630
3966
  path,
3631
3967
  viewLabel(view),
3632
3968
  sites
3633
3969
  );
3634
3970
  }
3635
- if (isRec12(obj.listViews)) {
3971
+ if (isRec13(obj.listViews)) {
3636
3972
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
3637
3973
  }
3638
3974
  }
@@ -3640,13 +3976,13 @@ function validateTranslatableSections(stack) {
3640
3976
  collectViewSites(view, path, viewLabel(view), sites);
3641
3977
  }
3642
3978
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
3643
- const pageName = strName13(page.name);
3979
+ const pageName = strName14(page.name);
3644
3980
  const pageLabel = pageName ? `page "${pageName}"` : "";
3645
3981
  for (const walked of walkPageComponents(page, pagePath)) {
3646
3982
  if (!walked.objectName) continue;
3647
- const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
3983
+ const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
3648
3984
  if (!props) continue;
3649
- const type = strName13(walked.component.type) ?? "component";
3985
+ const type = strName14(walked.component.type) ?? "component";
3650
3986
  sites.push({
3651
3987
  path: `${walked.path}.properties.sections`,
3652
3988
  surface: joinWhere(pageLabel, type),
@@ -3661,9 +3997,9 @@ function validateTranslatableSections(stack) {
3661
3997
  if (!Array.isArray(site.sections)) continue;
3662
3998
  for (let i = 0; i < site.sections.length; i++) {
3663
3999
  const section = site.sections[i];
3664
- if (!isRec12(section)) continue;
3665
- if (strName13(section.name)) continue;
3666
- const heading = strName13(section.label);
4000
+ if (!isRec13(section)) continue;
4001
+ if (strName14(section.name)) continue;
4002
+ const heading = strName14(section.label);
3667
4003
  if (!heading) continue;
3668
4004
  const slug = suggestedName(heading);
3669
4005
  findings.push({
@@ -3681,10 +4017,10 @@ function validateTranslatableSections(stack) {
3681
4017
 
3682
4018
  // src/flow-walk.ts
3683
4019
  import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
3684
- function isRec13(v) {
4020
+ function isRec14(v) {
3685
4021
  return !!v && typeof v === "object" && !Array.isArray(v);
3686
4022
  }
3687
- function strName14(v) {
4023
+ function strName15(v) {
3688
4024
  return typeof v === "string" && v.length > 0 ? v : void 0;
3689
4025
  }
3690
4026
  var REGION_SLOTS = new Map(
@@ -3693,10 +4029,10 @@ var REGION_SLOTS = new Map(
3693
4029
  var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
3694
4030
  var MAX_REGION_DEPTH = 16;
3695
4031
  function flowNodeLabel(node, index) {
3696
- return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
4032
+ return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
3697
4033
  }
3698
4034
  function stripRegions(config) {
3699
- if (!isRec13(config)) return void 0;
4035
+ if (!isRec14(config)) return void 0;
3700
4036
  let out;
3701
4037
  for (const key of Object.keys(config)) {
3702
4038
  if (!REGION_CONFIG_KEYS.has(key)) continue;
@@ -3707,11 +4043,11 @@ function stripRegions(config) {
3707
4043
  }
3708
4044
  function walkFlowNodes(flow, flowPath) {
3709
4045
  const out = [];
3710
- if (!isRec13(flow)) return out;
4046
+ if (!isRec14(flow)) return out;
3711
4047
  const visitList = (nodes, basePath, trail, depth) => {
3712
4048
  if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
3713
4049
  nodes.forEach((raw, index) => {
3714
- if (!isRec13(raw)) return;
4050
+ if (!isRec14(raw)) return;
3715
4051
  const path = `${basePath}[${index}]`;
3716
4052
  out.push({
3717
4053
  node: raw,
@@ -3720,9 +4056,9 @@ function walkFlowNodes(flow, flowPath) {
3720
4056
  regionTrail: trail,
3721
4057
  depth
3722
4058
  });
3723
- const type = strName14(raw.type);
4059
+ const type = strName15(raw.type);
3724
4060
  const slots = type ? REGION_SLOTS.get(type) : void 0;
3725
- if (!slots || !isRec13(raw.config)) return;
4061
+ if (!slots || !isRec14(raw.config)) return;
3726
4062
  const config = raw.config;
3727
4063
  const here = `${type} "${flowNodeLabel(raw, index)}"`;
3728
4064
  for (const slot of slots) {
@@ -3730,8 +4066,8 @@ function walkFlowNodes(flow, flowPath) {
3730
4066
  if (slot === "branches") {
3731
4067
  if (!Array.isArray(value)) continue;
3732
4068
  value.forEach((branch, b) => {
3733
- if (!isRec13(branch)) return;
3734
- const branchName = strName14(branch.name) ?? `#${b}`;
4069
+ if (!isRec14(branch)) return;
4070
+ const branchName = strName15(branch.name) ?? `#${b}`;
3735
4071
  visitList(
3736
4072
  branch.nodes,
3737
4073
  `${path}.config.branches[${b}].nodes`,
@@ -3741,7 +4077,7 @@ function walkFlowNodes(flow, flowPath) {
3741
4077
  });
3742
4078
  continue;
3743
4079
  }
3744
- if (!isRec13(value)) continue;
4080
+ if (!isRec14(value)) continue;
3745
4081
  visitList(
3746
4082
  value.nodes,
3747
4083
  `${path}.config.${slot}.nodes`,
@@ -3982,7 +4318,7 @@ function asArray17(v) {
3982
4318
  }
3983
4319
  return [];
3984
4320
  }
3985
- function strName15(v) {
4321
+ function strName16(v) {
3986
4322
  return typeof v === "string" && v.length > 0 ? v : void 0;
3987
4323
  }
3988
4324
  function surfaceOf(v) {
@@ -3993,17 +4329,17 @@ function validateAiSurfaceAffinity(stack) {
3993
4329
  if (!stack || typeof stack !== "object") return findings;
3994
4330
  const skillsByName = /* @__PURE__ */ new Map();
3995
4331
  for (const skill of asArray17(stack.skills)) {
3996
- const n = strName15(skill.name);
4332
+ const n = strName16(skill.name);
3997
4333
  if (n) skillsByName.set(n, skill);
3998
4334
  }
3999
4335
  const agents = asArray17(stack.agents);
4000
4336
  for (let ai = 0; ai < agents.length; ai++) {
4001
4337
  const agent = agents[ai];
4002
- const agentName = strName15(agent.name) ?? `#${ai}`;
4338
+ const agentName = strName16(agent.name) ?? `#${ai}`;
4003
4339
  const agentSurface = surfaceOf(agent.surface);
4004
4340
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
4005
4341
  for (let si = 0; si < skillRefs.length; si++) {
4006
- const ref = strName15(skillRefs[si]);
4342
+ const ref = strName16(skillRefs[si]);
4007
4343
  if (!ref) continue;
4008
4344
  const skill = skillsByName.get(ref);
4009
4345
  if (!skill) continue;
@@ -4032,10 +4368,10 @@ function asArray18(v) {
4032
4368
  }
4033
4369
  return [];
4034
4370
  }
4035
- function strName16(v) {
4371
+ function strName17(v) {
4036
4372
  return typeof v === "string" && v.length > 0 ? v : void 0;
4037
4373
  }
4038
- function distance6(a, b) {
4374
+ function distance7(a, b) {
4039
4375
  const m = a.length;
4040
4376
  const n = b.length;
4041
4377
  if (m === 0) return n;
@@ -4051,14 +4387,14 @@ function distance6(a, b) {
4051
4387
  }
4052
4388
  return prev[n];
4053
4389
  }
4054
- function suggest7(target, known) {
4390
+ function suggest8(target, known) {
4055
4391
  for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
4056
4392
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
4057
4393
  }
4058
4394
  let best;
4059
4395
  let bestScore = Infinity;
4060
4396
  for (const candidate of known) {
4061
- const d = distance6(target, candidate);
4397
+ const d = distance7(target, candidate);
4062
4398
  if (d < bestScore) {
4063
4399
  bestScore = d;
4064
4400
  best = candidate;
@@ -4073,8 +4409,8 @@ function materialisesAsTool(action) {
4073
4409
  if (!ai || typeof ai !== "object") return false;
4074
4410
  const aiRec = ai;
4075
4411
  if (aiRec.exposed !== true) return false;
4076
- if (!strName16(aiRec.description)) return false;
4077
- const type = strName16(action.type);
4412
+ if (!strName17(aiRec.description)) return false;
4413
+ const type = strName17(action.type);
4078
4414
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
4079
4415
  if (type === "script") return Boolean(action.target || action.body);
4080
4416
  return Boolean(action.target);
@@ -4082,12 +4418,12 @@ function materialisesAsTool(action) {
4082
4418
  function collectToolUniverse(stack) {
4083
4419
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
4084
4420
  for (const tool of asArray18(stack.tools)) {
4085
- const n = strName16(tool.name);
4421
+ const n = strName17(tool.name);
4086
4422
  if (n) universe.add(n);
4087
4423
  }
4088
4424
  const addActionFamily = (actions) => {
4089
4425
  for (const action of asArray18(actions)) {
4090
- const n = strName16(action.name);
4426
+ const n = strName17(action.name);
4091
4427
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
4092
4428
  }
4093
4429
  };
@@ -4101,7 +4437,7 @@ function collectUnexposedActionNames(stack) {
4101
4437
  const names = /* @__PURE__ */ new Set();
4102
4438
  const scan = (actions) => {
4103
4439
  for (const action of asArray18(actions)) {
4104
- const n = strName16(action.name);
4440
+ const n = strName17(action.name);
4105
4441
  if (n && !materialisesAsTool(action)) names.add(n);
4106
4442
  }
4107
4443
  };
@@ -4127,10 +4463,10 @@ function validateAiToolReferences(stack) {
4127
4463
  const skills = asArray18(stack.skills);
4128
4464
  for (let si = 0; si < skills.length; si++) {
4129
4465
  const skill = skills[si];
4130
- const skillName = strName16(skill.name) ?? `#${si}`;
4466
+ const skillName = strName17(skill.name) ?? `#${si}`;
4131
4467
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
4132
4468
  for (let ti = 0; ti < refs.length; ti++) {
4133
- const ref = strName16(refs[ti]);
4469
+ const ref = strName17(refs[ti]);
4134
4470
  if (!ref || resolves(ref)) continue;
4135
4471
  const isPattern = ref.endsWith("*");
4136
4472
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -4139,7 +4475,7 @@ function validateAiToolReferences(stack) {
4139
4475
  rule: AI_SKILL_TOOL_UNRESOLVED,
4140
4476
  where: `skill "${skillName}" \xB7 tools`,
4141
4477
  path: `skills[${si}].tools[${ti}]`,
4142
- message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest7(ref, universe),
4478
+ message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest8(ref, universe),
4143
4479
  hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
4144
4480
  });
4145
4481
  }
@@ -4157,7 +4493,7 @@ function asArray19(v) {
4157
4493
  }
4158
4494
  return [];
4159
4495
  }
4160
- function strName17(v) {
4496
+ function strName18(v) {
4161
4497
  return typeof v === "string" && v.length > 0 ? v : void 0;
4162
4498
  }
4163
4499
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -4167,7 +4503,7 @@ function validateAiAgentAuthoring(stack) {
4167
4503
  const agents = asArray19(stack.agents);
4168
4504
  for (let ai = 0; ai < agents.length; ai++) {
4169
4505
  const agent = agents[ai];
4170
- const name = strName17(agent.name) ?? `#${ai}`;
4506
+ const name = strName18(agent.name) ?? `#${ai}`;
4171
4507
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
4172
4508
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
4173
4509
  findings.push({
@@ -4183,9 +4519,9 @@ function validateAiAgentAuthoring(stack) {
4183
4519
  const apps = asArray19(stack.apps);
4184
4520
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
4185
4521
  const app = apps[appIdx];
4186
- const defaultAgent = strName17(app.defaultAgent);
4522
+ const defaultAgent = strName18(app.defaultAgent);
4187
4523
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
4188
- const appName = strName17(app.name) ?? `#${appIdx}`;
4524
+ const appName = strName18(app.name) ?? `#${appIdx}`;
4189
4525
  findings.push({
4190
4526
  severity: "warning",
4191
4527
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -4215,6 +4551,7 @@ function loadTypeScript() {
4215
4551
  return cachedTs;
4216
4552
  }
4217
4553
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
4554
+ var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
4218
4555
  var HOOK_BODY_WRITE_PATTERNS = [
4219
4556
  {
4220
4557
  id: "input-property-assign",
@@ -4281,13 +4618,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
4281
4618
  "owner",
4282
4619
  "record_type"
4283
4620
  ]);
4284
- var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4621
+ function unprovisionedAnchorWriteConsequence() {
4622
+ return `so the value can never land: the anchor exists only in the registered schema, which is what carries it PAST the write-path validator that refuses an undeclared name outright (INVALID_FIELD). The remote database is what rejects it \u2014 on a SQL remote with an untyped driver error ('no such column') that aborts the whole statement, so the correctly named fields in the same payload never land either; on a schemaless remote the key is persisted into a column no read surface returns (#4271).`;
4623
+ }
4624
+ var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4285
4625
  function asArray20(v) {
4286
- if (Array.isArray(v)) return v.filter((x) => isRec14(x));
4287
- if (isRec14(v)) {
4626
+ if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4627
+ if (isRec15(v)) {
4288
4628
  return Object.entries(v).map(([name, def]) => ({
4289
4629
  name,
4290
- ...isRec14(def) ? def : {}
4630
+ ...isRec15(def) ? def : {}
4291
4631
  }));
4292
4632
  }
4293
4633
  return [];
@@ -4431,14 +4771,16 @@ function validateHookBodyWrites(stack) {
4431
4771
  const hooks = asArray20(stack.hooks);
4432
4772
  if (hooks.length === 0) return findings;
4433
4773
  let objectFields = null;
4774
+ let anchors = null;
4434
4775
  hooks.forEach((hook, hookIndex) => {
4435
4776
  const body = hook.body;
4436
- if (!isRec14(body) || body.language !== "js") return;
4777
+ if (!isRec15(body) || body.language !== "js") return;
4437
4778
  const source = body.source;
4438
4779
  if (typeof source !== "string" || source.trim() === "") return;
4439
4780
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
4440
4781
  if (writes.length === 0) return;
4441
4782
  objectFields ?? (objectFields = indexObjectFields2(stack));
4783
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4442
4784
  const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
4443
4785
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
4444
4786
  (o) => typeof o === "string" && o.trim() !== ""
@@ -4453,7 +4795,20 @@ function validateHookBodyWrites(stack) {
4453
4795
  if (reported.has(dedupeKey)) continue;
4454
4796
  if (w.object === void 0) {
4455
4797
  if (!inputJudgeable) continue;
4456
- if (IMPLICIT_FIELDS2.has(w.field)) continue;
4798
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4799
+ if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
4800
+ reported.add(dedupeKey);
4801
+ const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
4802
+ findings.push({
4803
+ severity: "warning",
4804
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4805
+ where,
4806
+ path,
4807
+ message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
4808
+ hint: unprovisionedAnchorHint(anchorObj, w.field)
4809
+ });
4810
+ continue;
4811
+ }
4457
4812
  if (targetSets.some((s) => s.has(w.field))) continue;
4458
4813
  reported.add(dedupeKey);
4459
4814
  const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
@@ -4469,7 +4824,20 @@ function validateHookBodyWrites(stack) {
4469
4824
  } else {
4470
4825
  const known = judgeableFieldsOf(objectFields, w.object);
4471
4826
  if (!known) continue;
4472
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
4827
+ if (known.has(w.field)) continue;
4828
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4829
+ if (!anchors.get(w.object)?.has(w.field)) continue;
4830
+ reported.add(dedupeKey);
4831
+ findings.push({
4832
+ severity: "warning",
4833
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
4834
+ where,
4835
+ path,
4836
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
4837
+ hint: unprovisionedAnchorHint(w.object, w.field)
4838
+ });
4839
+ continue;
4840
+ }
4473
4841
  reported.add(dedupeKey);
4474
4842
  findings.push({
4475
4843
  severity: "warning",
@@ -4498,19 +4866,20 @@ function fixHint(field, declared) {
4498
4866
  import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
4499
4867
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
4500
4868
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
4869
+ var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
4501
4870
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
4502
4871
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
4503
4872
  var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
4504
4873
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
4505
4874
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
4506
4875
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
4507
- var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4876
+ var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4508
4877
  function asArray21(v) {
4509
- if (Array.isArray(v)) return v.filter((x) => isRec15(x));
4510
- if (isRec15(v)) {
4878
+ if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4879
+ if (isRec16(v)) {
4511
4880
  return Object.entries(v).map(([name, def]) => ({
4512
4881
  name,
4513
- ...isRec15(def) ? def : {}
4882
+ ...isRec16(def) ? def : {}
4514
4883
  }));
4515
4884
  }
4516
4885
  return [];
@@ -4528,7 +4897,7 @@ function collectActionBodies(stack) {
4528
4897
  const type = typeof action.type === "string" ? action.type : "script";
4529
4898
  if (type !== "script") return;
4530
4899
  const body = action.body;
4531
- if (!isRec15(body) || body.language !== "js") return;
4900
+ if (!isRec16(body) || body.language !== "js") return;
4532
4901
  const source = body.source;
4533
4902
  if (typeof source !== "string" || source.trim() === "") return;
4534
4903
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -4547,10 +4916,11 @@ function collectActionBodies(stack) {
4547
4916
  }
4548
4917
  function validateActionBodyWrites(stack) {
4549
4918
  const findings = [];
4550
- if (!isRec15(stack)) return findings;
4919
+ if (!isRec16(stack)) return findings;
4551
4920
  const sites = collectActionBodies(stack);
4552
4921
  if (sites.length === 0) return findings;
4553
4922
  let objectFields = null;
4923
+ let anchors = null;
4554
4924
  for (const site of sites) {
4555
4925
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
4556
4926
  const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
@@ -4575,6 +4945,7 @@ function validateActionBodyWrites(stack) {
4575
4945
  }
4576
4946
  if (writes.length === 0) continue;
4577
4947
  objectFields ?? (objectFields = indexObjectFields2(stack));
4948
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4578
4949
  const reported = /* @__PURE__ */ new Set();
4579
4950
  for (const w of writes) {
4580
4951
  if (w.object === void 0) continue;
@@ -4582,7 +4953,20 @@ function validateActionBodyWrites(stack) {
4582
4953
  if (reported.has(dedupeKey)) continue;
4583
4954
  const known = judgeableFieldsOf(objectFields, w.object);
4584
4955
  if (!known) continue;
4585
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
4956
+ if (known.has(w.field)) continue;
4957
+ if (IMPLICIT_FIELDS2.has(w.field)) {
4958
+ if (!anchors.get(w.object)?.has(w.field)) continue;
4959
+ reported.add(dedupeKey);
4960
+ findings.push({
4961
+ severity: "warning",
4962
+ rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
4963
+ where,
4964
+ path: site.path,
4965
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
4966
+ hint: unprovisionedAnchorHint(w.object, w.field)
4967
+ });
4968
+ continue;
4969
+ }
4586
4970
  reported.add(dedupeKey);
4587
4971
  findings.push({
4588
4972
  severity: "warning",
@@ -4604,14 +4988,15 @@ function fixHint2(field, declared) {
4604
4988
  // src/validate-flow-node-writes.ts
4605
4989
  import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
4606
4990
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
4991
+ var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
4607
4992
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
4608
- var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4993
+ var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4609
4994
  function asArray22(v) {
4610
- if (Array.isArray(v)) return v.filter((x) => isRec16(x));
4611
- if (isRec16(v)) {
4995
+ if (Array.isArray(v)) return v.filter((x) => isRec17(x));
4996
+ if (isRec17(v)) {
4612
4997
  return Object.entries(v).map(([name, def]) => ({
4613
4998
  name,
4614
- ...isRec16(def) ? def : {}
4999
+ ...isRec17(def) ? def : {}
4615
5000
  }));
4616
5001
  }
4617
5002
  return [];
@@ -4624,30 +5009,44 @@ function readLiteralObjectName(config) {
4624
5009
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
4625
5010
  function validateFlowNodeWrites(stack) {
4626
5011
  const findings = [];
4627
- if (!isRec16(stack)) return findings;
5012
+ if (!isRec17(stack)) return findings;
4628
5013
  const flows = asArray22(stack.flows);
4629
5014
  if (flows.length === 0) return findings;
4630
5015
  let objectFields = null;
5016
+ let anchors = null;
4631
5017
  flows.forEach((flow, flowIndex) => {
4632
5018
  const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
4633
5019
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
4634
5020
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
4635
5021
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
4636
- const config = isRec16(node.config) ? node.config : void 0;
5022
+ const config = isRec17(node.config) ? node.config : void 0;
4637
5023
  if (!config) return;
4638
5024
  const fields = config.fields;
4639
- if (!isRec16(fields)) return;
5025
+ if (!isRec17(fields)) return;
4640
5026
  const written = Object.keys(fields);
4641
5027
  if (written.length === 0) return;
4642
5028
  const objectName = readLiteralObjectName(config);
4643
5029
  if (!objectName) return;
4644
5030
  objectFields ?? (objectFields = indexObjectFields2(stack));
5031
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
4645
5032
  const known = judgeableFieldsOf(objectFields, objectName);
4646
5033
  if (!known) return;
4647
5034
  const nodeName = flowNodeLabel(node, walkIndex);
4648
5035
  const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
4649
5036
  for (const fieldName of written) {
4650
- if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
5037
+ if (known.has(fieldName)) continue;
5038
+ if (IMPLICIT_FIELDS2.has(fieldName)) {
5039
+ if (!anchors.get(objectName)?.has(fieldName)) continue;
5040
+ findings.push({
5041
+ severity: "warning",
5042
+ rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
5043
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
5044
+ path: `${nodePath}.config.fields.${fieldName}`,
5045
+ message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
5046
+ hint: unprovisionedAnchorHint(objectName, fieldName)
5047
+ });
5048
+ continue;
5049
+ }
4651
5050
  if (fieldName.includes(".")) continue;
4652
5051
  findings.push({
4653
5052
  severity: "error",
@@ -4769,14 +5168,14 @@ import {
4769
5168
  chartAggregateResultKeys,
4770
5169
  isRecordContextBlockType
4771
5170
  } from "@objectstack/spec/ui";
4772
- import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
5171
+ import { VALID_AST_OPERATORS as VALID_AST_OPERATORS2 } from "@objectstack/spec/data";
4773
5172
 
4774
5173
  // src/zod-issue-format.ts
4775
- var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5174
+ var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4776
5175
  var valueAtPath = (root, path) => {
4777
5176
  let cur = root;
4778
5177
  for (const key of path) {
4779
- if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
5178
+ if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
4780
5179
  cur = cur[key];
4781
5180
  }
4782
5181
  return cur;
@@ -4915,7 +5314,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
4915
5314
  var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
4916
5315
  function checkChartDrillDown(raw, push2) {
4917
5316
  if (raw === void 0 || raw === NOT_STATIC) return;
4918
- if (!isRec18(raw)) {
5317
+ if (!isRec19(raw)) {
4919
5318
  push2(
4920
5319
  "error",
4921
5320
  REACT_CHART_DRILLDOWN_INVALID,
@@ -4938,7 +5337,7 @@ function checkChartDrillDown(raw, push2) {
4938
5337
  }
4939
5338
  function checkChartAggregate(raw, push2) {
4940
5339
  if (raw === void 0 || raw === NOT_STATIC) return;
4941
- if (!isRec18(raw)) {
5340
+ if (!isRec19(raw)) {
4942
5341
  push2(
4943
5342
  "error",
4944
5343
  REACT_CHART_AGGREGATE_INVALID,
@@ -4969,7 +5368,7 @@ function checkChartAggregate(raw, push2) {
4969
5368
  );
4970
5369
  }
4971
5370
  }
4972
- var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5371
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4973
5372
  var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4974
5373
  function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
4975
5374
  const { values, where, path } = attrs;
@@ -4979,11 +5378,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
4979
5378
  const aggregate = values.get("aggregate");
4980
5379
  checkChartAggregate(aggregate, push2);
4981
5380
  if (aggregate === void 0 || aggregate === NOT_STATIC) return;
4982
- if (!isRec18(aggregate)) return;
5381
+ if (!isRec19(aggregate)) return;
4983
5382
  const fn = strOf(aggregate.function);
4984
5383
  const field = strOf(aggregate.field);
4985
5384
  const groupBy = aggregate.groupBy;
4986
- const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
5385
+ const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
4987
5386
  const objectName = strOf(values.get("objectName"));
4988
5387
  const known = objectName ? objectFields.get(objectName) : void 0;
4989
5388
  if (objectName && known) {
@@ -5027,18 +5426,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
5027
5426
  );
5028
5427
  };
5029
5428
  const xAxisRaw = values.get("xAxis");
5030
- const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5429
+ const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
5031
5430
  const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
5032
5431
  axisRef(categoryAxis, categoryProp);
5033
5432
  const yAxisRaw = values.get("yAxis");
5034
5433
  const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
5035
5434
  for (const a of yAxisList) {
5036
- axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
5435
+ axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
5037
5436
  }
5038
5437
  const series = values.get("series");
5039
5438
  if (Array.isArray(series)) {
5040
5439
  for (const s of series) {
5041
- if (!isRec18(s)) continue;
5440
+ if (!isRec19(s)) continue;
5042
5441
  const dataKey = strOf(s.dataKey);
5043
5442
  axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
5044
5443
  }
@@ -5094,7 +5493,7 @@ function subformFieldRefs(value, basePath) {
5094
5493
  if (!Array.isArray(value)) return { child, parent };
5095
5494
  for (let i = 0; i < value.length; i++) {
5096
5495
  const sub = value[i];
5097
- if (!isRec18(sub)) continue;
5496
+ if (!isRec19(sub)) continue;
5098
5497
  const at = (key) => `${basePath}[${i}].${key}`;
5099
5498
  child.push({
5100
5499
  objectName: strOf(sub.childObject),
@@ -5119,7 +5518,7 @@ function filterFieldRefs(node, basePath, out) {
5119
5518
  for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
5120
5519
  return;
5121
5520
  }
5122
- if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
5521
+ if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS2.has(node[1].toLowerCase())) {
5123
5522
  out.push({ name: head, path: `${basePath}[0]` });
5124
5523
  }
5125
5524
  }
@@ -5139,20 +5538,20 @@ function reactFieldRefs(spec, values, basePath) {
5139
5538
  }
5140
5539
  for (const key of spec.nestedFields ?? []) {
5141
5540
  const v = readable(key);
5142
- if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5541
+ if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
5143
5542
  }
5144
5543
  for (const key of spec.sections ?? []) {
5145
5544
  const v = readable(key);
5146
5545
  if (!Array.isArray(v)) continue;
5147
5546
  for (let i = 0; i < v.length; i++) {
5148
5547
  const section = v[i];
5149
- if (!isRec18(section)) continue;
5548
+ if (!isRec19(section)) continue;
5150
5549
  own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
5151
5550
  }
5152
5551
  }
5153
5552
  for (const key of spec.keyedByField ?? []) {
5154
5553
  const v = readable(key);
5155
- if (!isRec18(v)) continue;
5554
+ if (!isRec19(v)) continue;
5156
5555
  for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
5157
5556
  }
5158
5557
  for (const key of spec.filterArrays ?? []) {
@@ -5319,7 +5718,12 @@ function validateReactPageProps(stack) {
5319
5718
  searchTargets,
5320
5719
  where,
5321
5720
  `${path} \u203A searchableFields`,
5322
- "searchableFields"
5721
+ "searchableFields",
5722
+ // A `<ListView>` prop is a view-level narrowing — the checker's
5723
+ // default, spelled out here because the #8404 provenance index
5724
+ // follows it positionally.
5725
+ "narrowing",
5726
+ unprovisionedAnchors
5323
5727
  )
5324
5728
  );
5325
5729
  }
@@ -5341,6 +5745,13 @@ function validateReactPageProps(stack) {
5341
5745
  var REFERENCE_INTEGRITY_RULES = [
5342
5746
  { name: "validateObjectReferences", run: validateObjectReferences },
5343
5747
  { name: "validateSearchableFields", run: validateSearchableFields },
5748
+ // [#9257] The same reading, one axis over: a list view's `sort` is a field
5749
+ // name written in metadata, resolved against the object's declared fields. It
5750
+ // gates (`error`) because the runtime does not tolerate a bad one at all —
5751
+ // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
5752
+ // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
5753
+ // the refusal is the whole view, on every load, traced to nothing.
5754
+ { name: "validateSortableFields", run: validateSortableFields },
5344
5755
  { name: "validateActionNameRefs", run: validateActionNameRefs },
5345
5756
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
5346
5757
  { name: "validateChartBindings", run: validateChartBindings },
@@ -5459,10 +5870,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
5459
5870
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
5460
5871
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
5461
5872
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
5462
- function isRec19(v) {
5873
+ function isRec20(v) {
5463
5874
  return !!v && typeof v === "object" && !Array.isArray(v);
5464
5875
  }
5465
- function strName18(v) {
5876
+ function strName19(v) {
5466
5877
  return typeof v === "string" && v.length > 0 ? v : void 0;
5467
5878
  }
5468
5879
  function asArray25(v) {
@@ -5476,8 +5887,8 @@ var PROPS_SCHEMAS = ComponentPropsMap;
5476
5887
  var DATASOURCE_SUPPLIED_PROP = "object";
5477
5888
  function suppliedByDataSource(issue, component) {
5478
5889
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
5479
- const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
5480
- return strName18(dataSource?.object) !== void 0;
5890
+ const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
5891
+ return strName19(dataSource?.object) !== void 0;
5481
5892
  }
5482
5893
  function unrecognizedKeysFromUnionArm(issue) {
5483
5894
  if (issue.code !== "invalid_union") return void 0;
@@ -5494,18 +5905,18 @@ function unrecognizedKeysFromUnionArm(issue) {
5494
5905
  }
5495
5906
  function validateComponentProps(stack) {
5496
5907
  const findings = [];
5497
- if (!isRec19(stack)) return findings;
5908
+ if (!isRec20(stack)) return findings;
5498
5909
  const pages = asArray25(stack.pages);
5499
5910
  for (let pi = 0; pi < pages.length; pi++) {
5500
5911
  const page = pages[pi];
5501
- if (!isRec19(page)) continue;
5502
- const pageName = strName18(page.name) ?? `#${pi}`;
5912
+ if (!isRec20(page)) continue;
5913
+ const pageName = strName19(page.name) ?? `#${pi}`;
5503
5914
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
5504
- const type = strName18(component.type);
5915
+ const type = strName19(component.type);
5505
5916
  if (!type) continue;
5506
5917
  const schema = PROPS_SCHEMAS[type];
5507
5918
  if (!schema) continue;
5508
- const props = isRec19(component.properties) ? component.properties : void 0;
5919
+ const props = isRec20(component.properties) ? component.properties : void 0;
5509
5920
  if (!props) continue;
5510
5921
  const where = `page "${pageName}" \xB7 ${type}`;
5511
5922
  const base = `${path}.properties`;
@@ -6683,10 +7094,10 @@ function asArray35(v) {
6683
7094
  }
6684
7095
  return [];
6685
7096
  }
6686
- function isRec20(v) {
7097
+ function isRec21(v) {
6687
7098
  return !!v && typeof v === "object" && !Array.isArray(v);
6688
7099
  }
6689
- function strName19(v) {
7100
+ function strName20(v) {
6690
7101
  return typeof v === "string" && v.length > 0 ? v : void 0;
6691
7102
  }
6692
7103
  function fieldNameOf(entry) {
@@ -6707,7 +7118,7 @@ function validateFormLayout(stack) {
6707
7118
  objectFields.set(name, new Set(fields));
6708
7119
  }
6709
7120
  for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
6710
- const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
7121
+ const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
6711
7122
  const containerObject = viewObjectName(view);
6712
7123
  for (const site of formViewSites(view, viewPath)) {
6713
7124
  const objName = viewObjectName(site.view) ?? containerObject;
@@ -6717,7 +7128,7 @@ function validateFormLayout(stack) {
6717
7128
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
6718
7129
  for (let s = 0; s < sections.length; s++) {
6719
7130
  const sec = sections[s];
6720
- const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
7131
+ const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
6721
7132
  for (let f = 0; f < secFields.length; f++) {
6722
7133
  const entry = secFields[f];
6723
7134
  const fname = fieldNameOf(entry);
@@ -6732,7 +7143,7 @@ function validateFormLayout(stack) {
6732
7143
  hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
6733
7144
  });
6734
7145
  }
6735
- const colSpan = isRec20(entry) ? entry.colSpan : void 0;
7146
+ const colSpan = isRec21(entry) ? entry.colSpan : void 0;
6736
7147
  if (colSpan != null) {
6737
7148
  findings.push({
6738
7149
  severity: "warning",
@@ -6895,12 +7306,12 @@ function bareRhsOnlyIdentifiers(ast) {
6895
7306
  for (const name of elsewhere) rhs.delete(name);
6896
7307
  return rhs;
6897
7308
  }
6898
- function isRec21(v) {
7309
+ function isRec22(v) {
6899
7310
  return !!v && typeof v === "object" && !Array.isArray(v);
6900
7311
  }
6901
7312
  function schemaIdOf(view) {
6902
7313
  const data = view.data;
6903
- if (!isRec21(data)) return void 0;
7314
+ if (!isRec22(data)) return void 0;
6904
7315
  if (data.provider !== "schema") return void 0;
6905
7316
  return typeof data.schemaId === "string" ? data.schemaId : void 0;
6906
7317
  }
@@ -7272,7 +7683,7 @@ function predicateSource2(v) {
7272
7683
  }
7273
7684
  return void 0;
7274
7685
  }
7275
- function isRec22(v) {
7686
+ function isRec23(v) {
7276
7687
  return !!v && typeof v === "object" && !Array.isArray(v);
7277
7688
  }
7278
7689
  function checkPredicate(source, scope, where, path, findings) {
@@ -7343,7 +7754,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
7343
7754
  if (!Array.isArray(entries) || depth > 12) return;
7344
7755
  for (let i = 0; i < entries.length; i++) {
7345
7756
  const entry = entries[i];
7346
- if (!isRec22(entry)) continue;
7757
+ if (!isRec23(entry)) continue;
7347
7758
  const path = `${base}[${i}]`;
7348
7759
  for (const key of PREDICATE_KEYS) {
7349
7760
  const source = predicateSource2(entry[key]);
@@ -7377,7 +7788,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
7377
7788
  const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
7378
7789
  for (let s = 0; s < sections.length; s++) {
7379
7790
  const section = sections[s];
7380
- if (!isRec22(section)) continue;
7791
+ if (!isRec23(section)) continue;
7381
7792
  const sectionPath = `${site.path}.${bucket}[${s}]`;
7382
7793
  for (const key of PREDICATE_KEYS) {
7383
7794
  const source = predicateSource2(section[key]);
@@ -7661,6 +8072,7 @@ function validateSecurityPosture(stack, opts) {
7661
8072
  }
7662
8073
  }
7663
8074
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
8075
+ const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
7664
8076
  const nowMs = opts?.nowMs ?? Date.now();
7665
8077
  for (const [i, seed] of asArray36(stack.data).entries()) {
7666
8078
  const seedObject = typeof seed.object === "string" ? seed.object : "";
@@ -7684,7 +8096,7 @@ function validateSecurityPosture(stack, opts) {
7684
8096
  }
7685
8097
  }
7686
8098
  const delegatedFrom = rec.delegated_from;
7687
- if (delegatedFrom != null && delegatedFrom !== "") {
8099
+ if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
7688
8100
  const reason = rec.reason;
7689
8101
  if (typeof reason !== "string" || reason.trim().length === 0) {
7690
8102
  findings.push({
@@ -7850,6 +8262,8 @@ function validateOrgAxisRedLines(stack) {
7850
8262
  import { compileCelToFilter } from "@objectstack/formula";
7851
8263
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
7852
8264
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
8265
+ var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
8266
+ var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
7853
8267
  function asArray38(v) {
7854
8268
  if (Array.isArray(v)) return v;
7855
8269
  if (v && typeof v === "object") {
@@ -7874,10 +8288,67 @@ function sourceOf(condition) {
7874
8288
  return str2(input?.source);
7875
8289
  }
7876
8290
  var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column `record.<field>` paths (ADR-0058 D2).";
8291
+ function effectiveSharingModelOf(obj) {
8292
+ const m = obj.sharingModel;
8293
+ if (m === "private") return "private";
8294
+ if (m === "public_read") return "read";
8295
+ if (m === "public_read_write" || m === "controlled_by_parent") return "public";
8296
+ if (m == null) {
8297
+ const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
8298
+ return isSystem ? "public" : "private";
8299
+ }
8300
+ return "private";
8301
+ }
8302
+ function masterOf(obj) {
8303
+ for (const f of asArray38(obj.fields)) {
8304
+ if (f.type === "master_detail") {
8305
+ const ref = f.reference;
8306
+ if (typeof ref === "string" && ref) return ref;
8307
+ }
8308
+ }
8309
+ return void 0;
8310
+ }
8311
+ function anchorFindings(rule, index, objectsByName) {
8312
+ const object = str2(rule.object);
8313
+ if (!object) return [];
8314
+ const target = objectsByName.get(object);
8315
+ if (!target) return [];
8316
+ const name = str2(rule.name) || String(index);
8317
+ const where = `sharing rule "${name}" on object "${object}"`;
8318
+ const path = `sharingRules[${index}].object`;
8319
+ const owd = target.sharingModel;
8320
+ if (owd === "controlled_by_parent") {
8321
+ const master = masterOf(target);
8322
+ return [{
8323
+ severity: "error",
8324
+ rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
8325
+ where,
8326
+ path,
8327
+ message: `Sharing rule "${name}" is anchored on object "${object}", which declares sharingModel 'controlled_by_parent'. A detail record has no record-level access of its own \u2014 its visibility is DERIVED from its master (ADR-0055), so it holds no shares to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is controlled by its parent (master-detail); share the master record instead"), so the rule's boot backfill fails, no \`sys_record_share\` row is ever written, and the recipients this rule names get whatever the MASTER grants them \u2014 which may be nothing. The grant is declared and does not exist.`,
8328
+ hint: `Move the rule onto the MASTER object` + (master ? ` \u2014 "${object}" derives from "${master}" through its master_detail field, so share "${master}" and the detail rows follow` : `, and share that instead; the detail rows follow`) + `. If "${object}" is meant to carry a record-level baseline of its own, that is a different decision: change its sharingModel to 'private' (owner + shares) or 'public_read', and this rule becomes enforceable where it stands.`
8329
+ }];
8330
+ }
8331
+ if (effectiveSharingModelOf(target) !== "public") return [];
8332
+ const declared = owd === "public_read_write" ? `declares sharingModel 'public_read_write'` : `declares no sharingModel and is a system object (\`isSystem: true\` or a \`sys_\` name), which ADR-0090 D1 resolves to public`;
8333
+ return [{
8334
+ severity: "error",
8335
+ rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
8336
+ where,
8337
+ path,
8338
+ message: `Sharing rule "${name}" is anchored on object "${object}", which ${declared}. Its effective sharing model is therefore \`public\`, and sharing only ever WIDENS an OWD baseline \u2014 on the widest baseline there is nothing left to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is not under record-sharing enforcement"), so the rule's boot backfill fails and no \`sys_record_share\` row is ever written. Measured: \`buildReadFilter\` returns \`null\` for this object, i.e. NO record-level filter at all \u2014 every principal already reads every row, so this rule advertises a restriction that does not exist.`,
8339
+ hint: `Decide which half is wrong. If the ACCESS is right \u2014 everyone should read and write these records \u2014 the rule is dead metadata: delete it (ADR-0049 enforce-or-remove). If the RULE is right \u2014 only the named audience should reach these records \u2014 then "${object}"'s OWD is the defect: set sharingModel: 'private' (owner + shares) or 'public_read', and this rule starts enforcing. Do NOT re-home the rule onto another public object; that moves the inertness instead of removing it.`
8340
+ }];
8341
+ }
7877
8342
  function validateSharingRuleEnforceability(stack) {
7878
8343
  const findings = [];
7879
8344
  const cfg = stack ?? {};
8345
+ const objectsByName = /* @__PURE__ */ new Map();
8346
+ for (const obj of asArray38(cfg.objects)) {
8347
+ const name = str2(obj.name);
8348
+ if (name) objectsByName.set(name, obj);
8349
+ }
7880
8350
  asArray38(cfg.sharingRules).forEach((rule, index) => {
8351
+ anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
7881
8352
  const input = toCompilerInput(rule.condition);
7882
8353
  if (input === null) return;
7883
8354
  const result = compileCelToFilter(input, { variables: {} });
@@ -8007,11 +8478,11 @@ import { createRequire as createRequire4 } from "module";
8007
8478
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
8008
8479
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
8009
8480
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
8010
- var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8481
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8011
8482
  function asArray40(v) {
8012
- if (Array.isArray(v)) return v.filter(isRec23);
8013
- if (isRec23(v)) {
8014
- return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
8483
+ if (Array.isArray(v)) return v.filter(isRec24);
8484
+ if (isRec24(v)) {
8485
+ return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
8015
8486
  }
8016
8487
  return [];
8017
8488
  }
@@ -8028,7 +8499,7 @@ function loadAjv() {
8028
8499
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
8029
8500
  );
8030
8501
  }
8031
- const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
8502
+ const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
8032
8503
  cachedAjv = ctor;
8033
8504
  return ctor;
8034
8505
  }
@@ -8043,7 +8514,7 @@ function loadAddFormats() {
8043
8514
  `@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
8044
8515
  );
8045
8516
  }
8046
- const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
8517
+ const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
8047
8518
  cachedAddFormats = plugin;
8048
8519
  return plugin;
8049
8520
  }
@@ -8073,13 +8544,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
8073
8544
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
8074
8545
  for (const branch of ["then", "otherwise"]) {
8075
8546
  const nested = rule[branch];
8076
- if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8547
+ if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
8077
8548
  }
8078
8549
  return out;
8079
8550
  }
8080
8551
  function walkObjectValidationRules(stack) {
8081
8552
  const walked = [];
8082
- if (!isRec23(stack)) return walked;
8553
+ if (!isRec24(stack)) return walked;
8083
8554
  for (const obj of asArray40(stack.objects)) {
8084
8555
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
8085
8556
  const validations = obj.validations;
@@ -8114,7 +8585,7 @@ function validateRuleCompilability(stack) {
8114
8585
  });
8115
8586
  }
8116
8587
  }
8117
- if (rule.type === "json_schema" && isRec23(rule.schema)) {
8588
+ if (rule.type === "json_schema" && isRec24(rule.schema)) {
8118
8589
  try {
8119
8590
  createRuntimeAjv().compile(rule.schema);
8120
8591
  } catch (err) {
@@ -8134,7 +8605,7 @@ function validateRuleCompilability(stack) {
8134
8605
 
8135
8606
  // src/validate-rule-schema-formats.ts
8136
8607
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
8137
- var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8608
+ var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8138
8609
  var SUBSCHEMA_KEYS = [
8139
8610
  "additionalItems",
8140
8611
  "additionalProperties",
@@ -8158,7 +8629,7 @@ var SUBSCHEMA_MAP_KEYS = [
8158
8629
  var MAX_SCHEMA_WALK_DEPTH = 32;
8159
8630
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
8160
8631
  function collectFormatUses(schema, pointer, out, depth) {
8161
- if (!isRec24(schema)) return;
8632
+ if (!isRec25(schema)) return;
8162
8633
  if (typeof schema.format === "string") {
8163
8634
  out.push({ pointer: `${pointer}/format`, name: schema.format });
8164
8635
  }
@@ -8177,7 +8648,7 @@ function collectFormatUses(schema, pointer, out, depth) {
8177
8648
  }
8178
8649
  for (const key of SUBSCHEMA_MAP_KEYS) {
8179
8650
  const value = schema[key];
8180
- if (!isRec24(value)) continue;
8651
+ if (!isRec25(value)) continue;
8181
8652
  for (const [name, entry] of Object.entries(value)) {
8182
8653
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
8183
8654
  }
@@ -8185,13 +8656,13 @@ function collectFormatUses(schema, pointer, out, depth) {
8185
8656
  const items = schema.items;
8186
8657
  if (Array.isArray(items)) {
8187
8658
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
8188
- } else if (isRec24(items)) {
8659
+ } else if (isRec25(items)) {
8189
8660
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
8190
8661
  }
8191
8662
  const dependencies = schema.dependencies;
8192
- if (isRec24(dependencies)) {
8663
+ if (isRec25(dependencies)) {
8193
8664
  for (const [name, entry] of Object.entries(dependencies)) {
8194
- if (!isRec24(entry)) continue;
8665
+ if (!isRec25(entry)) continue;
8195
8666
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
8196
8667
  }
8197
8668
  }
@@ -8218,9 +8689,9 @@ function nearestRegisteredFormat(name, registered) {
8218
8689
  let best = null;
8219
8690
  let bestDistance = Number.POSITIVE_INFINITY;
8220
8691
  for (const candidate of [...registered].sort()) {
8221
- const distance7 = editDistance2(authored, candidate);
8222
- if (distance7 < bestDistance) {
8223
- bestDistance = distance7;
8692
+ const distance8 = editDistance2(authored, candidate);
8693
+ if (distance8 < bestDistance) {
8694
+ bestDistance = distance8;
8224
8695
  best = candidate;
8225
8696
  }
8226
8697
  }
@@ -8230,7 +8701,7 @@ function validateRuleSchemaFormats(stack) {
8230
8701
  const findings = [];
8231
8702
  const pending = [];
8232
8703
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
8233
- if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
8704
+ if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
8234
8705
  const uses = [];
8235
8706
  collectFormatUses(rule.schema, "", uses, 0);
8236
8707
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -8263,7 +8734,7 @@ function asArray41(v) {
8263
8734
  }
8264
8735
  return [];
8265
8736
  }
8266
- function strName20(v) {
8737
+ function strName21(v) {
8267
8738
  return typeof v === "string" && v.length > 0 ? v : void 0;
8268
8739
  }
8269
8740
  function strList3(v) {
@@ -8278,7 +8749,7 @@ function collectNamePlacedActions(stack) {
8278
8749
  for (const n of strList3(list3[key])) placed.add(n);
8279
8750
  }
8280
8751
  for (const def of asArray41(list3.bulkActionDefs)) {
8281
- const n = strName20(def?.name);
8752
+ const n = strName21(def?.name);
8282
8753
  if (n) placed.add(n);
8283
8754
  }
8284
8755
  };
@@ -8304,7 +8775,7 @@ function validateActionLocations(stack) {
8304
8775
  const check = (action, path) => {
8305
8776
  if (!action || typeof action !== "object") return;
8306
8777
  if ("locations" in action) return;
8307
- const name = strName20(action.name);
8778
+ const name = strName21(action.name);
8308
8779
  if (!name) return;
8309
8780
  if (namePlaced.has(name)) return;
8310
8781
  findings.push({
@@ -9157,6 +9628,17 @@ function lintViewRefs(stack) {
9157
9628
  }
9158
9629
 
9159
9630
  // src/data-model-rules.ts
9631
+ function objectWhere(obj) {
9632
+ return `object "${obj?.name}"`;
9633
+ }
9634
+ function indexWhere(obj, idx, j, cols) {
9635
+ const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
9636
+ const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
9637
+ return `${objectWhere(obj)} \xB7 index ${label2}`;
9638
+ }
9639
+ function fieldWhere(obj, fieldName) {
9640
+ return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
9641
+ }
9160
9642
  function fieldEntries2(fields) {
9161
9643
  if (!fields) return [];
9162
9644
  if (Array.isArray(fields)) {
@@ -9190,11 +9672,13 @@ function lintUnscopedDeclaredIndexes(objects) {
9190
9672
  for (let j = 0; j < declaredIndexes.length; j++) {
9191
9673
  const idx = declaredIndexes[j];
9192
9674
  if (idx?.unique !== true) continue;
9193
- const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
9675
+ const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
9676
+ const cols = colList.join(", ");
9194
9677
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
9195
9678
  issues.push({
9196
9679
  severity: "warning",
9197
9680
  rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
9681
+ where: indexWhere(obj, idx, j, colList),
9198
9682
  message: `"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` \u2014 a unique index whose scope is unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; an author who meant "unique per organization" gets no per-organization constraint and no error. Protocol 18 rejects this spelling (#5082).`,
9199
9683
  path: `objects[${i}].indexes[${j}]`,
9200
9684
  fix: `State the scope: \`unique: 'global'\` (installation-wide \u2014 exactly today's behavior) or \`unique: 'organization'\` (one holder per organization \u2014 the driver prepends the NULL-safe organization key part at registration).`
@@ -9243,6 +9727,11 @@ function lintUniqueDeclarations(objects) {
9243
9727
  issues.push({
9244
9728
  severity: "warning",
9245
9729
  rule: UNIQUE_DOUBLE_DECLARATION,
9730
+ // More specific than `path` on purpose: this rule's finding is about ONE
9731
+ // column, but its `path` has always been the whole object (`objects[i]`)
9732
+ // because the defect straddles `fields.<name>.unique` and an entry of
9733
+ // `indexes`. `where` can name the column without picking one of the two.
9734
+ where: fieldWhere(obj, name),
9246
9735
  message,
9247
9736
  path: `objects[${i}]`,
9248
9737
  fix
@@ -9271,6 +9760,7 @@ function lintLegacyOrganizationComposites(objects) {
9271
9760
  issues.push({
9272
9761
  severity: "warning",
9273
9762
  rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
9763
+ where: indexWhere(obj, idx, j, cols),
9274
9764
  message: `"${obj.name}" declares index${indexLabel} [${cols.join(", ")}] with ${spelling} and lists the organization column '${tenantColumn}' itself \u2014 the hand-written per-organization composite that predates the scope vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) \u2014 which on a single-organization deployment is every row.`,
9275
9765
  path: `objects[${i}].indexes[${j}]`,
9276
9766
  fix: `State the scope instead: \`unique: 'organization'\` on this index (keep \`fields\` exactly as they are \u2014 the driver makes the listed '${tenantColumn}' NULL-safe in place rather than prepending a second organization key part). ${rest.length > 0 ? `The constraint then really is "one ${rest.join(" + ")} per organization". ` : ""}Opting in is a physical tightening: it surfaces as a \`recreate_index\` drift op gated by the duplicate pre-flight probe (ADR-0120 D4), so pre-existing duplicate NULL-organization rows block it with a report rather than failing a boot. Leaving it as-is stays valid indefinitely and forces no drift.`
@@ -9287,7 +9777,7 @@ var CLI_ONLY = ["cli"];
9287
9777
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
9288
9778
  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.";
9289
9779
  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.";
9290
- 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.";
9780
+ var RUNTIME_OBJECT_ADVISORY_VOLUME = "Advisory-tier object rule: it cannot refuse a write, and it is held off the runtime door for advisory VOLUME (~8 findings per object write measured on unswept metadata, rendered in Studio since #4717), not refusal risk. Crossing it is a UX decision with its own card (#4716).";
9291
9781
  var EXPRESSION_INVALID = "expression-invalid";
9292
9782
  var AUTHORING_RULES = [
9293
9783
  // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
@@ -9348,8 +9838,14 @@ var AUTHORING_RULES = [
9348
9838
  input: "normalized",
9349
9839
  commands: ALL,
9350
9840
  source: "packages/lint/src/validate-functional-completeness.ts",
9351
- surfaces: CLI_ONLY,
9352
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9841
+ // Runtime publish gate (#4716): the OBJECT write door — the five gating
9842
+ // object rules cross together under the 2026-08-18 adjudication. The
9843
+ // false-positive budget the crossing owed was exempted on a measured
9844
+ // 0 refusals / 75 real object declarations (authored config-file metadata,
9845
+ // so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
9846
+ // advisory-tier object rules deliberately do NOT ride.
9847
+ surfaces: CLI_AND_RUNTIME,
9848
+ runtimeTypes: ["object"],
9353
9849
  run: (stack) => validateFunctionalCompleteness(stack)
9354
9850
  },
9355
9851
  // [#7521, via cloud#1225] A managed object advertising a generic write verb
@@ -9373,8 +9869,11 @@ var AUTHORING_RULES = [
9373
9869
  input: "normalized",
9374
9870
  commands: ALL,
9375
9871
  source: "packages/lint/src/validate-managed-api-methods.ts",
9376
- surfaces: CLI_ONLY,
9377
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
9872
+ // Runtime publish gate (#4716): a managed object advertising a verb its
9873
+ // own affordances refuse is exactly the contradiction a Studio/MCP author
9874
+ // can save today — the CLI sweep (#7934) never sees an overlay row.
9875
+ surfaces: CLI_AND_RUNTIME,
9876
+ runtimeTypes: ["object"],
9378
9877
  run: (stack) => validateManagedApiMethods(stack)
9379
9878
  },
9380
9879
  // A view container in `views: []` that registers zero views: nothing appears
@@ -9396,19 +9895,38 @@ var AUTHORING_RULES = [
9396
9895
  },
9397
9896
  // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
9398
9897
  // chartConfig axis/series must resolve against the declared datasets.
9898
+ //
9899
+ // Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
9900
+ // widget bound to a dataset that resolves to nothing sailed through save AND
9901
+ // publish — this rule provably caught the body and was simply never invoked,
9902
+ // because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
9903
+ // hold a forward reference; publishing refuses it with the key path named.
9904
+ // The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
9905
+ // — without it every legitimate board reads as dangling, the 3-phantom
9906
+ // measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
9907
+ // rule's error ids on the publish gate, not just `widget-dataset-unknown` —
9908
+ // ruled 2026-08-15: they are one coherent "this board cannot render"
9909
+ // reference-integrity class, and the ~6× wider accept-set narrowing was
9910
+ // accepted knowingly rather than splitting the dataset limb into its own
9911
+ // rule (traversal-duplication drift) or adding a per-finding-id surface
9912
+ // filter (registry machinery that weakens "delete a rule from the table and
9913
+ // enforcement stops in the same commit"). Warning-tier ids ride along on the
9914
+ // advisory channel and never block (#4463 P1).
9399
9915
  {
9400
9916
  name: "validateWidgetBindings",
9401
9917
  tier: "gating",
9402
9918
  input: "parsed",
9403
9919
  commands: ALL,
9404
9920
  source: "packages/lint/src/validate-widget-bindings.ts",
9405
- surfaces: CLI_ONLY,
9406
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9921
+ surfaces: CLI_AND_RUNTIME,
9922
+ runtimeTypes: ["dashboard"],
9407
9923
  run: (stack) => validateWidgetBindings(stack)
9408
9924
  },
9409
- // ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
9410
- // target that resolves to no defined action ships a button that renders and
9411
- // silently does nothing on click. Unresolved `url` routes stay advisory.
9925
+ // ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
9926
+ // button that renders and refuses (or does nothing) on click: a `script`
9927
+ // target must name a defined action, a `modal` target must name a declared
9928
+ // page (objectstack#6739-A — a modal string target names a PAGE, only).
9929
+ // Unresolved `url` routes stay advisory.
9412
9930
  {
9413
9931
  name: "validateDashboardActionRefs",
9414
9932
  tier: "gating",
@@ -9433,6 +9951,28 @@ var AUTHORING_RULES = [
9433
9951
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
9434
9952
  run: (stack) => validateFilterTokens(stack)
9435
9953
  },
9954
+ // #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
9955
+ // name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
9956
+ // no layer: the engine refuses it on a declared temporal field at query time
9957
+ // (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
9958
+ // literal string. This is the authoring-time refusal the ruling shipped
9959
+ // alongside the engine door, judging the filter literal in isolation —
9960
+ // ordering positions only, all three authored filter shapes. Like
9961
+ // `validateEmptyCombinators` it needs NO resolution context, so
9962
+ // RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
9963
+ // for every filter-carrying type the gate already maps: the write path is
9964
+ // the one door an AI author uses, and dashboards/views are where the preset
9965
+ // vocabulary is near enough to reach for.
9966
+ {
9967
+ name: "validatePresetComparands",
9968
+ tier: "gating",
9969
+ input: "parsed",
9970
+ commands: ALL,
9971
+ source: "packages/lint/src/validate-preset-comparands.ts",
9972
+ surfaces: CLI_AND_RUNTIME,
9973
+ runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
9974
+ run: (stack) => validatePresetComparands(stack)
9975
+ },
9436
9976
  // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
9437
9977
  // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
9438
9978
  // this rule does not touch it: it refuses the literal SPELLINGS at authoring
@@ -9651,7 +10191,7 @@ var AUTHORING_RULES = [
9651
10191
  commands: ALL,
9652
10192
  source: "packages/lint/src/validate-record-title.ts",
9653
10193
  surfaces: CLI_ONLY,
9654
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10194
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9655
10195
  run: (stack) => validateRecordTitle(stack)
9656
10196
  },
9657
10197
  // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
@@ -9664,7 +10204,7 @@ var AUTHORING_RULES = [
9664
10204
  commands: ALL,
9665
10205
  source: "packages/lint/src/validate-semantic-roles.ts",
9666
10206
  surfaces: CLI_ONLY,
9667
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10207
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9668
10208
  run: (stack) => validateSemanticRoles(stack)
9669
10209
  },
9670
10210
  // #2578 / #4449 — a form section's field reference that resolves to nothing
@@ -9875,7 +10415,7 @@ var AUTHORING_RULES = [
9875
10415
  commands: ALL,
9876
10416
  source: "packages/lint/src/lint-liveness-properties.ts",
9877
10417
  surfaces: CLI_ONLY,
9878
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10418
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9879
10419
  run: (stack) => lintLivenessProperties(stack).map((f) => ({
9880
10420
  severity: "warning",
9881
10421
  rule: f.rule,
@@ -9894,8 +10434,12 @@ var AUTHORING_RULES = [
9894
10434
  input: "parsed",
9895
10435
  commands: ALL,
9896
10436
  source: "packages/lint/src/lint-autonumber-formats.ts",
9897
- surfaces: CLI_ONLY,
9898
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10437
+ // Runtime publish gate (#4716): an autonumber referencing a field the
10438
+ // object does not carry is broken from the first record; only the error
10439
+ // arm blocks — the optional-field arm is `warning` and rides the
10440
+ // advisory channel like every other non-error finding (#4463 P1).
10441
+ surfaces: CLI_AND_RUNTIME,
10442
+ runtimeTypes: ["object"],
9899
10443
  run: (stack) => lintAutonumberFormats(stack).map((f) => ({
9900
10444
  severity: f.severity,
9901
10445
  rule: f.rule,
@@ -9936,12 +10480,12 @@ var AUTHORING_RULES = [
9936
10480
  commands: ["validate", "build"],
9937
10481
  source: "packages/lint/src/data-model-rules.ts",
9938
10482
  surfaces: CLI_ONLY,
9939
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10483
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9940
10484
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly ahead of R10 in 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.",
9941
10485
  run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9942
10486
  severity: f.severity === "suggestion" ? "info" : f.severity,
9943
10487
  rule: f.rule,
9944
- where: f.path,
10488
+ where: f.where,
9945
10489
  path: f.path,
9946
10490
  message: f.message,
9947
10491
  hint: f.fix ?? ""
@@ -9957,12 +10501,12 @@ var AUTHORING_RULES = [
9957
10501
  commands: ["validate", "build"],
9958
10502
  source: "packages/lint/src/data-model-rules.ts",
9959
10503
  surfaces: CLI_ONLY,
9960
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10504
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9961
10505
  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.",
9962
10506
  run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9963
10507
  severity: f.severity === "suggestion" ? "info" : f.severity,
9964
10508
  rule: f.rule,
9965
- where: f.path,
10509
+ where: f.where,
9966
10510
  path: f.path,
9967
10511
  message: f.message,
9968
10512
  hint: f.fix ?? ""
@@ -9979,12 +10523,12 @@ var AUTHORING_RULES = [
9979
10523
  commands: ["validate", "build"],
9980
10524
  source: "packages/lint/src/data-model-rules.ts",
9981
10525
  surfaces: CLI_ONLY,
9982
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10526
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
9983
10527
  scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly alongside R10/R11 in 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.",
9984
10528
  run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
9985
10529
  severity: f.severity === "suggestion" ? "info" : f.severity,
9986
10530
  rule: f.rule,
9987
- where: f.path,
10531
+ where: f.where,
9988
10532
  path: f.path,
9989
10533
  message: f.message,
9990
10534
  hint: f.fix ?? ""
@@ -10112,16 +10656,26 @@ var AUTHORING_RULES = [
10112
10656
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10113
10657
  run: (stack) => validateOrgAxisRedLines(stack)
10114
10658
  },
10115
- // #4698 — the "declared but never read" gate, for the one surface where the
10116
- // predicate is EXACT rather than inferred. A sharing rule's `condition` has a
10117
- // single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
10118
- // the key is `compileCelToFilter(condition, { variables: {} })`; a condition
10119
- // that does not lower means the rule is SKIPPED at boot, so the grant is
10120
- // declared and does not exist. The lint calls that same compiler, from the
10121
- // same package, with the same options the verdict cannot drift from the
10122
- // consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
10123
- // docblock states: the whole authorable surface is enforced, and this was the
10124
- // one field where that sentence was not yet true.
10659
+ // #4698 / #9698 — the "declared but never read" gate, for the two fields of a
10660
+ // sharing rule where the predicate is EXACT rather than inferred.
10661
+ //
10662
+ // - `condition` has a single runtime consumer
10663
+ // (`bootstrapDeclaredSharingRules`) whose only use of the key is
10664
+ // `compileCelToFilter(condition, { variables: {} })`; a condition that
10665
+ // does not lower means the rule is SKIPPED at boot.
10666
+ // - `object` decides whether the grant is refused outright: reconcile hands
10667
+ // each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
10668
+ // `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
10669
+ // `public` or it is a `controlled_by_parent` detail. Both are decidable
10670
+ // from authored metadata; the other arms of that verdict (`owner_id`, the
10671
+ // bypass set, federated anchors) are not, and are excluded by name.
10672
+ //
10673
+ // Either way the grant is declared and does not exist. The lint calls the
10674
+ // same compiler and mirrors the same verdict function, from the same inputs
10675
+ // — the verdict cannot drift from the consumers'. Gating for the ADR-0078
10676
+ // reason `SharingRuleSchema`'s own docblock states: the whole authorable
10677
+ // surface is enforced, and these were the fields where that sentence was not
10678
+ // yet true.
10125
10679
  {
10126
10680
  name: "validateSharingRuleEnforceability",
10127
10681
  tier: "gating",
@@ -10129,7 +10683,7 @@ var AUTHORING_RULES = [
10129
10683
  commands: ALL,
10130
10684
  source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
10131
10685
  surfaces: CLI_ONLY,
10132
- surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. The rule itself is snapshot-safe \u2014 it reads ONLY `stack.sharingRules[].condition` and needs no other collection \u2014 so widening it here is a `runtimeTypes: ['sharing_rule']` edit once the gate accepts that type, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
10686
+ surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. This entry used to add that the rule reads ONLY `stack.sharingRules[].condition` and needs no other collection, so crossing was a lone `runtimeTypes` edit. #9698 FALSIFIED that: the anchor arm resolves `sharingRules[].object` against `stack.objects` to read the anchor's OWD, so the rule is now cross-collection. `objects` IS carried by the per-write snapshot (`CONTEXT_STACK_KEYS`, #8309), so the remaining gap is unchanged in SHAPE \u2014 the gate must accept a `sharing_rule` type and the snapshot must carry `sharingRules`, which it does not \u2014 but it is now TWO collections, not one. Crossing with `sharingRules` uncarried would enforce this id for zero of its inputs while the entry claimed the door (#7220). Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
10133
10687
  run: (stack) => validateSharingRuleEnforceability(stack)
10134
10688
  },
10135
10689
  // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
@@ -10168,8 +10722,15 @@ var AUTHORING_RULES = [
10168
10722
  input: "parsed",
10169
10723
  commands: ALL,
10170
10724
  source: "packages/lint/src/validate-rule-compilability.ts",
10171
- surfaces: CLI_ONLY,
10172
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10725
+ // Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
10726
+ // judged snapshot actually carries a `json_schema` validation — so an
10727
+ // ordinary object write (no `json_schema` anywhere) still loads no
10728
+ // compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
10729
+ // The load it does take (~64 ms cold once, ~15 ms warm per publish
10730
+ // carrying such a rule) is the measured, adjudicated price of refusing a
10731
+ // validation rule that would otherwise ship compiled-by-nothing.
10732
+ surfaces: CLI_AND_RUNTIME,
10733
+ runtimeTypes: ["object"],
10173
10734
  run: (stack) => validateRuleCompilability(stack)
10174
10735
  },
10175
10736
  // #5178 — the residual half of #5029, which registering `ajv-formats` does
@@ -10190,8 +10751,12 @@ var AUTHORING_RULES = [
10190
10751
  input: "parsed",
10191
10752
  commands: ALL,
10192
10753
  source: "packages/lint/src/validate-rule-schema-formats.ts",
10193
- surfaces: CLI_ONLY,
10194
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10754
+ // Runtime publish gate (#4716): crosses with its compile sibling above —
10755
+ // the two judgements over one artifact stay on one side of the wall
10756
+ // (#7220's family discipline). Same lazy-ajv contract: the registered
10757
+ // format set is only enumerated once a schema actually names a format.
10758
+ surfaces: CLI_AND_RUNTIME,
10759
+ runtimeTypes: ["object"],
10195
10760
  run: (stack) => validateRuleSchemaFormats(stack)
10196
10761
  }
10197
10762
  ];
@@ -10233,7 +10798,20 @@ var TYPE_TO_STACK_KEY = {
10233
10798
  permission: "permissions",
10234
10799
  book: "books"
10235
10800
  };
10236
- var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
10801
+ function narrowObjectsToPackageClosure(objects, scope) {
10802
+ if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
10803
+ const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
10804
+ return objects.filter((entry) => {
10805
+ if (!entry || typeof entry !== "object") return true;
10806
+ const owner = entry[PACKAGE_PROVENANCE_KEY];
10807
+ if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
10808
+ if (reachable.has(owner)) return true;
10809
+ return isSystemObject(entry);
10810
+ });
10811
+ }
10812
+ var PACKAGE_PROVENANCE_KEY = "_packageId";
10813
+ var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
10814
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
10237
10815
  function runtimeAuthoringRulesFor(type) {
10238
10816
  return AUTHORING_RULES.filter(
10239
10817
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -10259,7 +10837,8 @@ function buildRuntimeWriteSnapshots(args) {
10259
10837
  const itemName = typeof item.name === "string" ? item.name : void 0;
10260
10838
  const baseline = {};
10261
10839
  for (const key of CONTEXT_STACK_KEYS) {
10262
- const collection = args.context?.[key] ?? [];
10840
+ const raw = args.context?.[key] ?? [];
10841
+ const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
10263
10842
  baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10264
10843
  }
10265
10844
  const candidate = {
@@ -10293,7 +10872,8 @@ function runRuntimeAuthoringRules(args) {
10293
10872
  const snapshots = buildRuntimeWriteSnapshots({
10294
10873
  type: args.type,
10295
10874
  item: args.item,
10296
- ...args.context !== void 0 ? { context: args.context } : {}
10875
+ ...args.context !== void 0 ? { context: args.context } : {},
10876
+ ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10297
10877
  });
10298
10878
  if (!snapshots) return empty;
10299
10879
  const ctx = { sduiManifest: args.sduiManifest };
@@ -10307,6 +10887,7 @@ function runRuntimeAuthoringRules(args) {
10307
10887
  }
10308
10888
  export {
10309
10889
  buildRuntimeWriteSnapshots,
10890
+ narrowObjectsToPackageClosure,
10310
10891
  runRuntimeAuthoringRules,
10311
10892
  runtimeAuthoringRulesFor,
10312
10893
  runtimeGatedTypes,