@objectstack/lint 17.0.0 → 17.2.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/index.js CHANGED
@@ -586,10 +586,10 @@ var OUTCOME_CLAUSE = {
586
586
  "fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
587
587
  "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)"
588
588
  };
589
- function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
589
+ function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
590
590
  const owner = objectName ? `'${objectName}'` : "this object";
591
- const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
592
- 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}`;
591
+ const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
592
+ 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}`;
593
593
  }
594
594
 
595
595
  // src/validate-expressions.ts
@@ -772,10 +772,10 @@ function validateStackExpressions(stack) {
772
772
  if (!nullableFields || nullableFields.size === 0) return;
773
773
  const source = celSourceOf(raw);
774
774
  if (!source) return;
775
- for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
775
+ for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
776
776
  issues.push({
777
777
  where,
778
- message: nullGuardMessage(subject, objectName, finding, outcome),
778
+ message: nullGuardMessage(subject, objectName, finding2, outcome),
779
779
  source,
780
780
  severity: "error"
781
781
  });
@@ -799,7 +799,7 @@ function validateStackExpressions(stack) {
799
799
  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";
800
800
  const FIELD_RULE_SLOT_CONSEQUENCE = {
801
801
  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)",
802
- 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",
802
+ 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",
803
803
  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",
804
804
  // Listed rather than left to the `??` below, so the map covers every slot
805
805
  // the field walk passes and the default stays unreachable. `FieldSchema`
@@ -858,14 +858,15 @@ function validateStackExpressions(stack) {
858
858
  if (retired.length > 0) {
859
859
  issues.push({
860
860
  where: `${at} \xB7 node '${node.id}' (script) callable`,
861
- 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
862
- // behaviour, never the retired key's fate — "rewrite it" reads two ways
863
- // over a branch that DELETES the key (template/recipients/variables/script),
864
- // "rewrite existing sources" only one. Plain-quoted (not a template literal)
861
+ 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
862
+ // sentence names the TOOL's behaviour, never the retired key's fate —
863
+ // "rewrite it" read two ways over a branch that DELETES the key
864
+ // (template/recipients/variables/script), and the tool never rewrote a
865
+ // source file at all. Plain-quoted (not a template literal)
865
866
  // so this site is a member of `retired-key-migrate-sentence.test.ts`'s
866
867
  // widened scan (#7030) on the same textual shape as the spec corpus — no
867
868
  // interpolation lives in this clause, so nothing is lost switching quote style.
868
- "Run `os migrate meta --from 16` to rewrite existing sources automatically.",
869
+ "Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
869
870
  source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
870
871
  });
871
872
  } else if (!fn) {
@@ -936,10 +937,10 @@ function validateStackExpressions(stack) {
936
937
  f.expression,
937
938
  objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
938
939
  );
939
- const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
940
- for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
941
- for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
942
- warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
940
+ const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
941
+ for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
942
+ for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
943
+ warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
943
944
  }
944
945
  }
945
946
  }
@@ -1015,6 +1016,38 @@ function validateStackExpressions(stack) {
1015
1016
 
1016
1017
  // src/lint-startup-registry-verdict.ts
1017
1018
  import { createRequire } from "module";
1019
+
1020
+ // src/checked-parse.ts
1021
+ function createSourceFileChecked(tsc, fileName, source, options) {
1022
+ const sourceFile = tsc.createSourceFile(
1023
+ fileName,
1024
+ source,
1025
+ options.target,
1026
+ options.setParentNodes,
1027
+ options.scriptKind
1028
+ );
1029
+ const diagnostics = sourceFile.parseDiagnostics;
1030
+ if (!diagnostics || diagnostics.length === 0) return { sourceFile };
1031
+ const first = diagnostics[0];
1032
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
1033
+ const offset = options.synthesizedLinesBefore ?? 0;
1034
+ return {
1035
+ sourceFile,
1036
+ failure: {
1037
+ message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
1038
+ line: Math.max(1, line + 1 - offset),
1039
+ column: character + 1,
1040
+ count: diagnostics.length
1041
+ }
1042
+ };
1043
+ }
1044
+ function describeParseFailure(failure) {
1045
+ const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
1046
+ return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
1047
+ }
1048
+ var PARSE_FAILURE_HINT = "The source is parsed (never executed) so it can be checked. A source with syntax errors is only partially recovered, so the checks that follow may have skipped real problems \u2014 fix the syntax error and re-run to get a verdict that covers the whole source.";
1049
+
1050
+ // src/lint-startup-registry-verdict.ts
1018
1051
  var cachedTs = null;
1019
1052
  function loadTypeScript() {
1020
1053
  if (cachedTs) return cachedTs;
@@ -1030,6 +1063,7 @@ function loadTypeScript() {
1030
1063
  }
1031
1064
  var STARTUP_OPEN_VOCABULARY_VERDICT = "startup-open-vocabulary-verdict";
1032
1065
  var STARTUP_VERDICT_ASSERTIVE_WORDING = "startup-verdict-assertive-wording";
1066
+ var STARTUP_SOURCE_UNPARSEABLE = "startup-source-unparseable";
1033
1067
  var OPEN_VOCABULARY_PROBES = /* @__PURE__ */ new Map([
1034
1068
  [
1035
1069
  "getRegisteredNodeTypes",
@@ -1258,13 +1292,22 @@ function findStartupRegistryVerdicts(source, options = {}) {
1258
1292
  if (!mentionsAny) return [];
1259
1293
  const t = loadTypeScript();
1260
1294
  const fileLabel = options.file ?? "source";
1261
- let sf;
1262
- try {
1263
- sf = t.createSourceFile(fileLabel, source, t.ScriptTarget.Latest, true, t.ScriptKind.TS);
1264
- } catch {
1265
- return [];
1266
- }
1295
+ const { sourceFile: sf, failure } = createSourceFileChecked(t, fileLabel, source, {
1296
+ target: t.ScriptTarget.Latest,
1297
+ setParentNodes: true,
1298
+ scriptKind: t.ScriptKind.TS
1299
+ });
1267
1300
  const findings = [];
1301
+ if (failure) {
1302
+ findings.push({
1303
+ severity: "warning",
1304
+ rule: STARTUP_SOURCE_UNPARSEABLE,
1305
+ where: fileLabel,
1306
+ path: `${fileLabel}:${failure.line}`,
1307
+ message: `source did not parse (${describeParseFailure(failure)}), so this rule read a partially recovered tree \u2014 a startup verdict in the unread part is not reported.`,
1308
+ hint: PARSE_FAILURE_HINT
1309
+ });
1310
+ }
1268
1311
  const moduleBindings = moduleLevelMutableBindings(t, sf);
1269
1312
  const functionBodies = indexFunctionBodies(t, sf);
1270
1313
  const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
@@ -2580,6 +2623,7 @@ import {
2580
2623
  } from "@objectstack/spec/data";
2581
2624
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
2582
2625
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
2626
+ var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
2583
2627
  function asArray10(v) {
2584
2628
  if (Array.isArray(v)) return v;
2585
2629
  if (v && typeof v === "object") {
@@ -2670,7 +2714,7 @@ function indexObjectSearchTargets(stack) {
2670
2714
  }
2671
2715
  return fieldsByObject;
2672
2716
  }
2673
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
2717
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
2674
2718
  const findings = [];
2675
2719
  if (!Array.isArray(declared) || declared.length === 0) return findings;
2676
2720
  if (!objectName) return findings;
@@ -2678,6 +2722,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2678
2722
  const target = fieldsByObject.get(objectName);
2679
2723
  if (!target) return findings;
2680
2724
  const known = target.names;
2725
+ const anchors = unprovisionedAnchors?.get(objectName);
2681
2726
  const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
2682
2727
  for (let i = 0; i < declared.length; i++) {
2683
2728
  const entry = declared[i];
@@ -2695,6 +2740,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2695
2740
  });
2696
2741
  continue;
2697
2742
  }
2743
+ if (anchors?.has(name)) {
2744
+ findings.push({
2745
+ severity: "warning",
2746
+ rule: SEARCHABLE_FIELD_UNPROVISIONED,
2747
+ where,
2748
+ path: `${path}[${i}]`,
2749
+ 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.`),
2750
+ hint: unprovisionedAnchorHint(objectName, name)
2751
+ });
2752
+ }
2698
2753
  if (isVirtualSearchField(target.fields[name])) {
2699
2754
  const vtype = target.fields[name]?.type;
2700
2755
  findings.push({
@@ -2748,9 +2803,19 @@ function validateSearchableFields(stack) {
2748
2803
  if (!isRec4(stack)) return findings;
2749
2804
  const objects = asArray10(stack.objects);
2750
2805
  const fieldsByObject = indexObjectSearchTargets(stack);
2806
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2751
2807
  const check = (declared, objectName, where, path, subject, role) => {
2752
2808
  findings.push(
2753
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
2809
+ ...checkSearchableFieldList(
2810
+ declared,
2811
+ objectName,
2812
+ fieldsByObject,
2813
+ where,
2814
+ path,
2815
+ subject,
2816
+ role,
2817
+ unprovisionedAnchors
2818
+ )
2754
2819
  );
2755
2820
  };
2756
2821
  for (let oi = 0; oi < objects.length; oi++) {
@@ -2788,6 +2853,26 @@ function validateSearchableFields(stack) {
2788
2853
  if (!isRec4(view)) continue;
2789
2854
  const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
2790
2855
  const viewObject = strName2(view.objectName) ?? strName2(view.object);
2856
+ if (view.viewKind === "list" && !isRec4(view.config)) {
2857
+ check(
2858
+ view.searchableFields,
2859
+ listViewObject(view) ?? viewObject,
2860
+ `view "${viewLabel2}" (flattened list overlay)`,
2861
+ `views[${vi}].searchableFields`,
2862
+ "list-view searchableFields",
2863
+ "narrowing"
2864
+ );
2865
+ }
2866
+ if (view.viewKind === "list" && isRec4(view.config)) {
2867
+ check(
2868
+ view.config.searchableFields,
2869
+ listViewObject(view.config) ?? viewObject,
2870
+ `view "${viewLabel2}" (ViewItem record)`,
2871
+ `views[${vi}].config.searchableFields`,
2872
+ "list-view searchableFields",
2873
+ "narrowing"
2874
+ );
2875
+ }
2791
2876
  if (isRec4(view.list)) {
2792
2877
  check(
2793
2878
  view.list.searchableFields,
@@ -2937,7 +3022,12 @@ var COMPONENT_FIELD_SPECS = {
2937
3022
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2938
3023
  "record:path": { props: ["statusField"] },
2939
3024
  "element:number": { props: ["field"] },
2940
- "element:filter": { props: ["fields"] },
3025
+ // `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
3026
+ // whole element at element grain (ADR-0049 — no renderer ever shipped for it).
3027
+ // Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
3028
+ // spec-conformant page carries `fields` on it, and the #5068 props gate
3029
+ // reports an authored one by name with the element-retirement prescription —
3030
+ // the same #5775/#6629 residue class as the record-picker entries below.
2941
3031
  "element:form": { props: ["fields"] },
2942
3032
  // `labelField` is the one field-bearing prop this element declares. Its former
2943
3033
  // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
@@ -3229,6 +3319,7 @@ function filterAttrValue(tsc, sf, attr) {
3229
3319
  };
3230
3320
  return perPosition(init.expression);
3231
3321
  }
3322
+ var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
3232
3323
  var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
3233
3324
  var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
3234
3325
  var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
@@ -3557,11 +3648,20 @@ function validateReactPageProps(stack) {
3557
3648
  if (typeof source !== "string" || source.trim() === "") continue;
3558
3649
  const name = String(page.name ?? `#${p}`);
3559
3650
  const tsc = loadTypeScript2();
3560
- let sf;
3561
- try {
3562
- sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
3563
- } catch {
3564
- continue;
3651
+ const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
3652
+ target: tsc.ScriptTarget.Latest,
3653
+ setParentNodes: true,
3654
+ scriptKind: tsc.ScriptKind.TSX
3655
+ });
3656
+ if (failure) {
3657
+ findings.push({
3658
+ severity: "warning",
3659
+ rule: REACT_PAGE_SOURCE_UNPARSEABLE,
3660
+ where: `page "${name}"`,
3661
+ path: `pages[${p}].source`,
3662
+ message: `kind:'react' source did not parse (${describeParseFailure(failure)}), so the component-contract checks read a partially recovered tree and may have missed real problems.`,
3663
+ hint: PARSE_FAILURE_HINT
3664
+ });
3565
3665
  }
3566
3666
  const locals = localComponentNames(tsc, sf);
3567
3667
  const visit = (node) => {
@@ -3640,7 +3740,12 @@ function validateReactPageProps(stack) {
3640
3740
  searchTargets,
3641
3741
  where,
3642
3742
  `${path} \u203A searchableFields`,
3643
- "searchableFields"
3743
+ "searchableFields",
3744
+ // A `<ListView>` prop is a view-level narrowing — the checker's
3745
+ // default, spelled out here because the #8404 provenance index
3746
+ // follows it positionally.
3747
+ "narrowing",
3748
+ unprovisionedAnchors
3644
3749
  )
3645
3750
  );
3646
3751
  }
@@ -5225,6 +5330,7 @@ function validateSecurityPosture(stack, opts) {
5225
5330
  }
5226
5331
  }
5227
5332
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
5333
+ const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
5228
5334
  const nowMs = opts?.nowMs ?? Date.now();
5229
5335
  for (const [i, seed] of asArray19(stack.data).entries()) {
5230
5336
  const seedObject = typeof seed.object === "string" ? seed.object : "";
@@ -5248,7 +5354,7 @@ function validateSecurityPosture(stack, opts) {
5248
5354
  }
5249
5355
  }
5250
5356
  const delegatedFrom = rec.delegated_from;
5251
- if (delegatedFrom != null && delegatedFrom !== "") {
5357
+ if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
5252
5358
  const reason = rec.reason;
5253
5359
  if (typeof reason !== "string" || reason.trim().length === 0) {
5254
5360
  findings.push({
@@ -5414,6 +5520,8 @@ function validateOrgAxisRedLines(stack) {
5414
5520
  import { compileCelToFilter } from "@objectstack/formula";
5415
5521
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
5416
5522
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
5523
+ var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
5524
+ var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
5417
5525
  function asArray21(v) {
5418
5526
  if (Array.isArray(v)) return v;
5419
5527
  if (v && typeof v === "object") {
@@ -5438,10 +5546,67 @@ function sourceOf(condition) {
5438
5546
  return str2(input?.source);
5439
5547
  }
5440
5548
  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).";
5549
+ function effectiveSharingModelOf(obj) {
5550
+ const m = obj.sharingModel;
5551
+ if (m === "private") return "private";
5552
+ if (m === "public_read") return "read";
5553
+ if (m === "public_read_write" || m === "controlled_by_parent") return "public";
5554
+ if (m == null) {
5555
+ const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
5556
+ return isSystem ? "public" : "private";
5557
+ }
5558
+ return "private";
5559
+ }
5560
+ function masterOf(obj) {
5561
+ for (const f of asArray21(obj.fields)) {
5562
+ if (f.type === "master_detail") {
5563
+ const ref = f.reference;
5564
+ if (typeof ref === "string" && ref) return ref;
5565
+ }
5566
+ }
5567
+ return void 0;
5568
+ }
5569
+ function anchorFindings(rule, index, objectsByName) {
5570
+ const object = str2(rule.object);
5571
+ if (!object) return [];
5572
+ const target = objectsByName.get(object);
5573
+ if (!target) return [];
5574
+ const name = str2(rule.name) || String(index);
5575
+ const where = `sharing rule "${name}" on object "${object}"`;
5576
+ const path = `sharingRules[${index}].object`;
5577
+ const owd = target.sharingModel;
5578
+ if (owd === "controlled_by_parent") {
5579
+ const master = masterOf(target);
5580
+ return [{
5581
+ severity: "error",
5582
+ rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
5583
+ where,
5584
+ path,
5585
+ 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.`,
5586
+ 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.`
5587
+ }];
5588
+ }
5589
+ if (effectiveSharingModelOf(target) !== "public") return [];
5590
+ 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`;
5591
+ return [{
5592
+ severity: "error",
5593
+ rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
5594
+ where,
5595
+ path,
5596
+ 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.`,
5597
+ 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.`
5598
+ }];
5599
+ }
5441
5600
  function validateSharingRuleEnforceability(stack) {
5442
5601
  const findings = [];
5443
5602
  const cfg = stack ?? {};
5603
+ const objectsByName = /* @__PURE__ */ new Map();
5604
+ for (const obj of asArray21(cfg.objects)) {
5605
+ const name = str2(obj.name);
5606
+ if (name) objectsByName.set(name, obj);
5607
+ }
5444
5608
  asArray21(cfg.sharingRules).forEach((rule, index) => {
5609
+ anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
5445
5610
  const input = toCompilerInput(rule.condition);
5446
5611
  if (input === null) return;
5447
5612
  const result = compileCelToFilter(input, { variables: {} });
@@ -5579,7 +5744,6 @@ function asArray23(v) {
5579
5744
  function strName7(v) {
5580
5745
  return typeof v === "string" && v.length > 0 ? v : void 0;
5581
5746
  }
5582
- var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
5583
5747
  var URL_COLLECTION_TO_STACK_KEY = {
5584
5748
  object: "objects",
5585
5749
  objects: "objects",
@@ -5624,13 +5788,10 @@ function collectKnownTargets(stack) {
5624
5788
  return { actions, objects, reports, dashboards, pages, views };
5625
5789
  }
5626
5790
  function resolveActionTarget(actionType, target, known) {
5627
- if (known.actions.has(target)) return true;
5628
5791
  if (actionType === "modal") {
5629
- if (known.objects.has(target)) return true;
5630
- const m = MODAL_VERB_RE.exec(target);
5631
- if (m && known.objects.has(m[1])) return true;
5792
+ return known.pages.has(target);
5632
5793
  }
5633
- return false;
5794
+ return known.actions.has(target);
5634
5795
  }
5635
5796
  function resolveUrlRoute(target, known) {
5636
5797
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
@@ -5660,14 +5821,13 @@ function validateDashboardActionRefs(stack) {
5660
5821
  const actionType = strName7(action.actionType) ?? "url";
5661
5822
  if (actionType === "script" || actionType === "modal") {
5662
5823
  if (resolveActionTarget(actionType, target, known)) return;
5663
- const kindWord = actionType === "script" ? "script" : "modal";
5664
5824
  findings.push({
5665
5825
  severity: "error",
5666
5826
  rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
5667
5827
  where,
5668
5828
  path,
5669
- 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).`,
5670
- 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.`
5829
+ 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).`,
5830
+ 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.`
5671
5831
  });
5672
5832
  return;
5673
5833
  }
@@ -5709,7 +5869,7 @@ function validateDashboardActionRefs(stack) {
5709
5869
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
5710
5870
 
5711
5871
  // src/filter-walk.ts
5712
- var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
5872
+ var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
5713
5873
  function asArray24(v) {
5714
5874
  if (Array.isArray(v)) return v;
5715
5875
  if (v && typeof v === "object") {
@@ -5813,6 +5973,149 @@ function validateFilterTokens(stack) {
5813
5973
  return out;
5814
5974
  }
5815
5975
 
5976
+ // src/validate-preset-comparands.ts
5977
+ import {
5978
+ VALID_AST_OPERATORS as VALID_AST_OPERATORS2,
5979
+ bareDateRangePresetComparandMessage,
5980
+ canonicalAstOperator,
5981
+ isDateRangePresetName
5982
+ } from "@objectstack/spec/data";
5983
+ import { normalizeFilterOperator } from "@objectstack/spec/ui";
5984
+ var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
5985
+ var PRESET_COMPARAND_SURFACES = [
5986
+ { key: "dashboards", kind: "dashboard" },
5987
+ { key: "objects", kind: "object" },
5988
+ { key: "views", kind: "view" },
5989
+ { key: "reports", kind: "report" },
5990
+ { key: "datasets", kind: "dataset" },
5991
+ { key: "pages", kind: "page" },
5992
+ { key: "apps", kind: "app" },
5993
+ { key: "flows", kind: "flow" }
5994
+ ];
5995
+ var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
5996
+ var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
5997
+ var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
5998
+ "greater_than",
5999
+ "greater_than_or_equal",
6000
+ "less_than",
6001
+ "less_than_or_equal",
6002
+ "before",
6003
+ "after"
6004
+ ]);
6005
+ var MAX_DEPTH = 32;
6006
+ function isPlainObject(v) {
6007
+ return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
6008
+ }
6009
+ function finding(where, path, preset, operator) {
6010
+ return {
6011
+ severity: "error",
6012
+ rule: FILTER_PRESET_COMPARAND,
6013
+ where,
6014
+ path,
6015
+ message: bareDateRangePresetComparandMessage(preset, operator),
6016
+ 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."
6017
+ };
6018
+ }
6019
+ function judgeConditionNode(node, path, where, out, depth) {
6020
+ if (depth > MAX_DEPTH) return;
6021
+ for (const [key, value] of Object.entries(node)) {
6022
+ const here = `${path}.${key}`;
6023
+ if (key === "$and" || key === "$or") {
6024
+ if (Array.isArray(value)) {
6025
+ value.forEach((arm, i) => {
6026
+ if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
6027
+ });
6028
+ }
6029
+ continue;
6030
+ }
6031
+ if (key === "$not") {
6032
+ if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
6033
+ continue;
6034
+ }
6035
+ if (key.startsWith("$")) continue;
6036
+ if (!isPlainObject(value)) continue;
6037
+ const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
6038
+ if (!hasOps) {
6039
+ judgeConditionNode(value, here, where, out, depth + 1);
6040
+ continue;
6041
+ }
6042
+ for (const [op, comparand] of Object.entries(value)) {
6043
+ if (ORDERING_DOLLAR_OPS.has(op) && isDateRangePresetName(comparand)) {
6044
+ out.push(finding(where, `${here}.${op}`, comparand, op));
6045
+ continue;
6046
+ }
6047
+ if (op === "$between" && Array.isArray(comparand)) {
6048
+ comparand.forEach((endpoint, i) => {
6049
+ if (isDateRangePresetName(endpoint)) {
6050
+ out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
6051
+ }
6052
+ });
6053
+ }
6054
+ }
6055
+ }
6056
+ }
6057
+ function judgeFilterRule(rule, path, where, out) {
6058
+ const operator = normalizeFilterOperator(rule.operator);
6059
+ if (typeof operator !== "string") return;
6060
+ const value = rule.value;
6061
+ if (ORDERING_RULE_OPS.has(operator) && isDateRangePresetName(value)) {
6062
+ out.push(finding(where, `${path}.value`, value, operator));
6063
+ return;
6064
+ }
6065
+ if (operator === "between" && Array.isArray(value)) {
6066
+ value.forEach((endpoint, i) => {
6067
+ if (isDateRangePresetName(endpoint)) {
6068
+ out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
6069
+ }
6070
+ });
6071
+ }
6072
+ }
6073
+ function judgeTriple(triple, path, where, out) {
6074
+ const op = triple[1];
6075
+ if (typeof op !== "string") return;
6076
+ const canonical = canonicalAstOperator(op);
6077
+ const value = triple[2];
6078
+ if (ORDERING_INFIX_OPS.has(canonical) && isDateRangePresetName(value)) {
6079
+ out.push(finding(where, `${path}[2]`, value, op));
6080
+ return;
6081
+ }
6082
+ if (canonical === "between" && Array.isArray(value)) {
6083
+ value.forEach((endpoint, i) => {
6084
+ if (isDateRangePresetName(endpoint)) {
6085
+ out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
6086
+ }
6087
+ });
6088
+ }
6089
+ }
6090
+ function judgeFilterValue(node, path, where, out, depth) {
6091
+ if (depth > MAX_DEPTH) return;
6092
+ if (Array.isArray(node)) {
6093
+ if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && VALID_AST_OPERATORS2.has(node[1].toLowerCase())) {
6094
+ judgeTriple(node, path, where, out);
6095
+ return;
6096
+ }
6097
+ node.forEach((member, i) => {
6098
+ if (typeof member === "string") return;
6099
+ judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
6100
+ });
6101
+ return;
6102
+ }
6103
+ if (!isPlainObject(node)) return;
6104
+ if (typeof node.field === "string" && typeof node.operator === "string") {
6105
+ judgeFilterRule(node, path, where, out);
6106
+ return;
6107
+ }
6108
+ judgeConditionNode(node, path, where, out, depth);
6109
+ }
6110
+ function validatePresetComparands(stack) {
6111
+ if (!stack || typeof stack !== "object") return [];
6112
+ const out = [];
6113
+ walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
6114
+ judgeFilterValue(value, path, where, out, 0);
6115
+ });
6116
+ return out;
6117
+ }
6118
+
5816
6119
  // src/validate-empty-combinators.ts
5817
6120
  import { reduceFilterVerdict } from "@objectstack/spec/data";
5818
6121
  var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
@@ -6264,6 +6567,217 @@ function validateNavObjectServability(stack) {
6264
6567
  return findings;
6265
6568
  }
6266
6569
 
6570
+ // src/validate-sortable-fields.ts
6571
+ import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
6572
+ var SORT_FIELD_UNKNOWN = "sort-field-unknown";
6573
+ var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
6574
+ var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
6575
+ function isRec16(v) {
6576
+ return !!v && typeof v === "object" && !Array.isArray(v);
6577
+ }
6578
+ function strName11(v) {
6579
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6580
+ }
6581
+ function readSortKeys(declared) {
6582
+ const fromShorthand = (raw, at) => {
6583
+ const trimmed = raw.trim();
6584
+ if (!trimmed) return void 0;
6585
+ const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
6586
+ return bare ? { field: bare, at } : void 0;
6587
+ };
6588
+ if (typeof declared === "string") {
6589
+ return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
6590
+ }
6591
+ if (Array.isArray(declared)) {
6592
+ const keys = [];
6593
+ for (let i = 0; i < declared.length; i++) {
6594
+ const el = declared[i];
6595
+ if (typeof el === "string") {
6596
+ const k = fromShorthand(el, `[${i}]`);
6597
+ if (k) keys.push(k);
6598
+ continue;
6599
+ }
6600
+ if (!isRec16(el)) continue;
6601
+ const field = strName11(el.field);
6602
+ if (field) keys.push({ field: field.trim(), at: `[${i}]` });
6603
+ }
6604
+ return keys;
6605
+ }
6606
+ return [];
6607
+ }
6608
+ function suggest4(target, known) {
6609
+ let best;
6610
+ let bestScore = Infinity;
6611
+ for (const candidate of known) {
6612
+ const d = distance3(target, candidate);
6613
+ if (d < bestScore) {
6614
+ bestScore = d;
6615
+ best = candidate;
6616
+ }
6617
+ }
6618
+ const limit = Math.max(2, Math.floor(target.length / 3));
6619
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
6620
+ }
6621
+ function distance3(a, b) {
6622
+ const m = a.length;
6623
+ const n = b.length;
6624
+ if (m === 0) return n;
6625
+ if (n === 0) return m;
6626
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
6627
+ for (let i = 1; i <= m; i++) {
6628
+ const curr = [i, ...new Array(n).fill(0)];
6629
+ for (let j = 1; j <= n; j++) {
6630
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6631
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
6632
+ }
6633
+ prev = curr;
6634
+ }
6635
+ return prev[n];
6636
+ }
6637
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
6638
+ const findings = [];
6639
+ if (declared === void 0 || declared === null) return findings;
6640
+ if (!objectName) return findings;
6641
+ if (!fieldsByObject.has(objectName)) return findings;
6642
+ const target = fieldsByObject.get(objectName);
6643
+ if (!target) return findings;
6644
+ const known = target.names;
6645
+ const anchors = unprovisionedAnchors?.get(objectName);
6646
+ for (const key of readSortKeys(declared)) {
6647
+ const name = key.field;
6648
+ const head = name.split(".")[0];
6649
+ if (SYSTEM_FIELDS.has(head)) {
6650
+ if (!name.includes(".") && anchors?.has(head)) {
6651
+ findings.push({
6652
+ severity: "warning",
6653
+ rule: SORT_FIELD_UNPROVISIONED,
6654
+ where,
6655
+ path: `${path}${key.at}`,
6656
+ message: `${subject} orders by "${name}", which resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 so the ORDER BY reaches the driver, finds no column, and is dropped. Measured on a federated object over a real remote table, 'asc' and 'desc' return BYTE-IDENTICAL row order under a 200 while the same query on a real column reverses. Unlike this rule's other two verdicts nothing refuses it: the REST ingress (#6994) and the engine (#7095) both judge only 'formula', so the view's FIRST fetch \u2014 and every fetch after it \u2014 silently answers in an arbitrary order, which 'limit'/'offset' then slice into an arbitrary page.`,
6657
+ hint: unprovisionedAnchorHint(objectName, name)
6658
+ });
6659
+ }
6660
+ continue;
6661
+ }
6662
+ if (!known.has(head)) {
6663
+ const dotted = name.includes(".");
6664
+ findings.push({
6665
+ severity: "error",
6666
+ rule: SORT_FIELD_UNKNOWN,
6667
+ where,
6668
+ path: `${path}${key.at}`,
6669
+ 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)),
6670
+ 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(", ")}.` : "")
6671
+ });
6672
+ continue;
6673
+ }
6674
+ const meta = target.fields[name];
6675
+ if (isVirtualSearchField2(meta)) {
6676
+ const vtype = meta?.type;
6677
+ findings.push({
6678
+ severity: "error",
6679
+ rule: SORT_FIELD_UNSORTABLE,
6680
+ where,
6681
+ path: `${path}${key.at}`,
6682
+ 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.`,
6683
+ 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.`
6684
+ });
6685
+ }
6686
+ }
6687
+ return findings;
6688
+ }
6689
+ function validateSortableFields(stack) {
6690
+ const findings = [];
6691
+ if (!isRec16(stack)) return findings;
6692
+ const objects = Array.isArray(stack.objects) ? stack.objects : isRec16(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
6693
+ const fieldsByObject = indexObjectSearchTargets(stack);
6694
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
6695
+ const check = (declared, objectName, where, path, subject) => {
6696
+ findings.push(
6697
+ ...checkSortDeclaration(
6698
+ declared,
6699
+ objectName,
6700
+ fieldsByObject,
6701
+ where,
6702
+ path,
6703
+ subject,
6704
+ unprovisionedAnchors
6705
+ )
6706
+ );
6707
+ };
6708
+ for (let oi = 0; oi < objects.length; oi++) {
6709
+ const obj = objects[oi];
6710
+ if (!isRec16(obj)) continue;
6711
+ const objName = strName11(obj.name);
6712
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
6713
+ if (isRec16(obj.listViews)) {
6714
+ for (const [key, lv] of Object.entries(obj.listViews)) {
6715
+ if (!isRec16(lv)) continue;
6716
+ check(
6717
+ lv.sort,
6718
+ // A built-in list view belongs to its object; an inline `data.object`
6719
+ // may still retarget it (ADR-0047 allows the explicit binding).
6720
+ listViewObject2(lv) ?? objName,
6721
+ `${label2} \u203A listViews.${key}`,
6722
+ `objects[${oi}].listViews.${key}.sort`,
6723
+ "list-view sort"
6724
+ );
6725
+ }
6726
+ }
6727
+ }
6728
+ const views = Array.isArray(stack.views) ? stack.views : [];
6729
+ for (let vi = 0; vi < views.length; vi++) {
6730
+ const view = views[vi];
6731
+ if (!isRec16(view)) continue;
6732
+ const viewLabel2 = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
6733
+ const viewObject = strName11(view.objectName) ?? strName11(view.object);
6734
+ if (view.viewKind === "list" && !isRec16(view.config)) {
6735
+ check(
6736
+ view.sort,
6737
+ listViewObject2(view) ?? viewObject,
6738
+ `view "${viewLabel2}" (flattened list overlay)`,
6739
+ `views[${vi}].sort`,
6740
+ "list-view sort"
6741
+ );
6742
+ }
6743
+ if (view.viewKind === "list" && isRec16(view.config)) {
6744
+ check(
6745
+ view.config.sort,
6746
+ listViewObject2(view.config) ?? viewObject,
6747
+ `view "${viewLabel2}" (ViewItem record)`,
6748
+ `views[${vi}].config.sort`,
6749
+ "list-view sort"
6750
+ );
6751
+ }
6752
+ if (isRec16(view.list)) {
6753
+ check(
6754
+ view.list.sort,
6755
+ listViewObject2(view.list) ?? viewObject,
6756
+ `view "${viewLabel2}" \u203A list`,
6757
+ `views[${vi}].list.sort`,
6758
+ "list-view sort"
6759
+ );
6760
+ }
6761
+ if (isRec16(view.listViews)) {
6762
+ for (const [key, lv] of Object.entries(view.listViews)) {
6763
+ if (!isRec16(lv)) continue;
6764
+ check(
6765
+ lv.sort,
6766
+ listViewObject2(lv) ?? viewObject,
6767
+ `view "${viewLabel2}" \u203A listViews.${key}`,
6768
+ `views[${vi}].listViews.${key}.sort`,
6769
+ "list-view sort"
6770
+ );
6771
+ }
6772
+ }
6773
+ }
6774
+ return findings;
6775
+ }
6776
+ function listViewObject2(listView) {
6777
+ const data = listView.data;
6778
+ return isRec16(data) ? strName11(data.object) : void 0;
6779
+ }
6780
+
6267
6781
  // src/validate-action-name-refs.ts
6268
6782
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
6269
6783
  function asArray28(v) {
@@ -6273,13 +6787,13 @@ function asArray28(v) {
6273
6787
  }
6274
6788
  return [];
6275
6789
  }
6276
- function strName11(v) {
6790
+ function strName12(v) {
6277
6791
  return typeof v === "string" && v.length > 0 ? v : void 0;
6278
6792
  }
6279
6793
  function strList(v) {
6280
6794
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
6281
6795
  }
6282
- function distance3(a, b) {
6796
+ function distance4(a, b) {
6283
6797
  const m = a.length;
6284
6798
  const n = b.length;
6285
6799
  if (m === 0) return n;
@@ -6295,11 +6809,11 @@ function distance3(a, b) {
6295
6809
  }
6296
6810
  return prev[n];
6297
6811
  }
6298
- function suggest4(target, known) {
6812
+ function suggest5(target, known) {
6299
6813
  let best;
6300
6814
  let bestScore = Infinity;
6301
6815
  for (const candidate of known) {
6302
- const d = distance3(target, candidate);
6816
+ const d = distance4(target, candidate);
6303
6817
  if (d < bestScore) {
6304
6818
  bestScore = d;
6305
6819
  best = candidate;
@@ -6311,13 +6825,13 @@ function suggest4(target, known) {
6311
6825
  function collectActionNames(stack) {
6312
6826
  const names = /* @__PURE__ */ new Set();
6313
6827
  for (const action of asArray28(stack.actions)) {
6314
- const n = strName11(action?.name);
6828
+ const n = strName12(action?.name);
6315
6829
  if (n) names.add(n);
6316
6830
  }
6317
6831
  for (const obj of asArray28(stack.objects)) {
6318
6832
  if (!obj || typeof obj !== "object") continue;
6319
6833
  for (const action of asArray28(obj.actions)) {
6320
- const n = strName11(action?.name);
6834
+ const n = strName12(action?.name);
6321
6835
  if (n) names.add(n);
6322
6836
  }
6323
6837
  }
@@ -6334,7 +6848,7 @@ function validateActionNameRefs(stack) {
6334
6848
  rule: ACTION_NAME_UNDEFINED,
6335
6849
  where,
6336
6850
  path,
6337
- 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),
6851
+ 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),
6338
6852
  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(", ")}.` : "")
6339
6853
  });
6340
6854
  };
@@ -6360,7 +6874,7 @@ function validateActionNameRefs(stack) {
6360
6874
  if (!def || typeof def !== "object") continue;
6361
6875
  if (def.execution !== "aggregate") continue;
6362
6876
  if (def.actionDef !== void 0) continue;
6363
- const name = strName11(def.name);
6877
+ const name = strName12(def.name);
6364
6878
  if (!name) continue;
6365
6879
  check(
6366
6880
  name,
@@ -6375,7 +6889,7 @@ function validateActionNameRefs(stack) {
6375
6889
  for (let vi = 0; vi < views.length; vi++) {
6376
6890
  const view = views[vi];
6377
6891
  if (!view || typeof view !== "object") continue;
6378
- const viewName = strName11(view.name) ?? strName11(view.object) ?? `#${vi}`;
6892
+ const viewName = strName12(view.name) ?? strName12(view.object) ?? `#${vi}`;
6379
6893
  const owner = `view "${viewName}"`;
6380
6894
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
6381
6895
  const listViews = view.listViews;
@@ -6391,7 +6905,7 @@ function validateActionNameRefs(stack) {
6391
6905
  if (!obj || typeof obj !== "object") continue;
6392
6906
  const objListViews = obj.listViews;
6393
6907
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
6394
- const owner = `object "${strName11(obj.name) ?? `#${oi}`}"`;
6908
+ const owner = `object "${strName12(obj.name) ?? `#${oi}`}"`;
6395
6909
  for (const [key, lv] of Object.entries(objListViews)) {
6396
6910
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
6397
6911
  }
@@ -6400,7 +6914,7 @@ function validateActionNameRefs(stack) {
6400
6914
  for (let pi = 0; pi < pages.length; pi++) {
6401
6915
  const page = pages[pi];
6402
6916
  if (!page || typeof page !== "object") continue;
6403
- const pageName = strName11(page.name) ?? `#${pi}`;
6917
+ const pageName = strName12(page.name) ?? `#${pi}`;
6404
6918
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6405
6919
  const props = component.properties;
6406
6920
  if (!props || typeof props !== "object") continue;
@@ -6408,7 +6922,7 @@ function validateActionNameRefs(stack) {
6408
6922
  for (let ai = 0; ai < names.length; ai++) {
6409
6923
  check(
6410
6924
  names[ai],
6411
- `page "${pageName}" \xB7 component "${strName11(component.type) ?? "?"}"`,
6925
+ `page "${pageName}" \xB7 component "${strName12(component.type) ?? "?"}"`,
6412
6926
  `${path}.properties.actionNames[${ai}]`,
6413
6927
  "Quick-actions bar"
6414
6928
  );
@@ -6419,7 +6933,7 @@ function validateActionNameRefs(stack) {
6419
6933
  for (let ai = 0; ai < apps.length; ai++) {
6420
6934
  const app = apps[ai];
6421
6935
  if (!app || typeof app !== "object") continue;
6422
- const appName = strName11(app.name) ?? `#${ai}`;
6936
+ const appName = strName12(app.name) ?? `#${ai}`;
6423
6937
  const walkNav = (items, basePath) => {
6424
6938
  const navItems = asArray28(items);
6425
6939
  for (let ni = 0; ni < navItems.length; ni++) {
@@ -6427,20 +6941,20 @@ function validateActionNameRefs(stack) {
6427
6941
  if (!nav || typeof nav !== "object") continue;
6428
6942
  const navPath = `${basePath}[${ni}]`;
6429
6943
  const actionDef = nav.actionDef;
6430
- const actionName = strName11(actionDef?.actionName);
6944
+ const actionName = strName12(actionDef?.actionName);
6431
6945
  if (nav.type === "action" && actionName) {
6432
6946
  check(
6433
6947
  actionName,
6434
- `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
6948
+ `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
6435
6949
  `${navPath}.actionDef.actionName`,
6436
6950
  "Navigation action item"
6437
6951
  );
6438
6952
  }
6439
- const runAction = strName11(nav.runAction);
6953
+ const runAction = strName12(nav.runAction);
6440
6954
  if (nav.type === "object" && runAction) {
6441
6955
  check(
6442
6956
  runAction,
6443
- `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
6957
+ `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
6444
6958
  `${navPath}.runAction`,
6445
6959
  "Navigation deep-link auto-run"
6446
6960
  );
@@ -6466,7 +6980,7 @@ function asArray29(v) {
6466
6980
  }
6467
6981
  return [];
6468
6982
  }
6469
- function strName12(v) {
6983
+ function strName13(v) {
6470
6984
  return typeof v === "string" && v.length > 0 ? v : void 0;
6471
6985
  }
6472
6986
  function strList2(v) {
@@ -6481,7 +6995,7 @@ function collectNamePlacedActions(stack) {
6481
6995
  for (const n of strList2(list3[key])) placed.add(n);
6482
6996
  }
6483
6997
  for (const def of asArray29(list3.bulkActionDefs)) {
6484
- const n = strName12(def?.name);
6998
+ const n = strName13(def?.name);
6485
6999
  if (n) placed.add(n);
6486
7000
  }
6487
7001
  };
@@ -6507,7 +7021,7 @@ function validateActionLocations(stack) {
6507
7021
  const check = (action, path) => {
6508
7022
  if (!action || typeof action !== "object") return;
6509
7023
  if ("locations" in action) return;
6510
- const name = strName12(action.name);
7024
+ const name = strName13(action.name);
6511
7025
  if (!name) return;
6512
7026
  if (namePlaced.has(name)) return;
6513
7027
  findings.push({
@@ -6536,10 +7050,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
6536
7050
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
6537
7051
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
6538
7052
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
6539
- function isRec16(v) {
7053
+ function isRec17(v) {
6540
7054
  return !!v && typeof v === "object" && !Array.isArray(v);
6541
7055
  }
6542
- function strName13(v) {
7056
+ function strName14(v) {
6543
7057
  return typeof v === "string" && v.length > 0 ? v : void 0;
6544
7058
  }
6545
7059
  function asArray30(v) {
@@ -6553,8 +7067,8 @@ var PROPS_SCHEMAS = ComponentPropsMap;
6553
7067
  var DATASOURCE_SUPPLIED_PROP = "object";
6554
7068
  function suppliedByDataSource(issue, component) {
6555
7069
  if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
6556
- const dataSource = isRec16(component.dataSource) ? component.dataSource : void 0;
6557
- return strName13(dataSource?.object) !== void 0;
7070
+ const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
7071
+ return strName14(dataSource?.object) !== void 0;
6558
7072
  }
6559
7073
  function unrecognizedKeysFromUnionArm(issue) {
6560
7074
  if (issue.code !== "invalid_union") return void 0;
@@ -6571,18 +7085,18 @@ function unrecognizedKeysFromUnionArm(issue) {
6571
7085
  }
6572
7086
  function validateComponentProps(stack) {
6573
7087
  const findings = [];
6574
- if (!isRec16(stack)) return findings;
7088
+ if (!isRec17(stack)) return findings;
6575
7089
  const pages = asArray30(stack.pages);
6576
7090
  for (let pi = 0; pi < pages.length; pi++) {
6577
7091
  const page = pages[pi];
6578
- if (!isRec16(page)) continue;
6579
- const pageName = strName13(page.name) ?? `#${pi}`;
7092
+ if (!isRec17(page)) continue;
7093
+ const pageName = strName14(page.name) ?? `#${pi}`;
6580
7094
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6581
- const type = strName13(component.type);
7095
+ const type = strName14(component.type);
6582
7096
  if (!type) continue;
6583
7097
  const schema = PROPS_SCHEMAS[type];
6584
7098
  if (!schema) continue;
6585
- const props = isRec16(component.properties) ? component.properties : void 0;
7099
+ const props = isRec17(component.properties) ? component.properties : void 0;
6586
7100
  if (!props) continue;
6587
7101
  const where = `page "${pageName}" \xB7 ${type}`;
6588
7102
  const base = `${path}.properties`;
@@ -6654,16 +7168,16 @@ function asArray31(v) {
6654
7168
  }
6655
7169
  return [];
6656
7170
  }
6657
- function strName14(v) {
7171
+ function strName15(v) {
6658
7172
  return typeof v === "string" && v.length > 0 ? v : void 0;
6659
7173
  }
6660
7174
  function strList3(v) {
6661
7175
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
6662
7176
  }
6663
- function isRec17(v) {
7177
+ function isRec18(v) {
6664
7178
  return !!v && typeof v === "object" && !Array.isArray(v);
6665
7179
  }
6666
- function distance4(a, b) {
7180
+ function distance5(a, b) {
6667
7181
  const m = a.length;
6668
7182
  const n = b.length;
6669
7183
  if (m === 0) return n;
@@ -6679,11 +7193,11 @@ function distance4(a, b) {
6679
7193
  }
6680
7194
  return prev[n];
6681
7195
  }
6682
- function suggest5(target, known) {
7196
+ function suggest6(target, known) {
6683
7197
  let best;
6684
7198
  let bestScore = Infinity;
6685
7199
  for (const c of known) {
6686
- const d = distance4(target, c);
7200
+ const d = distance5(target, c);
6687
7201
  if (d < bestScore) {
6688
7202
  bestScore = d;
6689
7203
  best = c;
@@ -6699,16 +7213,16 @@ function list2(names) {
6699
7213
  function indexDatasets(stack) {
6700
7214
  const out = /* @__PURE__ */ new Map();
6701
7215
  for (const ds of asArray31(stack.datasets)) {
6702
- const name = strName14(ds.name);
7216
+ const name = strName15(ds.name);
6703
7217
  if (!name) continue;
6704
7218
  const dimensions = /* @__PURE__ */ new Set();
6705
7219
  for (const d of asArray31(ds.dimensions)) {
6706
- const n = strName14(d.name);
7220
+ const n = strName15(d.name);
6707
7221
  if (n) dimensions.add(n);
6708
7222
  }
6709
7223
  const measures = /* @__PURE__ */ new Set();
6710
7224
  for (const m of asArray31(ds.measures)) {
6711
- const n = strName14(m.name);
7225
+ const n = strName15(m.name);
6712
7226
  if (n) measures.add(n);
6713
7227
  }
6714
7228
  out.set(name, { dimensions, measures });
@@ -6731,7 +7245,7 @@ function validateChartBindings(stack) {
6731
7245
  where: binding.where,
6732
7246
  path: `${binding.path}.dataset`,
6733
7247
  message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
6734
- hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
7248
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
6735
7249
  });
6736
7250
  return;
6737
7251
  }
@@ -6743,7 +7257,7 @@ function validateChartBindings(stack) {
6743
7257
  where: binding.where,
6744
7258
  path,
6745
7259
  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.`,
6746
- hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
7260
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
6747
7261
  });
6748
7262
  };
6749
7263
  const measureRef = (name, path, selected2) => {
@@ -6754,7 +7268,7 @@ function validateChartBindings(stack) {
6754
7268
  where: binding.where,
6755
7269
  path,
6756
7270
  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.`,
6757
- hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
7271
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
6758
7272
  });
6759
7273
  return;
6760
7274
  }
@@ -6789,26 +7303,26 @@ function validateChartBindings(stack) {
6789
7303
  const reports = asArray31(stack.reports);
6790
7304
  for (let ri = 0; ri < reports.length; ri++) {
6791
7305
  const report = reports[ri];
6792
- if (!isRec17(report)) continue;
6793
- const reportName = strName14(report.name) ?? `#${ri}`;
7306
+ if (!isRec18(report)) continue;
7307
+ const reportName = strName15(report.name) ?? `#${ri}`;
6794
7308
  const checkReportChart = (chart, dataset, values, where, path) => {
6795
- if (!isRec17(chart)) return;
7309
+ if (!isRec18(chart)) return;
6796
7310
  check({
6797
7311
  dataset,
6798
7312
  // `values` is the report's measure SELECTION, not a chart ref; feeding
6799
7313
  // it in lets the yAxis "declared but not selected" check work without
6800
7314
  // reporting the selection itself twice.
6801
7315
  values: { names: values, path: `${path}.values` },
6802
- xAxis: strName14(chart.xAxis) ? { name: strName14(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
6803
- yAxis: strName14(chart.yAxis) ? { name: strName14(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
6804
- series: asArray31(chart.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
7316
+ xAxis: strName15(chart.xAxis) ? { name: strName15(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
7317
+ yAxis: strName15(chart.yAxis) ? { name: strName15(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
7318
+ series: asArray31(chart.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
6805
7319
  where,
6806
7320
  path: `${path}.chart`
6807
7321
  });
6808
7322
  };
6809
7323
  checkReportChart(
6810
7324
  report.chart,
6811
- strName14(report.dataset),
7325
+ strName15(report.dataset),
6812
7326
  strList3(report.values),
6813
7327
  `report "${reportName}" \xB7 chart`,
6814
7328
  `reports[${ri}]`
@@ -6816,22 +7330,22 @@ function validateChartBindings(stack) {
6816
7330
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
6817
7331
  for (let bi = 0; bi < blocks.length; bi++) {
6818
7332
  const block = blocks[bi];
6819
- if (!isRec17(block)) continue;
7333
+ if (!isRec18(block)) continue;
6820
7334
  checkReportChart(
6821
7335
  block.chart,
6822
- strName14(block.dataset),
7336
+ strName15(block.dataset),
6823
7337
  strList3(block.values),
6824
- `report "${reportName}" \xB7 block "${strName14(block.name) ?? `#${bi}`}" chart`,
7338
+ `report "${reportName}" \xB7 block "${strName15(block.name) ?? `#${bi}`}" chart`,
6825
7339
  `reports[${ri}].blocks[${bi}]`
6826
7340
  );
6827
7341
  }
6828
7342
  }
6829
7343
  const checkListChart = (container, where, path) => {
6830
- if (!isRec17(container)) return;
7344
+ if (!isRec18(container)) return;
6831
7345
  const chart = container.chart;
6832
- if (!isRec17(chart)) return;
7346
+ if (!isRec18(chart)) return;
6833
7347
  check({
6834
- dataset: strName14(chart.dataset),
7348
+ dataset: strName15(chart.dataset),
6835
7349
  dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
6836
7350
  values: { names: strList3(chart.values), path: `${path}.chart.values` },
6837
7351
  where,
@@ -6841,10 +7355,10 @@ function validateChartBindings(stack) {
6841
7355
  const views = asArray31(stack.views);
6842
7356
  for (let vi = 0; vi < views.length; vi++) {
6843
7357
  const view = views[vi];
6844
- if (!isRec17(view)) continue;
6845
- const viewName = strName14(view.name) ?? strName14(view.objectName) ?? `#${vi}`;
7358
+ if (!isRec18(view)) continue;
7359
+ const viewName = strName15(view.name) ?? strName15(view.objectName) ?? `#${vi}`;
6846
7360
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
6847
- if (isRec17(view.listViews)) {
7361
+ if (isRec18(view.listViews)) {
6848
7362
  for (const [key, lv] of Object.entries(view.listViews)) {
6849
7363
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
6850
7364
  }
@@ -6853,8 +7367,8 @@ function validateChartBindings(stack) {
6853
7367
  const objects = asArray31(stack.objects);
6854
7368
  for (let oi = 0; oi < objects.length; oi++) {
6855
7369
  const obj = objects[oi];
6856
- if (!isRec17(obj) || !isRec17(obj.listViews)) continue;
6857
- const objName = strName14(obj.name) ?? `#${oi}`;
7370
+ if (!isRec18(obj) || !isRec18(obj.listViews)) continue;
7371
+ const objName = strName15(obj.name) ?? `#${oi}`;
6858
7372
  for (const [key, lv] of Object.entries(obj.listViews)) {
6859
7373
  checkListChart(
6860
7374
  lv,
@@ -6866,19 +7380,19 @@ function validateChartBindings(stack) {
6866
7380
  const pages = asArray31(stack.pages);
6867
7381
  for (let pi = 0; pi < pages.length; pi++) {
6868
7382
  const page = pages[pi];
6869
- if (!isRec17(page)) continue;
6870
- const pageName = strName14(page.name) ?? `#${pi}`;
7383
+ if (!isRec18(page)) continue;
7384
+ const pageName = strName15(page.name) ?? `#${pi}`;
6871
7385
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6872
- const props = isRec17(component.properties) ? component.properties : void 0;
6873
- if (!props || !strName14(props.dataset)) continue;
6874
- const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName14(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
6875
- const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
7386
+ const props = isRec18(component.properties) ? component.properties : void 0;
7387
+ if (!props || !strName15(props.dataset)) continue;
7388
+ const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName15(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
7389
+ const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
6876
7390
  check({
6877
- dataset: strName14(props.dataset),
7391
+ dataset: strName15(props.dataset),
6878
7392
  dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
6879
7393
  values: { names: strList3(props.values), path: `${path}.properties.values` },
6880
7394
  series: [...axisRefs, ...seriesRefs],
6881
- where: `page "${pageName}" \xB7 ${strName14(component.type) ?? "chart"}`,
7395
+ where: `page "${pageName}" \xB7 ${strName15(component.type) ?? "chart"}`,
6882
7396
  path: `${path}.properties`
6883
7397
  });
6884
7398
  }
@@ -6891,11 +7405,11 @@ import { createRequire as createRequire4 } from "module";
6891
7405
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
6892
7406
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
6893
7407
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
6894
- var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7408
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6895
7409
  function asArray32(v) {
6896
- if (Array.isArray(v)) return v.filter(isRec18);
6897
- if (isRec18(v)) {
6898
- return Object.entries(v).filter(([, def]) => isRec18(def)).map(([name, def]) => ({ name, ...def }));
7410
+ if (Array.isArray(v)) return v.filter(isRec19);
7411
+ if (isRec19(v)) {
7412
+ return Object.entries(v).filter(([, def]) => isRec19(def)).map(([name, def]) => ({ name, ...def }));
6899
7413
  }
6900
7414
  return [];
6901
7415
  }
@@ -6912,7 +7426,7 @@ function loadAjv() {
6912
7426
  `@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.`
6913
7427
  );
6914
7428
  }
6915
- const ctor = isRec18(mod) && "default" in mod ? mod.default : mod;
7429
+ const ctor = isRec19(mod) && "default" in mod ? mod.default : mod;
6916
7430
  cachedAjv = ctor;
6917
7431
  return ctor;
6918
7432
  }
@@ -6927,7 +7441,7 @@ function loadAddFormats() {
6927
7441
  `@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.`
6928
7442
  );
6929
7443
  }
6930
- const plugin = isRec18(mod) && "default" in mod ? mod.default : mod;
7444
+ const plugin = isRec19(mod) && "default" in mod ? mod.default : mod;
6931
7445
  cachedAddFormats = plugin;
6932
7446
  return plugin;
6933
7447
  }
@@ -6957,13 +7471,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
6957
7471
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
6958
7472
  for (const branch of ["then", "otherwise"]) {
6959
7473
  const nested = rule[branch];
6960
- if (isRec18(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
7474
+ if (isRec19(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6961
7475
  }
6962
7476
  return out;
6963
7477
  }
6964
7478
  function walkObjectValidationRules(stack) {
6965
7479
  const walked = [];
6966
- if (!isRec18(stack)) return walked;
7480
+ if (!isRec19(stack)) return walked;
6967
7481
  for (const obj of asArray32(stack.objects)) {
6968
7482
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6969
7483
  const validations = obj.validations;
@@ -6998,7 +7512,7 @@ function validateRuleCompilability(stack) {
6998
7512
  });
6999
7513
  }
7000
7514
  }
7001
- if (rule.type === "json_schema" && isRec18(rule.schema)) {
7515
+ if (rule.type === "json_schema" && isRec19(rule.schema)) {
7002
7516
  try {
7003
7517
  createRuntimeAjv().compile(rule.schema);
7004
7518
  } catch (err) {
@@ -7018,7 +7532,7 @@ function validateRuleCompilability(stack) {
7018
7532
 
7019
7533
  // src/validate-rule-schema-formats.ts
7020
7534
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7021
- var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7535
+ var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7022
7536
  var SUBSCHEMA_KEYS = [
7023
7537
  "additionalItems",
7024
7538
  "additionalProperties",
@@ -7042,7 +7556,7 @@ var SUBSCHEMA_MAP_KEYS = [
7042
7556
  var MAX_SCHEMA_WALK_DEPTH = 32;
7043
7557
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7044
7558
  function collectFormatUses(schema, pointer, out, depth) {
7045
- if (!isRec19(schema)) return;
7559
+ if (!isRec20(schema)) return;
7046
7560
  if (typeof schema.format === "string") {
7047
7561
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7048
7562
  }
@@ -7061,7 +7575,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7061
7575
  }
7062
7576
  for (const key of SUBSCHEMA_MAP_KEYS) {
7063
7577
  const value = schema[key];
7064
- if (!isRec19(value)) continue;
7578
+ if (!isRec20(value)) continue;
7065
7579
  for (const [name, entry] of Object.entries(value)) {
7066
7580
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7067
7581
  }
@@ -7069,13 +7583,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7069
7583
  const items = schema.items;
7070
7584
  if (Array.isArray(items)) {
7071
7585
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7072
- } else if (isRec19(items)) {
7586
+ } else if (isRec20(items)) {
7073
7587
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7074
7588
  }
7075
7589
  const dependencies = schema.dependencies;
7076
- if (isRec19(dependencies)) {
7590
+ if (isRec20(dependencies)) {
7077
7591
  for (const [name, entry] of Object.entries(dependencies)) {
7078
- if (!isRec19(entry)) continue;
7592
+ if (!isRec20(entry)) continue;
7079
7593
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7080
7594
  }
7081
7595
  }
@@ -7102,9 +7616,9 @@ function nearestRegisteredFormat(name, registered) {
7102
7616
  let best = null;
7103
7617
  let bestDistance = Number.POSITIVE_INFINITY;
7104
7618
  for (const candidate of [...registered].sort()) {
7105
- const distance7 = editDistance2(authored, candidate);
7106
- if (distance7 < bestDistance) {
7107
- bestDistance = distance7;
7619
+ const distance8 = editDistance2(authored, candidate);
7620
+ if (distance8 < bestDistance) {
7621
+ bestDistance = distance8;
7108
7622
  best = candidate;
7109
7623
  }
7110
7624
  }
@@ -7114,7 +7628,7 @@ function validateRuleSchemaFormats(stack) {
7114
7628
  const findings = [];
7115
7629
  const pending = [];
7116
7630
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7117
- if (rule.type !== "json_schema" || !isRec19(rule.schema)) continue;
7631
+ if (rule.type !== "json_schema" || !isRec20(rule.schema)) continue;
7118
7632
  const uses = [];
7119
7633
  collectFormatUses(rule.schema, "", uses, 0);
7120
7634
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7239,7 +7753,7 @@ function asArray34(v) {
7239
7753
  }
7240
7754
  return [];
7241
7755
  }
7242
- function strName15(v) {
7756
+ function strName16(v) {
7243
7757
  return typeof v === "string" && v.length > 0 ? v : void 0;
7244
7758
  }
7245
7759
  function collectNavExposures(stack) {
@@ -7248,18 +7762,18 @@ function collectNavExposures(stack) {
7248
7762
  for (let ai = 0; ai < apps.length; ai++) {
7249
7763
  const app = apps[ai];
7250
7764
  if (!app || typeof app !== "object") continue;
7251
- const appName = strName15(app.name) ?? `#${ai}`;
7765
+ const appName = strName16(app.name) ?? `#${ai}`;
7252
7766
  const walk = (items, basePath) => {
7253
7767
  const navItems = asArray34(items);
7254
7768
  for (let ni = 0; ni < navItems.length; ni++) {
7255
7769
  const nav = navItems[ni];
7256
7770
  if (!nav || typeof nav !== "object") continue;
7257
7771
  const navPath = `${basePath}[${ni}]`;
7258
- const objectName = strName15(nav.objectName);
7772
+ const objectName = strName16(nav.objectName);
7259
7773
  if (nav.type === "object" && objectName) {
7260
7774
  out.push({
7261
7775
  objectName,
7262
- where: `app "${appName}" \xB7 nav "${strName15(nav.id) ?? `#${ni}`}"`,
7776
+ where: `app "${appName}" \xB7 nav "${strName16(nav.id) ?? `#${ni}`}"`,
7263
7777
  path: `${navPath}.objectName`
7264
7778
  });
7265
7779
  }
@@ -7283,7 +7797,7 @@ function validateNavAccess(stack) {
7283
7797
  if (exposures.length === 0) return findings;
7284
7798
  const ownObjects = /* @__PURE__ */ new Set();
7285
7799
  for (const obj of asArray34(stack.objects)) {
7286
- const n = strName15(obj.name);
7800
+ const n = strName16(obj.name);
7287
7801
  if (n) ownObjects.add(n);
7288
7802
  }
7289
7803
  const readable = /* @__PURE__ */ new Set();
@@ -7316,18 +7830,18 @@ import { expandViewContainer } from "@objectstack/spec";
7316
7830
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
7317
7831
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
7318
7832
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
7319
- function isRec20(v) {
7833
+ function isRec21(v) {
7320
7834
  return !!v && typeof v === "object" && !Array.isArray(v);
7321
7835
  }
7322
7836
  function asArray35(v) {
7323
7837
  if (Array.isArray(v)) return v;
7324
- if (isRec20(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec20(def) ? def : {} }));
7838
+ if (isRec21(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec21(def) ? def : {} }));
7325
7839
  return [];
7326
7840
  }
7327
- function strName16(v) {
7841
+ function strName17(v) {
7328
7842
  return typeof v === "string" && v.length > 0 ? v : void 0;
7329
7843
  }
7330
- function distance5(a, b) {
7844
+ function distance6(a, b) {
7331
7845
  const m = a.length;
7332
7846
  const n = b.length;
7333
7847
  if (m === 0) return n;
@@ -7343,7 +7857,7 @@ function distance5(a, b) {
7343
7857
  }
7344
7858
  return prev[n];
7345
7859
  }
7346
- function suggest6(target, known) {
7860
+ function suggest7(target, known) {
7347
7861
  const names = [...known];
7348
7862
  const segmentMatch = names.find(
7349
7863
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -7352,7 +7866,7 @@ function suggest6(target, known) {
7352
7866
  let best;
7353
7867
  let bestScore = Infinity;
7354
7868
  for (const candidate of names) {
7355
- const d = distance5(target, candidate);
7869
+ const d = distance6(target, candidate);
7356
7870
  if (d < bestScore) {
7357
7871
  bestScore = d;
7358
7872
  best = candidate;
@@ -7385,33 +7899,33 @@ function collectViewRecord(view, factsFor) {
7385
7899
  const addSections = (container, binding) => {
7386
7900
  if (!binding) return;
7387
7901
  for (const section of asArray35(container.sections)) {
7388
- const sectionName = strName16(section.name);
7902
+ const sectionName = strName17(section.name);
7389
7903
  if (sectionName) factsFor(binding).sections.add(sectionName);
7390
7904
  }
7391
7905
  };
7392
- const listBinding = isRec20(view.list) ? bindingOf(view.list) : void 0;
7393
- if (isRec20(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
7394
- addView(recordObject ?? listBinding, strName16(view.name));
7906
+ const listBinding = isRec21(view.list) ? bindingOf(view.list) : void 0;
7907
+ if (isRec21(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
7908
+ addView(recordObject ?? listBinding, strName17(view.name));
7395
7909
  const named = namedViewKeys(view);
7396
7910
  for (const family of ["listViews", "formViews"]) {
7397
7911
  const container = view[family];
7398
- if (!isRec20(container)) continue;
7912
+ if (!isRec21(container)) continue;
7399
7913
  const registryKeys = family === "listViews" ? named.list : named.form;
7400
7914
  let at = 0;
7401
7915
  for (const sub of Object.values(container)) {
7402
7916
  if (!sub || typeof sub !== "object") continue;
7403
7917
  const registryKey = registryKeys[at++];
7404
- if (!isRec20(sub)) continue;
7918
+ if (!isRec21(sub)) continue;
7405
7919
  const binding = bindingOf(sub) ?? listBinding;
7406
7920
  addView(binding, registryKey);
7407
7921
  addSections(sub, binding);
7408
7922
  }
7409
7923
  }
7410
- if (isRec20(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
7924
+ if (isRec21(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
7411
7925
  addSections(view, recordObject ?? listBinding);
7412
7926
  }
7413
7927
  function defaultListViewKey(object, container) {
7414
- if (!object || !isRec20(container.list)) return void 0;
7928
+ if (!object || !isRec21(container.list)) return void 0;
7415
7929
  const item = expandViewContainer(object, container).find(
7416
7930
  (i) => i.viewKind === "list" && i.isDefault
7417
7931
  );
@@ -7423,7 +7937,7 @@ function namedViewKeys(container) {
7423
7937
  const object = "probe";
7424
7938
  const prefix = `${object}.`;
7425
7939
  const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
7426
- const countEntries = (v) => isRec20(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
7940
+ const countEntries = (v) => isRec21(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
7427
7941
  const listCount = countEntries(container.listViews);
7428
7942
  const formCount = countEntries(container.formViews);
7429
7943
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -7441,14 +7955,14 @@ function readOptions(field) {
7441
7955
  values.add(opt);
7442
7956
  continue;
7443
7957
  }
7444
- if (!isRec20(opt)) continue;
7445
- const value = strName16(opt.value);
7958
+ if (!isRec21(opt)) continue;
7959
+ const value = strName17(opt.value);
7446
7960
  if (!value) continue;
7447
7961
  values.add(value);
7448
- const label2 = strName16(opt.label);
7962
+ const label2 = strName17(opt.label);
7449
7963
  if (label2) byLabel.set(label2.toLowerCase(), value);
7450
7964
  }
7451
- } else if (isRec20(raw)) {
7965
+ } else if (isRec21(raw)) {
7452
7966
  for (const [value, label2] of Object.entries(raw)) {
7453
7967
  values.add(value);
7454
7968
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -7469,23 +7983,23 @@ function buildUniverse(stack) {
7469
7983
  return facts;
7470
7984
  };
7471
7985
  for (const obj of asArray35(stack.objects)) {
7472
- const objectName = strName16(obj.name);
7986
+ const objectName = strName17(obj.name);
7473
7987
  if (!objectName) continue;
7474
7988
  const facts = factsFor(objectName);
7475
7989
  for (const field of asArray35(obj.fields)) {
7476
- const fieldName = strName16(field.name);
7990
+ const fieldName = strName17(field.name);
7477
7991
  if (fieldName) facts.fields.set(fieldName, field);
7478
7992
  }
7479
7993
  for (const action of asArray35(obj.actions)) {
7480
- const actionName = strName16(action.name);
7994
+ const actionName = strName17(action.name);
7481
7995
  if (actionName) facts.actions.set(actionName, action);
7482
7996
  }
7483
7997
  for (const view of asArray35(obj.views)) {
7484
- collectViewRecord({ ...view, object: strName16(view.object) ?? objectName }, factsFor);
7998
+ collectViewRecord({ ...view, object: strName17(view.object) ?? objectName }, factsFor);
7485
7999
  }
7486
8000
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
7487
8001
  for (const group of asArray35(obj.fieldGroups)) {
7488
- const key = strName16(group.key) ?? strName16(group.name);
8002
+ const key = strName17(group.key) ?? strName17(group.name);
7489
8003
  if (key) facts.sections.add(key);
7490
8004
  }
7491
8005
  }
@@ -7496,10 +8010,10 @@ function buildUniverse(stack) {
7496
8010
  for (let pi = 0; pi < pages.length; pi++) {
7497
8011
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
7498
8012
  if (!walked.objectName) continue;
7499
- const props = isRec20(walked.component.properties) ? walked.component.properties : void 0;
8013
+ const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
7500
8014
  if (!props) continue;
7501
8015
  for (const section of asArray35(props.sections)) {
7502
- const sectionName = strName16(section.name);
8016
+ const sectionName = strName17(section.name);
7503
8017
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
7504
8018
  }
7505
8019
  }
@@ -7507,9 +8021,9 @@ function buildUniverse(stack) {
7507
8021
  const globalActions = /* @__PURE__ */ new Map();
7508
8022
  const actionOwners = /* @__PURE__ */ new Map();
7509
8023
  for (const action of asArray35(stack.actions)) {
7510
- const actionName = strName16(action.name);
8024
+ const actionName = strName17(action.name);
7511
8025
  if (!actionName) continue;
7512
- const owner = strName16(action.objectName) ?? strName16(action.object);
8026
+ const owner = strName17(action.objectName) ?? strName17(action.object);
7513
8027
  if (owner) {
7514
8028
  factsFor(owner).actions.set(actionName, action);
7515
8029
  actionOwners.set(actionName, owner);
@@ -7524,19 +8038,19 @@ function buildUniverse(stack) {
7524
8038
  }
7525
8039
  const apps = /* @__PURE__ */ new Map();
7526
8040
  for (const app of asArray35(stack.apps)) {
7527
- const appName = strName16(app.name);
8041
+ const appName = strName17(app.name);
7528
8042
  if (!appName) continue;
7529
8043
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
7530
8044
  const walkNav = (items) => {
7531
8045
  for (const item of asArray35(items)) {
7532
- const id = strName16(item.id);
8046
+ const id = strName17(item.id);
7533
8047
  if (id) navIds.add(id);
7534
8048
  if (item.children) walkNav(item.children);
7535
8049
  }
7536
8050
  };
7537
8051
  walkNav(app.navigation);
7538
8052
  for (const area of asArray35(app.areas)) {
7539
- const areaId = strName16(area.id);
8053
+ const areaId = strName17(area.id);
7540
8054
  if (areaId) navIds.add(areaId);
7541
8055
  walkNav(area.navigation);
7542
8056
  }
@@ -7544,20 +8058,20 @@ function buildUniverse(stack) {
7544
8058
  }
7545
8059
  const dashboards = /* @__PURE__ */ new Map();
7546
8060
  for (const dash of asArray35(stack.dashboards)) {
7547
- const dashName = strName16(dash.name);
8061
+ const dashName = strName17(dash.name);
7548
8062
  if (!dashName) continue;
7549
8063
  const widgets = /* @__PURE__ */ new Set();
7550
8064
  for (const widget of asArray35(dash.widgets)) {
7551
- const id = strName16(widget.id) ?? strName16(widget.name);
8065
+ const id = strName17(widget.id) ?? strName17(widget.name);
7552
8066
  if (id) widgets.add(id);
7553
8067
  }
7554
8068
  const actions = /* @__PURE__ */ new Set();
7555
8069
  const headerActions = [
7556
- ...asArray35(isRec20(dash.header) ? dash.header.actions : void 0),
8070
+ ...asArray35(isRec21(dash.header) ? dash.header.actions : void 0),
7557
8071
  ...asArray35(dash.actions)
7558
8072
  ];
7559
8073
  for (const action of headerActions) {
7560
- const key = strName16(action.actionUrl) ?? strName16(action.url) ?? strName16(action.name);
8074
+ const key = strName17(action.actionUrl) ?? strName17(action.url) ?? strName17(action.name);
7561
8075
  if (key) actions.add(key);
7562
8076
  }
7563
8077
  dashboards.set(dashName, { widgets, actions });
@@ -7569,7 +8083,7 @@ function localePath(bundleIndex, locale) {
7569
8083
  }
7570
8084
  function validateTranslationReferences(stack) {
7571
8085
  const findings = [];
7572
- if (!isRec20(stack)) return findings;
8086
+ if (!isRec21(stack)) return findings;
7573
8087
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
7574
8088
  if (bundles.length === 0) return findings;
7575
8089
  const universe = buildUniverse(stack);
@@ -7578,13 +8092,13 @@ function validateTranslationReferences(stack) {
7578
8092
  };
7579
8093
  for (let bi = 0; bi < bundles.length; bi++) {
7580
8094
  const bundle = bundles[bi];
7581
- if (!isRec20(bundle)) continue;
8095
+ if (!isRec21(bundle)) continue;
7582
8096
  for (const [locale, rawData] of Object.entries(bundle)) {
7583
- if (!isRec20(rawData)) continue;
8097
+ if (!isRec21(rawData)) continue;
7584
8098
  const base = localePath(bi, locale);
7585
8099
  const inLocale = `locale "${locale}"`;
7586
8100
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
7587
- if (!isRec20(rawNode)) continue;
8101
+ if (!isRec21(rawNode)) continue;
7588
8102
  const objPath = `${base}.objects.${objectName}`;
7589
8103
  const facts = universe.objects.get(objectName);
7590
8104
  if (!facts) {
@@ -7592,7 +8106,7 @@ function validateTranslationReferences(stack) {
7592
8106
  orphan(
7593
8107
  `${inLocale} \xB7 object "${objectName}"`,
7594
8108
  objPath,
7595
- 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()),
8109
+ 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()),
7596
8110
  `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())}.` : "")
7597
8111
  );
7598
8112
  continue;
@@ -7605,12 +8119,12 @@ function validateTranslationReferences(stack) {
7605
8119
  orphan(
7606
8120
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
7607
8121
  fieldPath,
7608
- `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()),
8122
+ `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()),
7609
8123
  `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())}.` : "")
7610
8124
  );
7611
8125
  continue;
7612
8126
  }
7613
- if (!isRec20(rawField)) continue;
8127
+ if (!isRec21(rawField)) continue;
7614
8128
  checkOptionKeys(findings, {
7615
8129
  optionMap: rawField.options,
7616
8130
  field,
@@ -7625,7 +8139,7 @@ function validateTranslationReferences(stack) {
7625
8139
  orphan(
7626
8140
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
7627
8141
  `${objPath}._views.${viewName}`,
7628
- `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),
8142
+ `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),
7629
8143
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
7630
8144
  );
7631
8145
  }
@@ -7634,7 +8148,7 @@ function validateTranslationReferences(stack) {
7634
8148
  orphan(
7635
8149
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
7636
8150
  `${objPath}._sections.${sectionName}`,
7637
- `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),
8151
+ `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),
7638
8152
  `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.`)
7639
8153
  );
7640
8154
  }
@@ -7645,7 +8159,7 @@ function validateTranslationReferences(stack) {
7645
8159
  orphan(
7646
8160
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
7647
8161
  actionPath,
7648
- `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()),
8162
+ `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()),
7649
8163
  `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())}.` : "")
7650
8164
  );
7651
8165
  continue;
@@ -7667,7 +8181,7 @@ function validateTranslationReferences(stack) {
7667
8181
  orphan(
7668
8182
  `${inLocale} \xB7 global action "${actionName}"`,
7669
8183
  actionPath,
7670
- 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()),
8184
+ 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()),
7671
8185
  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())}.` : "")
7672
8186
  );
7673
8187
  continue;
@@ -7687,18 +8201,18 @@ function validateTranslationReferences(stack) {
7687
8201
  orphan(
7688
8202
  `${inLocale} \xB7 app "${appName}"`,
7689
8203
  appPath,
7690
- `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()),
8204
+ `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()),
7691
8205
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
7692
8206
  );
7693
8207
  continue;
7694
8208
  }
7695
- if (!isRec20(rawApp)) continue;
8209
+ if (!isRec21(rawApp)) continue;
7696
8210
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
7697
8211
  if (navIds.has(navId)) continue;
7698
8212
  orphan(
7699
8213
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
7700
8214
  `${appPath}.navigation.${navId}`,
7701
- `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
8215
+ `Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
7702
8216
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
7703
8217
  );
7704
8218
  }
@@ -7710,18 +8224,18 @@ function validateTranslationReferences(stack) {
7710
8224
  orphan(
7711
8225
  `${inLocale} \xB7 dashboard "${dashName}"`,
7712
8226
  dashPath,
7713
- `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()),
8227
+ `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()),
7714
8228
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
7715
8229
  );
7716
8230
  continue;
7717
8231
  }
7718
- if (!isRec20(rawDash)) continue;
8232
+ if (!isRec21(rawDash)) continue;
7719
8233
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
7720
8234
  if (dash.widgets.has(widgetId)) continue;
7721
8235
  orphan(
7722
8236
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
7723
8237
  `${dashPath}.widgets.${widgetId}`,
7724
- `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
8238
+ `Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
7725
8239
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
7726
8240
  );
7727
8241
  }
@@ -7730,7 +8244,7 @@ function validateTranslationReferences(stack) {
7730
8244
  orphan(
7731
8245
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
7732
8246
  `${dashPath}.actions.${actionKey}`,
7733
- `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
8247
+ `Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
7734
8248
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
7735
8249
  );
7736
8250
  }
@@ -7740,7 +8254,7 @@ function validateTranslationReferences(stack) {
7740
8254
  return findings;
7741
8255
  }
7742
8256
  function asRecord(v) {
7743
- return isRec20(v) ? v : {};
8257
+ return isRec21(v) ? v : {};
7744
8258
  }
7745
8259
  function checkOptionKeys(findings, ctx) {
7746
8260
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -7752,7 +8266,7 @@ function checkOptionKeys(findings, ctx) {
7752
8266
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
7753
8267
  where: ctx.where,
7754
8268
  path: ctx.path,
7755
- message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName16(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
8269
+ message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName17(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
7756
8270
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
7757
8271
  });
7758
8272
  return;
@@ -7765,17 +8279,17 @@ function checkOptionKeys(findings, ctx) {
7765
8279
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
7766
8280
  where: ctx.where,
7767
8281
  path: `${ctx.path}.${key}`,
7768
- 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),
8282
+ 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),
7769
8283
  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)}.`
7770
8284
  });
7771
8285
  }
7772
8286
  }
7773
8287
  function checkActionParams(findings, ctx) {
7774
- const rawParams = Object.keys(asRecord(isRec20(ctx.rawAction) ? ctx.rawAction.params : void 0));
8288
+ const rawParams = Object.keys(asRecord(isRec21(ctx.rawAction) ? ctx.rawAction.params : void 0));
7775
8289
  if (rawParams.length === 0) return;
7776
8290
  const declared = /* @__PURE__ */ new Set();
7777
8291
  for (const param of asArray35(ctx.action.params)) {
7778
- const name = strName16(param.name) ?? strName16(param.field);
8292
+ const name = strName17(param.name) ?? strName17(param.field);
7779
8293
  if (name) declared.add(name);
7780
8294
  }
7781
8295
  for (const paramName of rawParams) {
@@ -7785,7 +8299,7 @@ function checkActionParams(findings, ctx) {
7785
8299
  rule: TRANSLATION_TARGET_UNKNOWN,
7786
8300
  where: `${ctx.where} \xB7 param "${paramName}"`,
7787
8301
  path: `${ctx.path}.params.${paramName}`,
7788
- 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),
8302
+ 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),
7789
8303
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
7790
8304
  });
7791
8305
  }
@@ -7793,14 +8307,14 @@ function checkActionParams(findings, ctx) {
7793
8307
 
7794
8308
  // src/validate-translatable-sections.ts
7795
8309
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
7796
- function isRec21(v) {
8310
+ function isRec22(v) {
7797
8311
  return !!v && typeof v === "object" && !Array.isArray(v);
7798
8312
  }
7799
- function strName17(v) {
8313
+ function strName18(v) {
7800
8314
  return typeof v === "string" && v.length > 0 ? v : void 0;
7801
8315
  }
7802
8316
  function viewLabel(view) {
7803
- const name = strName17(view.name);
8317
+ const name = strName18(view.name);
7804
8318
  return name ? `view "${name}"` : "";
7805
8319
  }
7806
8320
  function joinWhere(...parts) {
@@ -7808,7 +8322,7 @@ function joinWhere(...parts) {
7808
8322
  }
7809
8323
  function collectViewSites(view, basePath, label2, sites) {
7810
8324
  const recordObject = viewObjectName(view);
7811
- const listBinding = isRec21(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
8325
+ const listBinding = isRec22(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
7812
8326
  for (const site of viewContainerSites(view, basePath)) {
7813
8327
  sites.push({
7814
8328
  path: `${site.path}.sections`,
@@ -7822,11 +8336,11 @@ function translatedObjectNames(stack) {
7822
8336
  const out = /* @__PURE__ */ new Set();
7823
8337
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
7824
8338
  for (const bundle of bundles) {
7825
- if (!isRec21(bundle)) continue;
8339
+ if (!isRec22(bundle)) continue;
7826
8340
  for (const data of Object.values(bundle)) {
7827
- if (!isRec21(data) || !isRec21(data.objects)) continue;
8341
+ if (!isRec22(data) || !isRec22(data.objects)) continue;
7828
8342
  for (const [objectName, node] of Object.entries(data.objects)) {
7829
- if (isRec21(node)) out.add(objectName);
8343
+ if (isRec22(node)) out.add(objectName);
7830
8344
  }
7831
8345
  }
7832
8346
  }
@@ -7838,22 +8352,22 @@ function suggestedName(label2) {
7838
8352
  }
7839
8353
  function validateTranslatableSections(stack) {
7840
8354
  const findings = [];
7841
- if (!isRec21(stack)) return findings;
8355
+ if (!isRec22(stack)) return findings;
7842
8356
  const translated = translatedObjectNames(stack);
7843
8357
  if (translated.size === 0) return findings;
7844
8358
  const sites = [];
7845
8359
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
7846
- const objectName = strName17(obj.name);
8360
+ const objectName = strName18(obj.name);
7847
8361
  if (!objectName) continue;
7848
8362
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
7849
8363
  collectViewSites(
7850
- { ...view, object: strName17(view.object) ?? objectName },
8364
+ { ...view, object: strName18(view.object) ?? objectName },
7851
8365
  path,
7852
8366
  viewLabel(view),
7853
8367
  sites
7854
8368
  );
7855
8369
  }
7856
- if (isRec21(obj.listViews)) {
8370
+ if (isRec22(obj.listViews)) {
7857
8371
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
7858
8372
  }
7859
8373
  }
@@ -7861,13 +8375,13 @@ function validateTranslatableSections(stack) {
7861
8375
  collectViewSites(view, path, viewLabel(view), sites);
7862
8376
  }
7863
8377
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
7864
- const pageName = strName17(page.name);
8378
+ const pageName = strName18(page.name);
7865
8379
  const pageLabel = pageName ? `page "${pageName}"` : "";
7866
8380
  for (const walked of walkPageComponents(page, pagePath)) {
7867
8381
  if (!walked.objectName) continue;
7868
- const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
8382
+ const props = isRec22(walked.component.properties) ? walked.component.properties : void 0;
7869
8383
  if (!props) continue;
7870
- const type = strName17(walked.component.type) ?? "component";
8384
+ const type = strName18(walked.component.type) ?? "component";
7871
8385
  sites.push({
7872
8386
  path: `${walked.path}.properties.sections`,
7873
8387
  surface: joinWhere(pageLabel, type),
@@ -7882,9 +8396,9 @@ function validateTranslatableSections(stack) {
7882
8396
  if (!Array.isArray(site.sections)) continue;
7883
8397
  for (let i = 0; i < site.sections.length; i++) {
7884
8398
  const section = site.sections[i];
7885
- if (!isRec21(section)) continue;
7886
- if (strName17(section.name)) continue;
7887
- const heading = strName17(section.label);
8399
+ if (!isRec22(section)) continue;
8400
+ if (strName18(section.name)) continue;
8401
+ const heading = strName18(section.label);
7888
8402
  if (!heading) continue;
7889
8403
  const slug = suggestedName(heading);
7890
8404
  findings.push({
@@ -7909,7 +8423,7 @@ function asArray36(v) {
7909
8423
  }
7910
8424
  return [];
7911
8425
  }
7912
- function strName18(v) {
8426
+ function strName19(v) {
7913
8427
  return typeof v === "string" && v.length > 0 ? v : void 0;
7914
8428
  }
7915
8429
  function surfaceOf(v) {
@@ -7920,17 +8434,17 @@ function validateAiSurfaceAffinity(stack) {
7920
8434
  if (!stack || typeof stack !== "object") return findings;
7921
8435
  const skillsByName = /* @__PURE__ */ new Map();
7922
8436
  for (const skill of asArray36(stack.skills)) {
7923
- const n = strName18(skill.name);
8437
+ const n = strName19(skill.name);
7924
8438
  if (n) skillsByName.set(n, skill);
7925
8439
  }
7926
8440
  const agents = asArray36(stack.agents);
7927
8441
  for (let ai = 0; ai < agents.length; ai++) {
7928
8442
  const agent = agents[ai];
7929
- const agentName = strName18(agent.name) ?? `#${ai}`;
8443
+ const agentName = strName19(agent.name) ?? `#${ai}`;
7930
8444
  const agentSurface = surfaceOf(agent.surface);
7931
8445
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
7932
8446
  for (let si = 0; si < skillRefs.length; si++) {
7933
- const ref = strName18(skillRefs[si]);
8447
+ const ref = strName19(skillRefs[si]);
7934
8448
  if (!ref) continue;
7935
8449
  const skill = skillsByName.get(ref);
7936
8450
  if (!skill) continue;
@@ -7959,10 +8473,10 @@ function asArray37(v) {
7959
8473
  }
7960
8474
  return [];
7961
8475
  }
7962
- function strName19(v) {
8476
+ function strName20(v) {
7963
8477
  return typeof v === "string" && v.length > 0 ? v : void 0;
7964
8478
  }
7965
- function distance6(a, b) {
8479
+ function distance7(a, b) {
7966
8480
  const m = a.length;
7967
8481
  const n = b.length;
7968
8482
  if (m === 0) return n;
@@ -7978,14 +8492,14 @@ function distance6(a, b) {
7978
8492
  }
7979
8493
  return prev[n];
7980
8494
  }
7981
- function suggest7(target, known) {
8495
+ function suggest8(target, known) {
7982
8496
  for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
7983
8497
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
7984
8498
  }
7985
8499
  let best;
7986
8500
  let bestScore = Infinity;
7987
8501
  for (const candidate of known) {
7988
- const d = distance6(target, candidate);
8502
+ const d = distance7(target, candidate);
7989
8503
  if (d < bestScore) {
7990
8504
  bestScore = d;
7991
8505
  best = candidate;
@@ -8000,8 +8514,8 @@ function materialisesAsTool(action) {
8000
8514
  if (!ai || typeof ai !== "object") return false;
8001
8515
  const aiRec = ai;
8002
8516
  if (aiRec.exposed !== true) return false;
8003
- if (!strName19(aiRec.description)) return false;
8004
- const type = strName19(action.type);
8517
+ if (!strName20(aiRec.description)) return false;
8518
+ const type = strName20(action.type);
8005
8519
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
8006
8520
  if (type === "script") return Boolean(action.target || action.body);
8007
8521
  return Boolean(action.target);
@@ -8009,12 +8523,12 @@ function materialisesAsTool(action) {
8009
8523
  function collectToolUniverse(stack) {
8010
8524
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
8011
8525
  for (const tool of asArray37(stack.tools)) {
8012
- const n = strName19(tool.name);
8526
+ const n = strName20(tool.name);
8013
8527
  if (n) universe.add(n);
8014
8528
  }
8015
8529
  const addActionFamily = (actions) => {
8016
8530
  for (const action of asArray37(actions)) {
8017
- const n = strName19(action.name);
8531
+ const n = strName20(action.name);
8018
8532
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
8019
8533
  }
8020
8534
  };
@@ -8028,7 +8542,7 @@ function collectUnexposedActionNames(stack) {
8028
8542
  const names = /* @__PURE__ */ new Set();
8029
8543
  const scan = (actions) => {
8030
8544
  for (const action of asArray37(actions)) {
8031
- const n = strName19(action.name);
8545
+ const n = strName20(action.name);
8032
8546
  if (n && !materialisesAsTool(action)) names.add(n);
8033
8547
  }
8034
8548
  };
@@ -8054,10 +8568,10 @@ function validateAiToolReferences(stack) {
8054
8568
  const skills = asArray37(stack.skills);
8055
8569
  for (let si = 0; si < skills.length; si++) {
8056
8570
  const skill = skills[si];
8057
- const skillName = strName19(skill.name) ?? `#${si}`;
8571
+ const skillName = strName20(skill.name) ?? `#${si}`;
8058
8572
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
8059
8573
  for (let ti = 0; ti < refs.length; ti++) {
8060
- const ref = strName19(refs[ti]);
8574
+ const ref = strName20(refs[ti]);
8061
8575
  if (!ref || resolves(ref)) continue;
8062
8576
  const isPattern = ref.endsWith("*");
8063
8577
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -8066,7 +8580,7 @@ function validateAiToolReferences(stack) {
8066
8580
  rule: AI_SKILL_TOOL_UNRESOLVED,
8067
8581
  where: `skill "${skillName}" \xB7 tools`,
8068
8582
  path: `skills[${si}].tools[${ti}]`,
8069
- 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),
8583
+ 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),
8070
8584
  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(", ")}.`
8071
8585
  });
8072
8586
  }
@@ -8084,7 +8598,7 @@ function asArray38(v) {
8084
8598
  }
8085
8599
  return [];
8086
8600
  }
8087
- function strName20(v) {
8601
+ function strName21(v) {
8088
8602
  return typeof v === "string" && v.length > 0 ? v : void 0;
8089
8603
  }
8090
8604
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -8094,7 +8608,7 @@ function validateAiAgentAuthoring(stack) {
8094
8608
  const agents = asArray38(stack.agents);
8095
8609
  for (let ai = 0; ai < agents.length; ai++) {
8096
8610
  const agent = agents[ai];
8097
- const name = strName20(agent.name) ?? `#${ai}`;
8611
+ const name = strName21(agent.name) ?? `#${ai}`;
8098
8612
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
8099
8613
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
8100
8614
  findings.push({
@@ -8110,9 +8624,9 @@ function validateAiAgentAuthoring(stack) {
8110
8624
  const apps = asArray38(stack.apps);
8111
8625
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
8112
8626
  const app = apps[appIdx];
8113
- const defaultAgent = strName20(app.defaultAgent);
8627
+ const defaultAgent = strName21(app.defaultAgent);
8114
8628
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
8115
- const appName = strName20(app.name) ?? `#${appIdx}`;
8629
+ const appName = strName21(app.name) ?? `#${appIdx}`;
8116
8630
  findings.push({
8117
8631
  severity: "warning",
8118
8632
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -8142,6 +8656,8 @@ function loadTypeScript3() {
8142
8656
  return cachedTs3;
8143
8657
  }
8144
8658
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
8659
+ var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
8660
+ var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
8145
8661
  var HOOK_BODY_WRITE_PATTERNS = [
8146
8662
  {
8147
8663
  id: "input-property-assign",
@@ -8214,13 +8730,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
8214
8730
  "owner",
8215
8731
  "record_type"
8216
8732
  ]);
8217
- var isRec22 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8733
+ function unprovisionedAnchorWriteConsequence() {
8734
+ 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).`;
8735
+ }
8736
+ var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8218
8737
  function asArray39(v) {
8219
- if (Array.isArray(v)) return v.filter((x) => isRec22(x));
8220
- if (isRec22(v)) {
8738
+ if (Array.isArray(v)) return v.filter((x) => isRec23(x));
8739
+ if (isRec23(v)) {
8221
8740
  return Object.entries(v).map(([name, def]) => ({
8222
8741
  name,
8223
- ...isRec22(def) ? def : {}
8742
+ ...isRec23(def) ? def : {}
8224
8743
  }));
8225
8744
  }
8226
8745
  return [];
@@ -8251,15 +8770,18 @@ function extractHookBodyWriteSet(source) {
8251
8770
  return { writes: [], ctxRecordEscapes: false };
8252
8771
  }
8253
8772
  const tsc = loadTypeScript3();
8254
- const sf = tsc.createSourceFile(
8773
+ const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
8774
+ tsc,
8255
8775
  "hook-body.ts",
8256
8776
  `async function __body(ctx) {
8257
8777
  ${source}
8258
8778
  }`,
8259
- tsc.ScriptTarget.Latest,
8260
- /* setParentNodes */
8261
- false,
8262
- tsc.ScriptKind.TS
8779
+ {
8780
+ target: tsc.ScriptTarget.Latest,
8781
+ setParentNodes: false,
8782
+ scriptKind: tsc.ScriptKind.TS,
8783
+ synthesizedLinesBefore: 1
8784
+ }
8263
8785
  );
8264
8786
  const writes = [];
8265
8787
  const recordRefs = [];
@@ -8356,7 +8878,8 @@ ${source}
8356
8878
  visit(sf);
8357
8879
  return {
8358
8880
  writes,
8359
- ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
8881
+ ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
8882
+ ...parseFailure ? { parseFailure } : {}
8360
8883
  };
8361
8884
  }
8362
8885
  function validateHookBodyWrites(stack) {
@@ -8364,15 +8887,28 @@ function validateHookBodyWrites(stack) {
8364
8887
  const hooks = asArray39(stack.hooks);
8365
8888
  if (hooks.length === 0) return findings;
8366
8889
  let objectFields = null;
8890
+ let anchors = null;
8367
8891
  hooks.forEach((hook, hookIndex) => {
8368
8892
  const body = hook.body;
8369
- if (!isRec22(body) || body.language !== "js") return;
8893
+ if (!isRec23(body) || body.language !== "js") return;
8370
8894
  const source = body.source;
8371
8895
  if (typeof source !== "string" || source.trim() === "") return;
8372
- const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
8896
+ const extracted = extractHookBodyWriteSet(source);
8897
+ const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
8898
+ if (extracted.parseFailure) {
8899
+ findings.push({
8900
+ severity: "warning",
8901
+ rule: HOOK_BODY_SOURCE_UNPARSEABLE,
8902
+ where: `hook "${hookName}" \u203A body`,
8903
+ path: `hooks[${hookIndex}].body.source`,
8904
+ message: `L2 body did not parse (${describeParseFailure(extracted.parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
8905
+ hint: PARSE_FAILURE_HINT
8906
+ });
8907
+ }
8908
+ const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
8373
8909
  if (writes.length === 0) return;
8374
8910
  objectFields ?? (objectFields = indexObjectFields2(stack));
8375
- const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
8911
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8376
8912
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
8377
8913
  (o) => typeof o === "string" && o.trim() !== ""
8378
8914
  );
@@ -8386,7 +8922,20 @@ function validateHookBodyWrites(stack) {
8386
8922
  if (reported.has(dedupeKey)) continue;
8387
8923
  if (w.object === void 0) {
8388
8924
  if (!inputJudgeable) continue;
8389
- if (IMPLICIT_FIELDS2.has(w.field)) continue;
8925
+ if (IMPLICIT_FIELDS2.has(w.field)) {
8926
+ if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
8927
+ reported.add(dedupeKey);
8928
+ const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
8929
+ findings.push({
8930
+ severity: "warning",
8931
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
8932
+ where,
8933
+ path,
8934
+ message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
8935
+ hint: unprovisionedAnchorHint(anchorObj, w.field)
8936
+ });
8937
+ continue;
8938
+ }
8390
8939
  if (targetSets.some((s) => s.has(w.field))) continue;
8391
8940
  reported.add(dedupeKey);
8392
8941
  const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
@@ -8402,7 +8951,20 @@ function validateHookBodyWrites(stack) {
8402
8951
  } else {
8403
8952
  const known = judgeableFieldsOf(objectFields, w.object);
8404
8953
  if (!known) continue;
8405
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
8954
+ if (known.has(w.field)) continue;
8955
+ if (IMPLICIT_FIELDS2.has(w.field)) {
8956
+ if (!anchors.get(w.object)?.has(w.field)) continue;
8957
+ reported.add(dedupeKey);
8958
+ findings.push({
8959
+ severity: "warning",
8960
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
8961
+ where,
8962
+ path,
8963
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
8964
+ hint: unprovisionedAnchorHint(w.object, w.field)
8965
+ });
8966
+ continue;
8967
+ }
8406
8968
  reported.add(dedupeKey);
8407
8969
  findings.push({
8408
8970
  severity: "warning",
@@ -8431,6 +8993,8 @@ function fixHint(field, declared) {
8431
8993
  import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
8432
8994
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
8433
8995
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
8996
+ var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
8997
+ var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
8434
8998
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
8435
8999
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
8436
9000
  var ACTION_BODY_WRITE_EXCLUSIONS = [
@@ -8447,13 +9011,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
8447
9011
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
8448
9012
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
8449
9013
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
8450
- var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9014
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8451
9015
  function asArray40(v) {
8452
- if (Array.isArray(v)) return v.filter((x) => isRec23(x));
8453
- if (isRec23(v)) {
9016
+ if (Array.isArray(v)) return v.filter((x) => isRec24(x));
9017
+ if (isRec24(v)) {
8454
9018
  return Object.entries(v).map(([name, def]) => ({
8455
9019
  name,
8456
- ...isRec23(def) ? def : {}
9020
+ ...isRec24(def) ? def : {}
8457
9021
  }));
8458
9022
  }
8459
9023
  return [];
@@ -8471,7 +9035,7 @@ function collectActionBodies(stack) {
8471
9035
  const type = typeof action.type === "string" ? action.type : "script";
8472
9036
  if (type !== "script") return;
8473
9037
  const body = action.body;
8474
- if (!isRec23(body) || body.language !== "js") return;
9038
+ if (!isRec24(body) || body.language !== "js") return;
8475
9039
  const source = body.source;
8476
9040
  if (typeof source !== "string" || source.trim() === "") return;
8477
9041
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -8490,17 +9054,28 @@ function collectActionBodies(stack) {
8490
9054
  }
8491
9055
  function validateActionBodyWrites(stack) {
8492
9056
  const findings = [];
8493
- if (!isRec23(stack)) return findings;
9057
+ if (!isRec24(stack)) return findings;
8494
9058
  const sites = collectActionBodies(stack);
8495
9059
  if (sites.length === 0) return findings;
8496
9060
  let objectFields = null;
9061
+ let anchors = null;
8497
9062
  for (const site of sites) {
8498
9063
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
8499
- const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
9064
+ const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
8500
9065
  const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
8501
9066
  const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
8502
- if (writes.length === 0 && recordWrites.length === 0) continue;
8503
9067
  const where = `action "${site.name}" \u203A body`;
9068
+ if (parseFailure) {
9069
+ findings.push({
9070
+ severity: "warning",
9071
+ rule: ACTION_BODY_SOURCE_UNPARSEABLE,
9072
+ where,
9073
+ path: site.path,
9074
+ message: `L2 body did not parse (${describeParseFailure(parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
9075
+ hint: PARSE_FAILURE_HINT
9076
+ });
9077
+ }
9078
+ if (writes.length === 0 && recordWrites.length === 0) continue;
8504
9079
  if (recordWrites.length > 0 && !ctxRecordEscapes) {
8505
9080
  const reportedFields = /* @__PURE__ */ new Set();
8506
9081
  for (const w of recordWrites) {
@@ -8518,6 +9093,7 @@ function validateActionBodyWrites(stack) {
8518
9093
  }
8519
9094
  if (writes.length === 0) continue;
8520
9095
  objectFields ?? (objectFields = indexObjectFields2(stack));
9096
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8521
9097
  const reported = /* @__PURE__ */ new Set();
8522
9098
  for (const w of writes) {
8523
9099
  if (w.object === void 0) continue;
@@ -8525,7 +9101,20 @@ function validateActionBodyWrites(stack) {
8525
9101
  if (reported.has(dedupeKey)) continue;
8526
9102
  const known = judgeableFieldsOf(objectFields, w.object);
8527
9103
  if (!known) continue;
8528
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
9104
+ if (known.has(w.field)) continue;
9105
+ if (IMPLICIT_FIELDS2.has(w.field)) {
9106
+ if (!anchors.get(w.object)?.has(w.field)) continue;
9107
+ reported.add(dedupeKey);
9108
+ findings.push({
9109
+ severity: "warning",
9110
+ rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
9111
+ where,
9112
+ path: site.path,
9113
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
9114
+ hint: unprovisionedAnchorHint(w.object, w.field)
9115
+ });
9116
+ continue;
9117
+ }
8529
9118
  reported.add(dedupeKey);
8530
9119
  findings.push({
8531
9120
  severity: "warning",
@@ -8547,15 +9136,16 @@ function fixHint2(field, declared) {
8547
9136
  // src/validate-flow-node-writes.ts
8548
9137
  import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec/shared";
8549
9138
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
9139
+ var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
8550
9140
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
8551
9141
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
8552
- var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9142
+ var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8553
9143
  function asArray41(v) {
8554
- if (Array.isArray(v)) return v.filter((x) => isRec24(x));
8555
- if (isRec24(v)) {
9144
+ if (Array.isArray(v)) return v.filter((x) => isRec25(x));
9145
+ if (isRec25(v)) {
8556
9146
  return Object.entries(v).map(([name, def]) => ({
8557
9147
  name,
8558
- ...isRec24(def) ? def : {}
9148
+ ...isRec25(def) ? def : {}
8559
9149
  }));
8560
9150
  }
8561
9151
  return [];
@@ -8568,30 +9158,44 @@ function readLiteralObjectName2(config) {
8568
9158
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
8569
9159
  function validateFlowNodeWrites(stack) {
8570
9160
  const findings = [];
8571
- if (!isRec24(stack)) return findings;
9161
+ if (!isRec25(stack)) return findings;
8572
9162
  const flows = asArray41(stack.flows);
8573
9163
  if (flows.length === 0) return findings;
8574
9164
  let objectFields = null;
9165
+ let anchors = null;
8575
9166
  flows.forEach((flow, flowIndex) => {
8576
9167
  const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
8577
9168
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
8578
9169
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
8579
9170
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
8580
- const config = isRec24(node.config) ? node.config : void 0;
9171
+ const config = isRec25(node.config) ? node.config : void 0;
8581
9172
  if (!config) return;
8582
9173
  const fields = config.fields;
8583
- if (!isRec24(fields)) return;
9174
+ if (!isRec25(fields)) return;
8584
9175
  const written = Object.keys(fields);
8585
9176
  if (written.length === 0) return;
8586
9177
  const objectName = readLiteralObjectName2(config);
8587
9178
  if (!objectName) return;
8588
9179
  objectFields ?? (objectFields = indexObjectFields2(stack));
9180
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8589
9181
  const known = judgeableFieldsOf(objectFields, objectName);
8590
9182
  if (!known) return;
8591
9183
  const nodeName = flowNodeLabel(node, walkIndex);
8592
9184
  const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
8593
9185
  for (const fieldName of written) {
8594
- if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
9186
+ if (known.has(fieldName)) continue;
9187
+ if (IMPLICIT_FIELDS2.has(fieldName)) {
9188
+ if (!anchors.get(objectName)?.has(fieldName)) continue;
9189
+ findings.push({
9190
+ severity: "warning",
9191
+ rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
9192
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
9193
+ path: `${nodePath}.config.fields.${fieldName}`,
9194
+ message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
9195
+ hint: unprovisionedAnchorHint(objectName, fieldName)
9196
+ });
9197
+ continue;
9198
+ }
8595
9199
  if (fieldName.includes(".")) continue;
8596
9200
  findings.push({
8597
9201
  severity: "error",
@@ -8612,9 +9216,26 @@ function fixHint3(field, declared) {
8612
9216
  }
8613
9217
 
8614
9218
  // src/reference-integrity-suite.ts
9219
+ var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
8615
9220
  var REFERENCE_INTEGRITY_RULES = [
8616
9221
  { name: "validateObjectReferences", run: validateObjectReferences },
8617
- { name: "validateSearchableFields", run: validateSearchableFields },
9222
+ // [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
9223
+ // both judge a LIST VIEW's field references, and a standalone list view is
9224
+ // written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
9225
+ // an MCP/AI author has. Their walks read the flattened overlay shape that
9226
+ // door carries (see each rule's `views[]` self rung), and they resolve only
9227
+ // against `stack.objects`, which the per-write snapshot DOES carry — so the
9228
+ // crossing has no missing-collection false-positive channel. Measured over
9229
+ // the shipped view corpus before crossing (0 refusals; population in the
9230
+ // #9313 PR).
9231
+ { name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
9232
+ // [#9257] The same reading, one axis over: a list view's `sort` is a field
9233
+ // name written in metadata, resolved against the object's declared fields. It
9234
+ // gates (`error`) because the runtime does not tolerate a bad one at all —
9235
+ // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
9236
+ // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
9237
+ // the refusal is the whole view, on every load, traced to nothing.
9238
+ { name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
8618
9239
  { name: "validateActionNameRefs", run: validateActionNameRefs },
8619
9240
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
8620
9241
  { name: "validateChartBindings", run: validateChartBindings },
@@ -8720,9 +9341,11 @@ var REFERENCE_INTEGRITY_RULES = [
8720
9341
  // — only a page that is actually `kind:'react'` loads the compiler.
8721
9342
  { name: "validateReactPageProps", run: validateReactPageProps }
8722
9343
  ];
8723
- function validateReferenceIntegrity(stack) {
9344
+ function validateReferenceIntegrity(stack, options) {
8724
9345
  const findings = [];
9346
+ const writeType = options?.runtimeWriteType;
8725
9347
  for (const rule of REFERENCE_INTEGRITY_RULES) {
9348
+ if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
8726
9349
  findings.push(...rule.run(stack));
8727
9350
  }
8728
9351
  return findings;
@@ -9557,6 +10180,17 @@ function lintViewRefs(stack) {
9557
10180
  }
9558
10181
 
9559
10182
  // src/data-model-rules.ts
10183
+ function objectWhere(obj) {
10184
+ return `object "${obj?.name}"`;
10185
+ }
10186
+ function indexWhere(obj, idx, j, cols) {
10187
+ const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
10188
+ const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
10189
+ return `${objectWhere(obj)} \xB7 index ${label2}`;
10190
+ }
10191
+ function fieldWhere(obj, fieldName) {
10192
+ return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
10193
+ }
9560
10194
  var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
9561
10195
  var NUMERIC_TYPES = /* @__PURE__ */ new Set([
9562
10196
  "number",
@@ -9629,11 +10263,13 @@ function lintUnscopedDeclaredIndexes(objects) {
9629
10263
  for (let j = 0; j < declaredIndexes.length; j++) {
9630
10264
  const idx = declaredIndexes[j];
9631
10265
  if (idx?.unique !== true) continue;
9632
- const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
10266
+ const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
10267
+ const cols = colList.join(", ");
9633
10268
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
9634
10269
  issues.push({
9635
10270
  severity: "warning",
9636
10271
  rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
10272
+ where: indexWhere(obj, idx, j, colList),
9637
10273
  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).`,
9638
10274
  path: `objects[${i}].indexes[${j}]`,
9639
10275
  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).`
@@ -9682,6 +10318,11 @@ function lintUniqueDeclarations(objects) {
9682
10318
  issues.push({
9683
10319
  severity: "warning",
9684
10320
  rule: UNIQUE_DOUBLE_DECLARATION,
10321
+ // More specific than `path` on purpose: this rule's finding is about ONE
10322
+ // column, but its `path` has always been the whole object (`objects[i]`)
10323
+ // because the defect straddles `fields.<name>.unique` and an entry of
10324
+ // `indexes`. `where` can name the column without picking one of the two.
10325
+ where: fieldWhere(obj, name),
9685
10326
  message,
9686
10327
  path: `objects[${i}]`,
9687
10328
  fix
@@ -9710,6 +10351,7 @@ function lintLegacyOrganizationComposites(objects) {
9710
10351
  issues.push({
9711
10352
  severity: "warning",
9712
10353
  rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
10354
+ where: indexWhere(obj, idx, j, cols),
9713
10355
  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.`,
9714
10356
  path: `objects[${i}].indexes[${j}]`,
9715
10357
  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.`
@@ -9856,7 +10498,7 @@ var CLI_ONLY = ["cli"];
9856
10498
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
9857
10499
  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.";
9858
10500
  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.";
9859
- 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.";
10501
+ 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).";
9860
10502
  var EXPRESSION_INVALID = "expression-invalid";
9861
10503
  var AUTHORING_RULES = [
9862
10504
  // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
@@ -9917,8 +10559,14 @@ var AUTHORING_RULES = [
9917
10559
  input: "normalized",
9918
10560
  commands: ALL,
9919
10561
  source: "packages/lint/src/validate-functional-completeness.ts",
9920
- surfaces: CLI_ONLY,
9921
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10562
+ // Runtime publish gate (#4716): the OBJECT write door — the five gating
10563
+ // object rules cross together under the 2026-08-18 adjudication. The
10564
+ // false-positive budget the crossing owed was exempted on a measured
10565
+ // 0 refusals / 75 real object declarations (authored config-file metadata,
10566
+ // so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
10567
+ // advisory-tier object rules deliberately do NOT ride.
10568
+ surfaces: CLI_AND_RUNTIME,
10569
+ runtimeTypes: ["object"],
9922
10570
  run: (stack) => validateFunctionalCompleteness(stack)
9923
10571
  },
9924
10572
  // [#7521, via cloud#1225] A managed object advertising a generic write verb
@@ -9942,8 +10590,11 @@ var AUTHORING_RULES = [
9942
10590
  input: "normalized",
9943
10591
  commands: ALL,
9944
10592
  source: "packages/lint/src/validate-managed-api-methods.ts",
9945
- surfaces: CLI_ONLY,
9946
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10593
+ // Runtime publish gate (#4716): a managed object advertising a verb its
10594
+ // own affordances refuse is exactly the contradiction a Studio/MCP author
10595
+ // can save today — the CLI sweep (#7934) never sees an overlay row.
10596
+ surfaces: CLI_AND_RUNTIME,
10597
+ runtimeTypes: ["object"],
9947
10598
  run: (stack) => validateManagedApiMethods(stack)
9948
10599
  },
9949
10600
  // A view container in `views: []` that registers zero views: nothing appears
@@ -9965,19 +10616,38 @@ var AUTHORING_RULES = [
9965
10616
  },
9966
10617
  // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
9967
10618
  // chartConfig axis/series must resolve against the declared datasets.
10619
+ //
10620
+ // Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
10621
+ // widget bound to a dataset that resolves to nothing sailed through save AND
10622
+ // publish — this rule provably caught the body and was simply never invoked,
10623
+ // because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
10624
+ // hold a forward reference; publishing refuses it with the key path named.
10625
+ // The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
10626
+ // — without it every legitimate board reads as dangling, the 3-phantom
10627
+ // measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
10628
+ // rule's error ids on the publish gate, not just `widget-dataset-unknown` —
10629
+ // ruled 2026-08-15: they are one coherent "this board cannot render"
10630
+ // reference-integrity class, and the ~6× wider accept-set narrowing was
10631
+ // accepted knowingly rather than splitting the dataset limb into its own
10632
+ // rule (traversal-duplication drift) or adding a per-finding-id surface
10633
+ // filter (registry machinery that weakens "delete a rule from the table and
10634
+ // enforcement stops in the same commit"). Warning-tier ids ride along on the
10635
+ // advisory channel and never block (#4463 P1).
9968
10636
  {
9969
10637
  name: "validateWidgetBindings",
9970
10638
  tier: "gating",
9971
10639
  input: "parsed",
9972
10640
  commands: ALL,
9973
10641
  source: "packages/lint/src/validate-widget-bindings.ts",
9974
- surfaces: CLI_ONLY,
9975
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10642
+ surfaces: CLI_AND_RUNTIME,
10643
+ runtimeTypes: ["dashboard"],
9976
10644
  run: (stack) => validateWidgetBindings(stack)
9977
10645
  },
9978
- // ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
9979
- // target that resolves to no defined action ships a button that renders and
9980
- // silently does nothing on click. Unresolved `url` routes stay advisory.
10646
+ // ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
10647
+ // button that renders and refuses (or does nothing) on click: a `script`
10648
+ // target must name a defined action, a `modal` target must name a declared
10649
+ // page (objectstack#6739-A — a modal string target names a PAGE, only).
10650
+ // Unresolved `url` routes stay advisory.
9981
10651
  {
9982
10652
  name: "validateDashboardActionRefs",
9983
10653
  tier: "gating",
@@ -10002,6 +10672,28 @@ var AUTHORING_RULES = [
10002
10672
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10003
10673
  run: (stack) => validateFilterTokens(stack)
10004
10674
  },
10675
+ // #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
10676
+ // name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
10677
+ // no layer: the engine refuses it on a declared temporal field at query time
10678
+ // (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
10679
+ // literal string. This is the authoring-time refusal the ruling shipped
10680
+ // alongside the engine door, judging the filter literal in isolation —
10681
+ // ordering positions only, all three authored filter shapes. Like
10682
+ // `validateEmptyCombinators` it needs NO resolution context, so
10683
+ // RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
10684
+ // for every filter-carrying type the gate already maps: the write path is
10685
+ // the one door an AI author uses, and dashboards/views are where the preset
10686
+ // vocabulary is near enough to reach for.
10687
+ {
10688
+ name: "validatePresetComparands",
10689
+ tier: "gating",
10690
+ input: "parsed",
10691
+ commands: ALL,
10692
+ source: "packages/lint/src/validate-preset-comparands.ts",
10693
+ surfaces: CLI_AND_RUNTIME,
10694
+ runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
10695
+ run: (stack) => validatePresetComparands(stack)
10696
+ },
10005
10697
  // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
10006
10698
  // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
10007
10699
  // this rule does not touch it: it refuses the literal SPELLINGS at authoring
@@ -10045,9 +10737,31 @@ var AUTHORING_RULES = [
10045
10737
  // collection in the snapshot and return nothing, and the two that would
10046
10738
  // load `typescript` need a hook/action/react body the snapshot never
10047
10739
  // carries — which is what `runtime-lazy-deps.test.ts` pins.
10740
+ //
10741
+ // [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
10742
+ // taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
10743
+ // dispatch the suite, and the suite's own per-member `runtimeTypes`
10744
+ // (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
10745
+ // that snapshot — the gate passes the written type through
10746
+ // `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
10747
+ // members that judge a list view's field references and resolve only
10748
+ // against the `objects` collection the snapshot carries
10749
+ // (`validateSearchableFields`, `validateSortableFields`). The whole suite
10750
+ // is NOT the right granularity for this door, measured not assumed, and
10751
+ // the crossing fails differently per body shape — both ways wrong:
10752
+ // `validateActionNameRefs` (error-tier) resolves `views[].list` /
10753
+ // `views[].listViews.*` action names against `stack.actions`, a collection
10754
+ // no per-write snapshot carries, so on a CONTAINER view write it would
10755
+ // refuse every stack-level action the body names (measured:
10756
+ // `action-name-undefined` on the snapshot shape, clean on the full stack)
10757
+ // — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
10758
+ // type the gate has. On a FLATTENED overlay it has no rung at all, so the
10759
+ // crossing would be a silent no-op that reads as coverage — the very shape
10760
+ // #9313 was filed about. Flow snapshots are unchanged: every member keeps
10761
+ // the default `flow` declaration.
10048
10762
  surfaces: CLI_AND_RUNTIME,
10049
- runtimeTypes: ["flow"],
10050
- run: (stack) => validateReferenceIntegrity(stack)
10763
+ runtimeTypes: ["flow", "view"],
10764
+ run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
10051
10765
  },
10052
10766
  // ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
10053
10767
  // is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
@@ -10220,7 +10934,7 @@ var AUTHORING_RULES = [
10220
10934
  commands: ALL,
10221
10935
  source: "packages/lint/src/validate-record-title.ts",
10222
10936
  surfaces: CLI_ONLY,
10223
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10937
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10224
10938
  run: (stack) => validateRecordTitle(stack)
10225
10939
  },
10226
10940
  // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
@@ -10233,7 +10947,7 @@ var AUTHORING_RULES = [
10233
10947
  commands: ALL,
10234
10948
  source: "packages/lint/src/validate-semantic-roles.ts",
10235
10949
  surfaces: CLI_ONLY,
10236
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10950
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10237
10951
  run: (stack) => validateSemanticRoles(stack)
10238
10952
  },
10239
10953
  // #2578 / #4449 — a form section's field reference that resolves to nothing
@@ -10444,7 +11158,7 @@ var AUTHORING_RULES = [
10444
11158
  commands: ALL,
10445
11159
  source: "packages/lint/src/lint-liveness-properties.ts",
10446
11160
  surfaces: CLI_ONLY,
10447
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11161
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10448
11162
  run: (stack) => lintLivenessProperties(stack).map((f) => ({
10449
11163
  severity: "warning",
10450
11164
  rule: f.rule,
@@ -10463,8 +11177,12 @@ var AUTHORING_RULES = [
10463
11177
  input: "parsed",
10464
11178
  commands: ALL,
10465
11179
  source: "packages/lint/src/lint-autonumber-formats.ts",
10466
- surfaces: CLI_ONLY,
10467
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11180
+ // Runtime publish gate (#4716): an autonumber referencing a field the
11181
+ // object does not carry is broken from the first record; only the error
11182
+ // arm blocks — the optional-field arm is `warning` and rides the
11183
+ // advisory channel like every other non-error finding (#4463 P1).
11184
+ surfaces: CLI_AND_RUNTIME,
11185
+ runtimeTypes: ["object"],
10468
11186
  run: (stack) => lintAutonumberFormats(stack).map((f) => ({
10469
11187
  severity: f.severity,
10470
11188
  rule: f.rule,
@@ -10505,12 +11223,12 @@ var AUTHORING_RULES = [
10505
11223
  commands: ["validate", "build"],
10506
11224
  source: "packages/lint/src/data-model-rules.ts",
10507
11225
  surfaces: CLI_ONLY,
10508
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11226
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10509
11227
  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.",
10510
11228
  run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10511
11229
  severity: f.severity === "suggestion" ? "info" : f.severity,
10512
11230
  rule: f.rule,
10513
- where: f.path,
11231
+ where: f.where,
10514
11232
  path: f.path,
10515
11233
  message: f.message,
10516
11234
  hint: f.fix ?? ""
@@ -10526,12 +11244,12 @@ var AUTHORING_RULES = [
10526
11244
  commands: ["validate", "build"],
10527
11245
  source: "packages/lint/src/data-model-rules.ts",
10528
11246
  surfaces: CLI_ONLY,
10529
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11247
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10530
11248
  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.",
10531
11249
  run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10532
11250
  severity: f.severity === "suggestion" ? "info" : f.severity,
10533
11251
  rule: f.rule,
10534
- where: f.path,
11252
+ where: f.where,
10535
11253
  path: f.path,
10536
11254
  message: f.message,
10537
11255
  hint: f.fix ?? ""
@@ -10548,12 +11266,12 @@ var AUTHORING_RULES = [
10548
11266
  commands: ["validate", "build"],
10549
11267
  source: "packages/lint/src/data-model-rules.ts",
10550
11268
  surfaces: CLI_ONLY,
10551
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11269
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10552
11270
  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.",
10553
11271
  run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10554
11272
  severity: f.severity === "suggestion" ? "info" : f.severity,
10555
11273
  rule: f.rule,
10556
- where: f.path,
11274
+ where: f.where,
10557
11275
  path: f.path,
10558
11276
  message: f.message,
10559
11277
  hint: f.fix ?? ""
@@ -10681,16 +11399,26 @@ var AUTHORING_RULES = [
10681
11399
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10682
11400
  run: (stack) => validateOrgAxisRedLines(stack)
10683
11401
  },
10684
- // #4698 — the "declared but never read" gate, for the one surface where the
10685
- // predicate is EXACT rather than inferred. A sharing rule's `condition` has a
10686
- // single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
10687
- // the key is `compileCelToFilter(condition, { variables: {} })`; a condition
10688
- // that does not lower means the rule is SKIPPED at boot, so the grant is
10689
- // declared and does not exist. The lint calls that same compiler, from the
10690
- // same package, with the same options the verdict cannot drift from the
10691
- // consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
10692
- // docblock states: the whole authorable surface is enforced, and this was the
10693
- // one field where that sentence was not yet true.
11402
+ // #4698 / #9698 — the "declared but never read" gate, for the two fields of a
11403
+ // sharing rule where the predicate is EXACT rather than inferred.
11404
+ //
11405
+ // - `condition` has a single runtime consumer
11406
+ // (`bootstrapDeclaredSharingRules`) whose only use of the key is
11407
+ // `compileCelToFilter(condition, { variables: {} })`; a condition that
11408
+ // does not lower means the rule is SKIPPED at boot.
11409
+ // - `object` decides whether the grant is refused outright: reconcile hands
11410
+ // each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
11411
+ // `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
11412
+ // `public` or it is a `controlled_by_parent` detail. Both are decidable
11413
+ // from authored metadata; the other arms of that verdict (`owner_id`, the
11414
+ // bypass set, federated anchors) are not, and are excluded by name.
11415
+ //
11416
+ // Either way the grant is declared and does not exist. The lint calls the
11417
+ // same compiler and mirrors the same verdict function, from the same inputs
11418
+ // — the verdict cannot drift from the consumers'. Gating for the ADR-0078
11419
+ // reason `SharingRuleSchema`'s own docblock states: the whole authorable
11420
+ // surface is enforced, and these were the fields where that sentence was not
11421
+ // yet true.
10694
11422
  {
10695
11423
  name: "validateSharingRuleEnforceability",
10696
11424
  tier: "gating",
@@ -10698,7 +11426,7 @@ var AUTHORING_RULES = [
10698
11426
  commands: ALL,
10699
11427
  source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
10700
11428
  surfaces: CLI_ONLY,
10701
- 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.",
11429
+ 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.",
10702
11430
  run: (stack) => validateSharingRuleEnforceability(stack)
10703
11431
  },
10704
11432
  // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
@@ -10737,8 +11465,15 @@ var AUTHORING_RULES = [
10737
11465
  input: "parsed",
10738
11466
  commands: ALL,
10739
11467
  source: "packages/lint/src/validate-rule-compilability.ts",
10740
- surfaces: CLI_ONLY,
10741
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11468
+ // Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
11469
+ // judged snapshot actually carries a `json_schema` validation — so an
11470
+ // ordinary object write (no `json_schema` anywhere) still loads no
11471
+ // compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
11472
+ // The load it does take (~64 ms cold once, ~15 ms warm per publish
11473
+ // carrying such a rule) is the measured, adjudicated price of refusing a
11474
+ // validation rule that would otherwise ship compiled-by-nothing.
11475
+ surfaces: CLI_AND_RUNTIME,
11476
+ runtimeTypes: ["object"],
10742
11477
  run: (stack) => validateRuleCompilability(stack)
10743
11478
  },
10744
11479
  // #5178 — the residual half of #5029, which registering `ajv-formats` does
@@ -10759,8 +11494,12 @@ var AUTHORING_RULES = [
10759
11494
  input: "parsed",
10760
11495
  commands: ALL,
10761
11496
  source: "packages/lint/src/validate-rule-schema-formats.ts",
10762
- surfaces: CLI_ONLY,
10763
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11497
+ // Runtime publish gate (#4716): crosses with its compile sibling above —
11498
+ // the two judgements over one artifact stay on one side of the wall
11499
+ // (#7220's family discipline). Same lazy-ajv contract: the registered
11500
+ // format set is only enumerated once a schema actually names a format.
11501
+ surfaces: CLI_AND_RUNTIME,
11502
+ runtimeTypes: ["object"],
10764
11503
  run: (stack) => validateRuleSchemaFormats(stack)
10765
11504
  }
10766
11505
  ];
@@ -10820,7 +11559,20 @@ var TYPE_TO_STACK_KEY = {
10820
11559
  permission: "permissions",
10821
11560
  book: "books"
10822
11561
  };
10823
- var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
11562
+ function narrowObjectsToPackageClosure(objects, scope) {
11563
+ if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
11564
+ const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
11565
+ return objects.filter((entry) => {
11566
+ if (!entry || typeof entry !== "object") return true;
11567
+ const owner = entry[PACKAGE_PROVENANCE_KEY];
11568
+ if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
11569
+ if (reachable.has(owner)) return true;
11570
+ return isSystemObject(entry);
11571
+ });
11572
+ }
11573
+ var PACKAGE_PROVENANCE_KEY = "_packageId";
11574
+ var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
11575
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
10824
11576
  function runtimeAuthoringRulesFor(type) {
10825
11577
  return AUTHORING_RULES.filter(
10826
11578
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -10846,7 +11598,8 @@ function buildRuntimeWriteSnapshots(args) {
10846
11598
  const itemName = typeof item.name === "string" ? item.name : void 0;
10847
11599
  const baseline = {};
10848
11600
  for (const key of CONTEXT_STACK_KEYS) {
10849
- const collection = args.context?.[key] ?? [];
11601
+ const raw = args.context?.[key] ?? [];
11602
+ const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
10850
11603
  baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10851
11604
  }
10852
11605
  const candidate = {
@@ -10855,6 +11608,20 @@ function buildRuntimeWriteSnapshots(args) {
10855
11608
  };
10856
11609
  return { baseline, candidate };
10857
11610
  }
11611
+ var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
11612
+ var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
11613
+ var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
11614
+ function nameKeyFindingPath(path, candidate) {
11615
+ const m = TOP_LEVEL_INDEX.exec(path);
11616
+ if (!m) return path;
11617
+ const [, stackKey, index, rest] = m;
11618
+ if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
11619
+ const collection = candidate[stackKey];
11620
+ const entry = collection?.[Number(index)];
11621
+ const name = entry && typeof entry === "object" ? entry.name : void 0;
11622
+ if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
11623
+ return `${stackKey}.${name}${rest}`;
11624
+ }
10858
11625
  function runRules(rules, stack, ctx) {
10859
11626
  const findings = [];
10860
11627
  for (const rule of rules) {
@@ -10880,12 +11647,16 @@ function runRuntimeAuthoringRules(args) {
10880
11647
  const snapshots = buildRuntimeWriteSnapshots({
10881
11648
  type: args.type,
10882
11649
  item: args.item,
10883
- ...args.context !== void 0 ? { context: args.context } : {}
11650
+ ...args.context !== void 0 ? { context: args.context } : {},
11651
+ ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10884
11652
  });
10885
11653
  if (!snapshots) return empty;
10886
- const ctx = { sduiManifest: args.sduiManifest };
11654
+ const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
10887
11655
  const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
10888
- const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
11656
+ const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
11657
+ const path = nameKeyFindingPath(f.path, snapshots.candidate);
11658
+ return path === f.path ? f : { ...f, path };
11659
+ });
10889
11660
  return {
10890
11661
  errors: added.filter((f) => f.severity === "error"),
10891
11662
  advisories: added.filter((f) => f.severity !== "error"),
@@ -10893,10 +11664,12 @@ function runRuntimeAuthoringRules(args) {
10893
11664
  };
10894
11665
  }
10895
11666
  export {
11667
+ ACTION_BODY_SOURCE_UNPARSEABLE,
10896
11668
  ACTION_BODY_WRITE_EXCLUSIONS,
10897
11669
  ACTION_BODY_WRITE_PATTERNS,
10898
11670
  ACTION_BODY_WRITE_PATTERN_IDS,
10899
11671
  ACTION_BODY_WRITE_UNKNOWN_FIELD,
11672
+ ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
10900
11673
  ACTION_NAME_UNDEFINED,
10901
11674
  ACTION_NO_PLACEMENT,
10902
11675
  ACTION_RECORD_WRITE_DISCARDED,
@@ -10942,6 +11715,7 @@ export {
10942
11715
  FIELD_GROUP_UNDECLARED,
10943
11716
  FILTER_EMPTY_COMBINATOR,
10944
11717
  FILTER_EMPTY_NODE,
11718
+ FILTER_PRESET_COMPARAND,
10945
11719
  FILTER_TOKEN_UNKNOWN,
10946
11720
  FLOW_APPROVAL_REVISE_DEAD_END,
10947
11721
  FLOW_APPROVAL_REVISE_DISABLED,
@@ -10959,6 +11733,7 @@ export {
10959
11733
  FLOW_MULTIPLE_DEFAULT_EDGES,
10960
11734
  FLOW_MULTI_WRITE_UNFILTERED,
10961
11735
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
11736
+ FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
10962
11737
  FLOW_PHANTOM_AGGREGATION,
10963
11738
  FLOW_RUNAS_UNSCOPED,
10964
11739
  FLOW_TEMPLATE_FIELD_UNPROVISIONED,
@@ -10976,10 +11751,12 @@ export {
10976
11751
  FLOW_WRITE_NODE_TYPES_DEFERRED,
10977
11752
  FORM_COLSPAN_ABSOLUTE,
10978
11753
  FORM_FIELD_UNKNOWN,
11754
+ HOOK_BODY_SOURCE_UNPARSEABLE,
10979
11755
  HOOK_BODY_WRITE_EXCLUSIONS,
10980
11756
  HOOK_BODY_WRITE_PATTERNS,
10981
11757
  HOOK_BODY_WRITE_PATTERN_IDS,
10982
11758
  HOOK_BODY_WRITE_UNKNOWN_FIELD,
11759
+ HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
10983
11760
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
10984
11761
  LIVENESS_DEAD_PROPERTY,
10985
11762
  LIVENESS_EXPERIMENTAL_PROPERTY,
@@ -10998,6 +11775,7 @@ export {
10998
11775
  PAGE_FIELD_UNKNOWN,
10999
11776
  PAGE_FIELD_UNPROVISIONED,
11000
11777
  PAGE_SOURCE_CLASSNAME,
11778
+ PARSE_FAILURE_HINT,
11001
11779
  PREDICATE_PATH_UNRESOLVED,
11002
11780
  PREDICATE_PATH_UNROOTED,
11003
11781
  PREDICATE_RHS_PATH_SHAPED,
@@ -11008,6 +11786,7 @@ export {
11008
11786
  REACT_CHART_DRILLDOWN_INVALID,
11009
11787
  REACT_CHART_FIELD_UNKNOWN,
11010
11788
  REACT_CHART_FIELD_UNPROVISIONED,
11789
+ REACT_PAGE_SOURCE_UNPARSEABLE,
11011
11790
  REFERENCE_INTEGRITY_RULES,
11012
11791
  RLS_PREDICATE_OVER_BUDGET,
11013
11792
  RLS_PREDICATE_UNENFORCEABLE,
@@ -11015,6 +11794,7 @@ export {
11015
11794
  RUNTIME_AJV_OPTIONS,
11016
11795
  SEAL_MARKERS,
11017
11796
  SEARCHABLE_FIELD_UNKNOWN,
11797
+ SEARCHABLE_FIELD_UNPROVISIONED,
11018
11798
  SEARCHABLE_FIELD_UNSEARCHABLE,
11019
11799
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
11020
11800
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
@@ -11033,9 +11813,15 @@ export {
11033
11813
  SEED_VALUE_OUTSIDE_STATE_MACHINE,
11034
11814
  SEMANTIC_ROLE_FIELD_UNKNOWN,
11035
11815
  SEMANTIC_ROLE_FIELD_UNPROVISIONED,
11816
+ SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
11817
+ SHARING_RULE_OBJECT_NOT_SHAREABLE,
11036
11818
  SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
11037
11819
  SHARING_RULE_UNLOWERABLE_CONDITION,
11820
+ SORT_FIELD_UNKNOWN,
11821
+ SORT_FIELD_UNPROVISIONED,
11822
+ SORT_FIELD_UNSORTABLE,
11038
11823
  STARTUP_OPEN_VOCABULARY_VERDICT,
11824
+ STARTUP_SOURCE_UNPARSEABLE,
11039
11825
  STARTUP_VERDICT_ASSERTIVE_WORDING,
11040
11826
  STARTUP_VERDICT_HINT,
11041
11827
  STYLE_CLASSNAME_TAILWIND,
@@ -11071,6 +11857,8 @@ export {
11071
11857
  authoringRulesFor,
11072
11858
  buildAccessMatrix,
11073
11859
  buildRuntimeWriteSnapshots,
11860
+ checkSortDeclaration,
11861
+ describeParseFailure,
11074
11862
  diffAccessMatrix,
11075
11863
  extractHookBodyWriteSet,
11076
11864
  extractHookBodyWrites,
@@ -11085,6 +11873,7 @@ export {
11085
11873
  lintUniqueDeclarations,
11086
11874
  lintUnscopedDeclaredIndexes,
11087
11875
  lintViewRefs,
11876
+ narrowObjectsToPackageClosure,
11088
11877
  nearestRegisteredFormat,
11089
11878
  nullGuardMessage,
11090
11879
  runAuthoringRules,
@@ -11123,6 +11912,7 @@ export {
11123
11912
  validatePageFieldBindings,
11124
11913
  validatePageSourceStyling,
11125
11914
  validatePredicatePathRefs,
11915
+ validatePresetComparands,
11126
11916
  validateReactPageProps,
11127
11917
  validateReactPages,
11128
11918
  validateReadonlyFlowWrites,
@@ -11139,6 +11929,7 @@ export {
11139
11929
  validateSeedStateMachine,
11140
11930
  validateSemanticRoles,
11141
11931
  validateSharingRuleEnforceability,
11932
+ validateSortableFields,
11142
11933
  validateStackExpressions,
11143
11934
  validateTranslatableSections,
11144
11935
  validateTranslationReferences,