@objectstack/lint 17.0.0 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
  }
@@ -2580,6 +2581,7 @@ import {
2580
2581
  } from "@objectstack/spec/data";
2581
2582
  var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
2582
2583
  var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
2584
+ var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
2583
2585
  function asArray10(v) {
2584
2586
  if (Array.isArray(v)) return v;
2585
2587
  if (v && typeof v === "object") {
@@ -2670,7 +2672,7 @@ function indexObjectSearchTargets(stack) {
2670
2672
  }
2671
2673
  return fieldsByObject;
2672
2674
  }
2673
- function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
2675
+ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
2674
2676
  const findings = [];
2675
2677
  if (!Array.isArray(declared) || declared.length === 0) return findings;
2676
2678
  if (!objectName) return findings;
@@ -2678,6 +2680,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2678
2680
  const target = fieldsByObject.get(objectName);
2679
2681
  if (!target) return findings;
2680
2682
  const known = target.names;
2683
+ const anchors = unprovisionedAnchors?.get(objectName);
2681
2684
  const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
2682
2685
  for (let i = 0; i < declared.length; i++) {
2683
2686
  const entry = declared[i];
@@ -2695,6 +2698,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
2695
2698
  });
2696
2699
  continue;
2697
2700
  }
2701
+ if (anchors?.has(name)) {
2702
+ findings.push({
2703
+ severity: "warning",
2704
+ rule: SEARCHABLE_FIELD_UNPROVISIONED,
2705
+ where,
2706
+ path: `${path}[${i}]`,
2707
+ 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.`),
2708
+ hint: unprovisionedAnchorHint(objectName, name)
2709
+ });
2710
+ }
2698
2711
  if (isVirtualSearchField(target.fields[name])) {
2699
2712
  const vtype = target.fields[name]?.type;
2700
2713
  findings.push({
@@ -2748,9 +2761,19 @@ function validateSearchableFields(stack) {
2748
2761
  if (!isRec4(stack)) return findings;
2749
2762
  const objects = asArray10(stack.objects);
2750
2763
  const fieldsByObject = indexObjectSearchTargets(stack);
2764
+ const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
2751
2765
  const check = (declared, objectName, where, path, subject, role) => {
2752
2766
  findings.push(
2753
- ...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
2767
+ ...checkSearchableFieldList(
2768
+ declared,
2769
+ objectName,
2770
+ fieldsByObject,
2771
+ where,
2772
+ path,
2773
+ subject,
2774
+ role,
2775
+ unprovisionedAnchors
2776
+ )
2754
2777
  );
2755
2778
  };
2756
2779
  for (let oi = 0; oi < objects.length; oi++) {
@@ -2937,7 +2960,12 @@ var COMPONENT_FIELD_SPECS = {
2937
2960
  "record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
2938
2961
  "record:path": { props: ["statusField"] },
2939
2962
  "element:number": { props: ["field"] },
2940
- "element:filter": { props: ["fields"] },
2963
+ // `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
2964
+ // whole element at element grain (ADR-0049 — no renderer ever shipped for it).
2965
+ // Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
2966
+ // spec-conformant page carries `fields` on it, and the #5068 props gate
2967
+ // reports an authored one by name with the element-retirement prescription —
2968
+ // the same #5775/#6629 residue class as the record-picker entries below.
2941
2969
  "element:form": { props: ["fields"] },
2942
2970
  // `labelField` is the one field-bearing prop this element declares. Its former
2943
2971
  // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
@@ -3640,7 +3668,12 @@ function validateReactPageProps(stack) {
3640
3668
  searchTargets,
3641
3669
  where,
3642
3670
  `${path} \u203A searchableFields`,
3643
- "searchableFields"
3671
+ "searchableFields",
3672
+ // A `<ListView>` prop is a view-level narrowing — the checker's
3673
+ // default, spelled out here because the #8404 provenance index
3674
+ // follows it positionally.
3675
+ "narrowing",
3676
+ unprovisionedAnchors
3644
3677
  )
3645
3678
  );
3646
3679
  }
@@ -5225,6 +5258,7 @@ function validateSecurityPosture(stack, opts) {
5225
5258
  }
5226
5259
  }
5227
5260
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
5261
+ const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
5228
5262
  const nowMs = opts?.nowMs ?? Date.now();
5229
5263
  for (const [i, seed] of asArray19(stack.data).entries()) {
5230
5264
  const seedObject = typeof seed.object === "string" ? seed.object : "";
@@ -5248,7 +5282,7 @@ function validateSecurityPosture(stack, opts) {
5248
5282
  }
5249
5283
  }
5250
5284
  const delegatedFrom = rec.delegated_from;
5251
- if (delegatedFrom != null && delegatedFrom !== "") {
5285
+ if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
5252
5286
  const reason = rec.reason;
5253
5287
  if (typeof reason !== "string" || reason.trim().length === 0) {
5254
5288
  findings.push({
@@ -5414,6 +5448,8 @@ function validateOrgAxisRedLines(stack) {
5414
5448
  import { compileCelToFilter } from "@objectstack/formula";
5415
5449
  var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
5416
5450
  var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
5451
+ var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
5452
+ var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
5417
5453
  function asArray21(v) {
5418
5454
  if (Array.isArray(v)) return v;
5419
5455
  if (v && typeof v === "object") {
@@ -5438,10 +5474,67 @@ function sourceOf(condition) {
5438
5474
  return str2(input?.source);
5439
5475
  }
5440
5476
  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).";
5477
+ function effectiveSharingModelOf(obj) {
5478
+ const m = obj.sharingModel;
5479
+ if (m === "private") return "private";
5480
+ if (m === "public_read") return "read";
5481
+ if (m === "public_read_write" || m === "controlled_by_parent") return "public";
5482
+ if (m == null) {
5483
+ const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
5484
+ return isSystem ? "public" : "private";
5485
+ }
5486
+ return "private";
5487
+ }
5488
+ function masterOf(obj) {
5489
+ for (const f of asArray21(obj.fields)) {
5490
+ if (f.type === "master_detail") {
5491
+ const ref = f.reference;
5492
+ if (typeof ref === "string" && ref) return ref;
5493
+ }
5494
+ }
5495
+ return void 0;
5496
+ }
5497
+ function anchorFindings(rule, index, objectsByName) {
5498
+ const object = str2(rule.object);
5499
+ if (!object) return [];
5500
+ const target = objectsByName.get(object);
5501
+ if (!target) return [];
5502
+ const name = str2(rule.name) || String(index);
5503
+ const where = `sharing rule "${name}" on object "${object}"`;
5504
+ const path = `sharingRules[${index}].object`;
5505
+ const owd = target.sharingModel;
5506
+ if (owd === "controlled_by_parent") {
5507
+ const master = masterOf(target);
5508
+ return [{
5509
+ severity: "error",
5510
+ rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
5511
+ where,
5512
+ path,
5513
+ 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.`,
5514
+ 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.`
5515
+ }];
5516
+ }
5517
+ if (effectiveSharingModelOf(target) !== "public") return [];
5518
+ 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`;
5519
+ return [{
5520
+ severity: "error",
5521
+ rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
5522
+ where,
5523
+ path,
5524
+ 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.`,
5525
+ 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.`
5526
+ }];
5527
+ }
5441
5528
  function validateSharingRuleEnforceability(stack) {
5442
5529
  const findings = [];
5443
5530
  const cfg = stack ?? {};
5531
+ const objectsByName = /* @__PURE__ */ new Map();
5532
+ for (const obj of asArray21(cfg.objects)) {
5533
+ const name = str2(obj.name);
5534
+ if (name) objectsByName.set(name, obj);
5535
+ }
5444
5536
  asArray21(cfg.sharingRules).forEach((rule, index) => {
5537
+ anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
5445
5538
  const input = toCompilerInput(rule.condition);
5446
5539
  if (input === null) return;
5447
5540
  const result = compileCelToFilter(input, { variables: {} });
@@ -5579,7 +5672,6 @@ function asArray23(v) {
5579
5672
  function strName7(v) {
5580
5673
  return typeof v === "string" && v.length > 0 ? v : void 0;
5581
5674
  }
5582
- var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
5583
5675
  var URL_COLLECTION_TO_STACK_KEY = {
5584
5676
  object: "objects",
5585
5677
  objects: "objects",
@@ -5624,13 +5716,10 @@ function collectKnownTargets(stack) {
5624
5716
  return { actions, objects, reports, dashboards, pages, views };
5625
5717
  }
5626
5718
  function resolveActionTarget(actionType, target, known) {
5627
- if (known.actions.has(target)) return true;
5628
5719
  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;
5720
+ return known.pages.has(target);
5632
5721
  }
5633
- return false;
5722
+ return known.actions.has(target);
5634
5723
  }
5635
5724
  function resolveUrlRoute(target, known) {
5636
5725
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
@@ -5660,14 +5749,13 @@ function validateDashboardActionRefs(stack) {
5660
5749
  const actionType = strName7(action.actionType) ?? "url";
5661
5750
  if (actionType === "script" || actionType === "modal") {
5662
5751
  if (resolveActionTarget(actionType, target, known)) return;
5663
- const kindWord = actionType === "script" ? "script" : "modal";
5664
5752
  findings.push({
5665
5753
  severity: "error",
5666
5754
  rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
5667
5755
  where,
5668
5756
  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.`
5757
+ 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).`,
5758
+ 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
5759
  });
5672
5760
  return;
5673
5761
  }
@@ -5709,7 +5797,7 @@ function validateDashboardActionRefs(stack) {
5709
5797
  import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
5710
5798
 
5711
5799
  // src/filter-walk.ts
5712
- var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
5800
+ var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
5713
5801
  function asArray24(v) {
5714
5802
  if (Array.isArray(v)) return v;
5715
5803
  if (v && typeof v === "object") {
@@ -5813,6 +5901,149 @@ function validateFilterTokens(stack) {
5813
5901
  return out;
5814
5902
  }
5815
5903
 
5904
+ // src/validate-preset-comparands.ts
5905
+ import {
5906
+ VALID_AST_OPERATORS as VALID_AST_OPERATORS2,
5907
+ bareDateRangePresetComparandMessage,
5908
+ canonicalAstOperator,
5909
+ isDateRangePresetName
5910
+ } from "@objectstack/spec/data";
5911
+ import { normalizeFilterOperator } from "@objectstack/spec/ui";
5912
+ var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
5913
+ var PRESET_COMPARAND_SURFACES = [
5914
+ { key: "dashboards", kind: "dashboard" },
5915
+ { key: "objects", kind: "object" },
5916
+ { key: "views", kind: "view" },
5917
+ { key: "reports", kind: "report" },
5918
+ { key: "datasets", kind: "dataset" },
5919
+ { key: "pages", kind: "page" },
5920
+ { key: "apps", kind: "app" },
5921
+ { key: "flows", kind: "flow" }
5922
+ ];
5923
+ var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
5924
+ var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
5925
+ var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
5926
+ "greater_than",
5927
+ "greater_than_or_equal",
5928
+ "less_than",
5929
+ "less_than_or_equal",
5930
+ "before",
5931
+ "after"
5932
+ ]);
5933
+ var MAX_DEPTH = 32;
5934
+ function isPlainObject(v) {
5935
+ return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
5936
+ }
5937
+ function finding(where, path, preset, operator) {
5938
+ return {
5939
+ severity: "error",
5940
+ rule: FILTER_PRESET_COMPARAND,
5941
+ where,
5942
+ path,
5943
+ message: bareDateRangePresetComparandMessage(preset, operator),
5944
+ 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."
5945
+ };
5946
+ }
5947
+ function judgeConditionNode(node, path, where, out, depth) {
5948
+ if (depth > MAX_DEPTH) return;
5949
+ for (const [key, value] of Object.entries(node)) {
5950
+ const here = `${path}.${key}`;
5951
+ if (key === "$and" || key === "$or") {
5952
+ if (Array.isArray(value)) {
5953
+ value.forEach((arm, i) => {
5954
+ if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
5955
+ });
5956
+ }
5957
+ continue;
5958
+ }
5959
+ if (key === "$not") {
5960
+ if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
5961
+ continue;
5962
+ }
5963
+ if (key.startsWith("$")) continue;
5964
+ if (!isPlainObject(value)) continue;
5965
+ const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
5966
+ if (!hasOps) {
5967
+ judgeConditionNode(value, here, where, out, depth + 1);
5968
+ continue;
5969
+ }
5970
+ for (const [op, comparand] of Object.entries(value)) {
5971
+ if (ORDERING_DOLLAR_OPS.has(op) && isDateRangePresetName(comparand)) {
5972
+ out.push(finding(where, `${here}.${op}`, comparand, op));
5973
+ continue;
5974
+ }
5975
+ if (op === "$between" && Array.isArray(comparand)) {
5976
+ comparand.forEach((endpoint, i) => {
5977
+ if (isDateRangePresetName(endpoint)) {
5978
+ out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
5979
+ }
5980
+ });
5981
+ }
5982
+ }
5983
+ }
5984
+ }
5985
+ function judgeFilterRule(rule, path, where, out) {
5986
+ const operator = normalizeFilterOperator(rule.operator);
5987
+ if (typeof operator !== "string") return;
5988
+ const value = rule.value;
5989
+ if (ORDERING_RULE_OPS.has(operator) && isDateRangePresetName(value)) {
5990
+ out.push(finding(where, `${path}.value`, value, operator));
5991
+ return;
5992
+ }
5993
+ if (operator === "between" && Array.isArray(value)) {
5994
+ value.forEach((endpoint, i) => {
5995
+ if (isDateRangePresetName(endpoint)) {
5996
+ out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
5997
+ }
5998
+ });
5999
+ }
6000
+ }
6001
+ function judgeTriple(triple, path, where, out) {
6002
+ const op = triple[1];
6003
+ if (typeof op !== "string") return;
6004
+ const canonical = canonicalAstOperator(op);
6005
+ const value = triple[2];
6006
+ if (ORDERING_INFIX_OPS.has(canonical) && isDateRangePresetName(value)) {
6007
+ out.push(finding(where, `${path}[2]`, value, op));
6008
+ return;
6009
+ }
6010
+ if (canonical === "between" && Array.isArray(value)) {
6011
+ value.forEach((endpoint, i) => {
6012
+ if (isDateRangePresetName(endpoint)) {
6013
+ out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
6014
+ }
6015
+ });
6016
+ }
6017
+ }
6018
+ function judgeFilterValue(node, path, where, out, depth) {
6019
+ if (depth > MAX_DEPTH) return;
6020
+ if (Array.isArray(node)) {
6021
+ if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && VALID_AST_OPERATORS2.has(node[1].toLowerCase())) {
6022
+ judgeTriple(node, path, where, out);
6023
+ return;
6024
+ }
6025
+ node.forEach((member, i) => {
6026
+ if (typeof member === "string") return;
6027
+ judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
6028
+ });
6029
+ return;
6030
+ }
6031
+ if (!isPlainObject(node)) return;
6032
+ if (typeof node.field === "string" && typeof node.operator === "string") {
6033
+ judgeFilterRule(node, path, where, out);
6034
+ return;
6035
+ }
6036
+ judgeConditionNode(node, path, where, out, depth);
6037
+ }
6038
+ function validatePresetComparands(stack) {
6039
+ if (!stack || typeof stack !== "object") return [];
6040
+ const out = [];
6041
+ walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
6042
+ judgeFilterValue(value, path, where, out, 0);
6043
+ });
6044
+ return out;
6045
+ }
6046
+
5816
6047
  // src/validate-empty-combinators.ts
5817
6048
  import { reduceFilterVerdict } from "@objectstack/spec/data";
5818
6049
  var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
@@ -6264,6 +6495,176 @@ function validateNavObjectServability(stack) {
6264
6495
  return findings;
6265
6496
  }
6266
6497
 
6498
+ // src/validate-sortable-fields.ts
6499
+ import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
6500
+ var SORT_FIELD_UNKNOWN = "sort-field-unknown";
6501
+ var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
6502
+ function isRec16(v) {
6503
+ return !!v && typeof v === "object" && !Array.isArray(v);
6504
+ }
6505
+ function strName11(v) {
6506
+ return typeof v === "string" && v.length > 0 ? v : void 0;
6507
+ }
6508
+ function readSortKeys(declared) {
6509
+ const fromShorthand = (raw, at) => {
6510
+ const trimmed = raw.trim();
6511
+ if (!trimmed) return void 0;
6512
+ const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
6513
+ return bare ? { field: bare, at } : void 0;
6514
+ };
6515
+ if (typeof declared === "string") {
6516
+ return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
6517
+ }
6518
+ if (Array.isArray(declared)) {
6519
+ const keys = [];
6520
+ for (let i = 0; i < declared.length; i++) {
6521
+ const el = declared[i];
6522
+ if (typeof el === "string") {
6523
+ const k = fromShorthand(el, `[${i}]`);
6524
+ if (k) keys.push(k);
6525
+ continue;
6526
+ }
6527
+ if (!isRec16(el)) continue;
6528
+ const field = strName11(el.field);
6529
+ if (field) keys.push({ field: field.trim(), at: `[${i}]` });
6530
+ }
6531
+ return keys;
6532
+ }
6533
+ return [];
6534
+ }
6535
+ function suggest4(target, known) {
6536
+ let best;
6537
+ let bestScore = Infinity;
6538
+ for (const candidate of known) {
6539
+ const d = distance3(target, candidate);
6540
+ if (d < bestScore) {
6541
+ bestScore = d;
6542
+ best = candidate;
6543
+ }
6544
+ }
6545
+ const limit = Math.max(2, Math.floor(target.length / 3));
6546
+ return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
6547
+ }
6548
+ function distance3(a, b) {
6549
+ const m = a.length;
6550
+ const n = b.length;
6551
+ if (m === 0) return n;
6552
+ if (n === 0) return m;
6553
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
6554
+ for (let i = 1; i <= m; i++) {
6555
+ const curr = [i, ...new Array(n).fill(0)];
6556
+ for (let j = 1; j <= n; j++) {
6557
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6558
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
6559
+ }
6560
+ prev = curr;
6561
+ }
6562
+ return prev[n];
6563
+ }
6564
+ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
6565
+ const findings = [];
6566
+ if (declared === void 0 || declared === null) return findings;
6567
+ if (!objectName) return findings;
6568
+ if (!fieldsByObject.has(objectName)) return findings;
6569
+ const target = fieldsByObject.get(objectName);
6570
+ if (!target) return findings;
6571
+ const known = target.names;
6572
+ for (const key of readSortKeys(declared)) {
6573
+ const name = key.field;
6574
+ const head = name.split(".")[0];
6575
+ if (SYSTEM_FIELDS.has(head)) continue;
6576
+ if (!known.has(head)) {
6577
+ const dotted = name.includes(".");
6578
+ findings.push({
6579
+ severity: "error",
6580
+ rule: SORT_FIELD_UNKNOWN,
6581
+ where,
6582
+ path: `${path}${key.at}`,
6583
+ 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)),
6584
+ 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(", ")}.` : "")
6585
+ });
6586
+ continue;
6587
+ }
6588
+ const meta = target.fields[name];
6589
+ if (isVirtualSearchField2(meta)) {
6590
+ const vtype = meta?.type;
6591
+ findings.push({
6592
+ severity: "error",
6593
+ rule: SORT_FIELD_UNSORTABLE,
6594
+ where,
6595
+ path: `${path}${key.at}`,
6596
+ 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.`,
6597
+ 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.`
6598
+ });
6599
+ }
6600
+ }
6601
+ return findings;
6602
+ }
6603
+ function validateSortableFields(stack) {
6604
+ const findings = [];
6605
+ if (!isRec16(stack)) return findings;
6606
+ const objects = Array.isArray(stack.objects) ? stack.objects : isRec16(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
6607
+ const fieldsByObject = indexObjectSearchTargets(stack);
6608
+ const check = (declared, objectName, where, path, subject) => {
6609
+ findings.push(
6610
+ ...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
6611
+ );
6612
+ };
6613
+ for (let oi = 0; oi < objects.length; oi++) {
6614
+ const obj = objects[oi];
6615
+ if (!isRec16(obj)) continue;
6616
+ const objName = strName11(obj.name);
6617
+ const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
6618
+ if (isRec16(obj.listViews)) {
6619
+ for (const [key, lv] of Object.entries(obj.listViews)) {
6620
+ if (!isRec16(lv)) continue;
6621
+ check(
6622
+ lv.sort,
6623
+ // A built-in list view belongs to its object; an inline `data.object`
6624
+ // may still retarget it (ADR-0047 allows the explicit binding).
6625
+ listViewObject2(lv) ?? objName,
6626
+ `${label2} \u203A listViews.${key}`,
6627
+ `objects[${oi}].listViews.${key}.sort`,
6628
+ "list-view sort"
6629
+ );
6630
+ }
6631
+ }
6632
+ }
6633
+ const views = Array.isArray(stack.views) ? stack.views : [];
6634
+ for (let vi = 0; vi < views.length; vi++) {
6635
+ const view = views[vi];
6636
+ if (!isRec16(view)) continue;
6637
+ const viewLabel2 = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
6638
+ const viewObject = strName11(view.objectName) ?? strName11(view.object);
6639
+ if (isRec16(view.list)) {
6640
+ check(
6641
+ view.list.sort,
6642
+ listViewObject2(view.list) ?? viewObject,
6643
+ `view "${viewLabel2}" \u203A list`,
6644
+ `views[${vi}].list.sort`,
6645
+ "list-view sort"
6646
+ );
6647
+ }
6648
+ if (isRec16(view.listViews)) {
6649
+ for (const [key, lv] of Object.entries(view.listViews)) {
6650
+ if (!isRec16(lv)) continue;
6651
+ check(
6652
+ lv.sort,
6653
+ listViewObject2(lv) ?? viewObject,
6654
+ `view "${viewLabel2}" \u203A listViews.${key}`,
6655
+ `views[${vi}].listViews.${key}.sort`,
6656
+ "list-view sort"
6657
+ );
6658
+ }
6659
+ }
6660
+ }
6661
+ return findings;
6662
+ }
6663
+ function listViewObject2(listView) {
6664
+ const data = listView.data;
6665
+ return isRec16(data) ? strName11(data.object) : void 0;
6666
+ }
6667
+
6267
6668
  // src/validate-action-name-refs.ts
6268
6669
  var ACTION_NAME_UNDEFINED = "action-name-undefined";
6269
6670
  function asArray28(v) {
@@ -6273,13 +6674,13 @@ function asArray28(v) {
6273
6674
  }
6274
6675
  return [];
6275
6676
  }
6276
- function strName11(v) {
6677
+ function strName12(v) {
6277
6678
  return typeof v === "string" && v.length > 0 ? v : void 0;
6278
6679
  }
6279
6680
  function strList(v) {
6280
6681
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
6281
6682
  }
6282
- function distance3(a, b) {
6683
+ function distance4(a, b) {
6283
6684
  const m = a.length;
6284
6685
  const n = b.length;
6285
6686
  if (m === 0) return n;
@@ -6295,11 +6696,11 @@ function distance3(a, b) {
6295
6696
  }
6296
6697
  return prev[n];
6297
6698
  }
6298
- function suggest4(target, known) {
6699
+ function suggest5(target, known) {
6299
6700
  let best;
6300
6701
  let bestScore = Infinity;
6301
6702
  for (const candidate of known) {
6302
- const d = distance3(target, candidate);
6703
+ const d = distance4(target, candidate);
6303
6704
  if (d < bestScore) {
6304
6705
  bestScore = d;
6305
6706
  best = candidate;
@@ -6311,13 +6712,13 @@ function suggest4(target, known) {
6311
6712
  function collectActionNames(stack) {
6312
6713
  const names = /* @__PURE__ */ new Set();
6313
6714
  for (const action of asArray28(stack.actions)) {
6314
- const n = strName11(action?.name);
6715
+ const n = strName12(action?.name);
6315
6716
  if (n) names.add(n);
6316
6717
  }
6317
6718
  for (const obj of asArray28(stack.objects)) {
6318
6719
  if (!obj || typeof obj !== "object") continue;
6319
6720
  for (const action of asArray28(obj.actions)) {
6320
- const n = strName11(action?.name);
6721
+ const n = strName12(action?.name);
6321
6722
  if (n) names.add(n);
6322
6723
  }
6323
6724
  }
@@ -6334,7 +6735,7 @@ function validateActionNameRefs(stack) {
6334
6735
  rule: ACTION_NAME_UNDEFINED,
6335
6736
  where,
6336
6737
  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),
6738
+ 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
6739
  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
6740
  });
6340
6741
  };
@@ -6360,7 +6761,7 @@ function validateActionNameRefs(stack) {
6360
6761
  if (!def || typeof def !== "object") continue;
6361
6762
  if (def.execution !== "aggregate") continue;
6362
6763
  if (def.actionDef !== void 0) continue;
6363
- const name = strName11(def.name);
6764
+ const name = strName12(def.name);
6364
6765
  if (!name) continue;
6365
6766
  check(
6366
6767
  name,
@@ -6375,7 +6776,7 @@ function validateActionNameRefs(stack) {
6375
6776
  for (let vi = 0; vi < views.length; vi++) {
6376
6777
  const view = views[vi];
6377
6778
  if (!view || typeof view !== "object") continue;
6378
- const viewName = strName11(view.name) ?? strName11(view.object) ?? `#${vi}`;
6779
+ const viewName = strName12(view.name) ?? strName12(view.object) ?? `#${vi}`;
6379
6780
  const owner = `view "${viewName}"`;
6380
6781
  checkListContainer(view.list, owner, "list", `views[${vi}].list`);
6381
6782
  const listViews = view.listViews;
@@ -6391,7 +6792,7 @@ function validateActionNameRefs(stack) {
6391
6792
  if (!obj || typeof obj !== "object") continue;
6392
6793
  const objListViews = obj.listViews;
6393
6794
  if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
6394
- const owner = `object "${strName11(obj.name) ?? `#${oi}`}"`;
6795
+ const owner = `object "${strName12(obj.name) ?? `#${oi}`}"`;
6395
6796
  for (const [key, lv] of Object.entries(objListViews)) {
6396
6797
  checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
6397
6798
  }
@@ -6400,7 +6801,7 @@ function validateActionNameRefs(stack) {
6400
6801
  for (let pi = 0; pi < pages.length; pi++) {
6401
6802
  const page = pages[pi];
6402
6803
  if (!page || typeof page !== "object") continue;
6403
- const pageName = strName11(page.name) ?? `#${pi}`;
6804
+ const pageName = strName12(page.name) ?? `#${pi}`;
6404
6805
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6405
6806
  const props = component.properties;
6406
6807
  if (!props || typeof props !== "object") continue;
@@ -6408,7 +6809,7 @@ function validateActionNameRefs(stack) {
6408
6809
  for (let ai = 0; ai < names.length; ai++) {
6409
6810
  check(
6410
6811
  names[ai],
6411
- `page "${pageName}" \xB7 component "${strName11(component.type) ?? "?"}"`,
6812
+ `page "${pageName}" \xB7 component "${strName12(component.type) ?? "?"}"`,
6412
6813
  `${path}.properties.actionNames[${ai}]`,
6413
6814
  "Quick-actions bar"
6414
6815
  );
@@ -6419,7 +6820,7 @@ function validateActionNameRefs(stack) {
6419
6820
  for (let ai = 0; ai < apps.length; ai++) {
6420
6821
  const app = apps[ai];
6421
6822
  if (!app || typeof app !== "object") continue;
6422
- const appName = strName11(app.name) ?? `#${ai}`;
6823
+ const appName = strName12(app.name) ?? `#${ai}`;
6423
6824
  const walkNav = (items, basePath) => {
6424
6825
  const navItems = asArray28(items);
6425
6826
  for (let ni = 0; ni < navItems.length; ni++) {
@@ -6427,20 +6828,20 @@ function validateActionNameRefs(stack) {
6427
6828
  if (!nav || typeof nav !== "object") continue;
6428
6829
  const navPath = `${basePath}[${ni}]`;
6429
6830
  const actionDef = nav.actionDef;
6430
- const actionName = strName11(actionDef?.actionName);
6831
+ const actionName = strName12(actionDef?.actionName);
6431
6832
  if (nav.type === "action" && actionName) {
6432
6833
  check(
6433
6834
  actionName,
6434
- `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
6835
+ `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
6435
6836
  `${navPath}.actionDef.actionName`,
6436
6837
  "Navigation action item"
6437
6838
  );
6438
6839
  }
6439
- const runAction = strName11(nav.runAction);
6840
+ const runAction = strName12(nav.runAction);
6440
6841
  if (nav.type === "object" && runAction) {
6441
6842
  check(
6442
6843
  runAction,
6443
- `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
6844
+ `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
6444
6845
  `${navPath}.runAction`,
6445
6846
  "Navigation deep-link auto-run"
6446
6847
  );
@@ -6466,7 +6867,7 @@ function asArray29(v) {
6466
6867
  }
6467
6868
  return [];
6468
6869
  }
6469
- function strName12(v) {
6870
+ function strName13(v) {
6470
6871
  return typeof v === "string" && v.length > 0 ? v : void 0;
6471
6872
  }
6472
6873
  function strList2(v) {
@@ -6481,7 +6882,7 @@ function collectNamePlacedActions(stack) {
6481
6882
  for (const n of strList2(list3[key])) placed.add(n);
6482
6883
  }
6483
6884
  for (const def of asArray29(list3.bulkActionDefs)) {
6484
- const n = strName12(def?.name);
6885
+ const n = strName13(def?.name);
6485
6886
  if (n) placed.add(n);
6486
6887
  }
6487
6888
  };
@@ -6507,7 +6908,7 @@ function validateActionLocations(stack) {
6507
6908
  const check = (action, path) => {
6508
6909
  if (!action || typeof action !== "object") return;
6509
6910
  if ("locations" in action) return;
6510
- const name = strName12(action.name);
6911
+ const name = strName13(action.name);
6511
6912
  if (!name) return;
6512
6913
  if (namePlaced.has(name)) return;
6513
6914
  findings.push({
@@ -6536,10 +6937,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
6536
6937
  import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
6537
6938
  var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
6538
6939
  var COMPONENT_PROPS_INVALID = "component-props-invalid";
6539
- function isRec16(v) {
6940
+ function isRec17(v) {
6540
6941
  return !!v && typeof v === "object" && !Array.isArray(v);
6541
6942
  }
6542
- function strName13(v) {
6943
+ function strName14(v) {
6543
6944
  return typeof v === "string" && v.length > 0 ? v : void 0;
6544
6945
  }
6545
6946
  function asArray30(v) {
@@ -6553,8 +6954,8 @@ var PROPS_SCHEMAS = ComponentPropsMap;
6553
6954
  var DATASOURCE_SUPPLIED_PROP = "object";
6554
6955
  function suppliedByDataSource(issue, component) {
6555
6956
  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;
6957
+ const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
6958
+ return strName14(dataSource?.object) !== void 0;
6558
6959
  }
6559
6960
  function unrecognizedKeysFromUnionArm(issue) {
6560
6961
  if (issue.code !== "invalid_union") return void 0;
@@ -6571,18 +6972,18 @@ function unrecognizedKeysFromUnionArm(issue) {
6571
6972
  }
6572
6973
  function validateComponentProps(stack) {
6573
6974
  const findings = [];
6574
- if (!isRec16(stack)) return findings;
6975
+ if (!isRec17(stack)) return findings;
6575
6976
  const pages = asArray30(stack.pages);
6576
6977
  for (let pi = 0; pi < pages.length; pi++) {
6577
6978
  const page = pages[pi];
6578
- if (!isRec16(page)) continue;
6579
- const pageName = strName13(page.name) ?? `#${pi}`;
6979
+ if (!isRec17(page)) continue;
6980
+ const pageName = strName14(page.name) ?? `#${pi}`;
6580
6981
  for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
6581
- const type = strName13(component.type);
6982
+ const type = strName14(component.type);
6582
6983
  if (!type) continue;
6583
6984
  const schema = PROPS_SCHEMAS[type];
6584
6985
  if (!schema) continue;
6585
- const props = isRec16(component.properties) ? component.properties : void 0;
6986
+ const props = isRec17(component.properties) ? component.properties : void 0;
6586
6987
  if (!props) continue;
6587
6988
  const where = `page "${pageName}" \xB7 ${type}`;
6588
6989
  const base = `${path}.properties`;
@@ -6654,16 +7055,16 @@ function asArray31(v) {
6654
7055
  }
6655
7056
  return [];
6656
7057
  }
6657
- function strName14(v) {
7058
+ function strName15(v) {
6658
7059
  return typeof v === "string" && v.length > 0 ? v : void 0;
6659
7060
  }
6660
7061
  function strList3(v) {
6661
7062
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
6662
7063
  }
6663
- function isRec17(v) {
7064
+ function isRec18(v) {
6664
7065
  return !!v && typeof v === "object" && !Array.isArray(v);
6665
7066
  }
6666
- function distance4(a, b) {
7067
+ function distance5(a, b) {
6667
7068
  const m = a.length;
6668
7069
  const n = b.length;
6669
7070
  if (m === 0) return n;
@@ -6679,11 +7080,11 @@ function distance4(a, b) {
6679
7080
  }
6680
7081
  return prev[n];
6681
7082
  }
6682
- function suggest5(target, known) {
7083
+ function suggest6(target, known) {
6683
7084
  let best;
6684
7085
  let bestScore = Infinity;
6685
7086
  for (const c of known) {
6686
- const d = distance4(target, c);
7087
+ const d = distance5(target, c);
6687
7088
  if (d < bestScore) {
6688
7089
  bestScore = d;
6689
7090
  best = c;
@@ -6699,16 +7100,16 @@ function list2(names) {
6699
7100
  function indexDatasets(stack) {
6700
7101
  const out = /* @__PURE__ */ new Map();
6701
7102
  for (const ds of asArray31(stack.datasets)) {
6702
- const name = strName14(ds.name);
7103
+ const name = strName15(ds.name);
6703
7104
  if (!name) continue;
6704
7105
  const dimensions = /* @__PURE__ */ new Set();
6705
7106
  for (const d of asArray31(ds.dimensions)) {
6706
- const n = strName14(d.name);
7107
+ const n = strName15(d.name);
6707
7108
  if (n) dimensions.add(n);
6708
7109
  }
6709
7110
  const measures = /* @__PURE__ */ new Set();
6710
7111
  for (const m of asArray31(ds.measures)) {
6711
- const n = strName14(m.name);
7112
+ const n = strName15(m.name);
6712
7113
  if (n) measures.add(n);
6713
7114
  }
6714
7115
  out.set(name, { dimensions, measures });
@@ -6731,7 +7132,7 @@ function validateChartBindings(stack) {
6731
7132
  where: binding.where,
6732
7133
  path: `${binding.path}.dataset`,
6733
7134
  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).`
7135
+ hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
6735
7136
  });
6736
7137
  return;
6737
7138
  }
@@ -6743,7 +7144,7 @@ function validateChartBindings(stack) {
6743
7144
  where: binding.where,
6744
7145
  path,
6745
7146
  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.`
7147
+ hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
6747
7148
  });
6748
7149
  };
6749
7150
  const measureRef = (name, path, selected2) => {
@@ -6754,7 +7155,7 @@ function validateChartBindings(stack) {
6754
7155
  where: binding.where,
6755
7156
  path,
6756
7157
  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.`
7158
+ hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
6758
7159
  });
6759
7160
  return;
6760
7161
  }
@@ -6789,26 +7190,26 @@ function validateChartBindings(stack) {
6789
7190
  const reports = asArray31(stack.reports);
6790
7191
  for (let ri = 0; ri < reports.length; ri++) {
6791
7192
  const report = reports[ri];
6792
- if (!isRec17(report)) continue;
6793
- const reportName = strName14(report.name) ?? `#${ri}`;
7193
+ if (!isRec18(report)) continue;
7194
+ const reportName = strName15(report.name) ?? `#${ri}`;
6794
7195
  const checkReportChart = (chart, dataset, values, where, path) => {
6795
- if (!isRec17(chart)) return;
7196
+ if (!isRec18(chart)) return;
6796
7197
  check({
6797
7198
  dataset,
6798
7199
  // `values` is the report's measure SELECTION, not a chart ref; feeding
6799
7200
  // it in lets the yAxis "declared but not selected" check work without
6800
7201
  // reporting the selection itself twice.
6801
7202
  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),
7203
+ xAxis: strName15(chart.xAxis) ? { name: strName15(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
7204
+ yAxis: strName15(chart.yAxis) ? { name: strName15(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
7205
+ series: asArray31(chart.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
6805
7206
  where,
6806
7207
  path: `${path}.chart`
6807
7208
  });
6808
7209
  };
6809
7210
  checkReportChart(
6810
7211
  report.chart,
6811
- strName14(report.dataset),
7212
+ strName15(report.dataset),
6812
7213
  strList3(report.values),
6813
7214
  `report "${reportName}" \xB7 chart`,
6814
7215
  `reports[${ri}]`
@@ -6816,22 +7217,22 @@ function validateChartBindings(stack) {
6816
7217
  const blocks = Array.isArray(report.blocks) ? report.blocks : [];
6817
7218
  for (let bi = 0; bi < blocks.length; bi++) {
6818
7219
  const block = blocks[bi];
6819
- if (!isRec17(block)) continue;
7220
+ if (!isRec18(block)) continue;
6820
7221
  checkReportChart(
6821
7222
  block.chart,
6822
- strName14(block.dataset),
7223
+ strName15(block.dataset),
6823
7224
  strList3(block.values),
6824
- `report "${reportName}" \xB7 block "${strName14(block.name) ?? `#${bi}`}" chart`,
7225
+ `report "${reportName}" \xB7 block "${strName15(block.name) ?? `#${bi}`}" chart`,
6825
7226
  `reports[${ri}].blocks[${bi}]`
6826
7227
  );
6827
7228
  }
6828
7229
  }
6829
7230
  const checkListChart = (container, where, path) => {
6830
- if (!isRec17(container)) return;
7231
+ if (!isRec18(container)) return;
6831
7232
  const chart = container.chart;
6832
- if (!isRec17(chart)) return;
7233
+ if (!isRec18(chart)) return;
6833
7234
  check({
6834
- dataset: strName14(chart.dataset),
7235
+ dataset: strName15(chart.dataset),
6835
7236
  dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
6836
7237
  values: { names: strList3(chart.values), path: `${path}.chart.values` },
6837
7238
  where,
@@ -6841,10 +7242,10 @@ function validateChartBindings(stack) {
6841
7242
  const views = asArray31(stack.views);
6842
7243
  for (let vi = 0; vi < views.length; vi++) {
6843
7244
  const view = views[vi];
6844
- if (!isRec17(view)) continue;
6845
- const viewName = strName14(view.name) ?? strName14(view.objectName) ?? `#${vi}`;
7245
+ if (!isRec18(view)) continue;
7246
+ const viewName = strName15(view.name) ?? strName15(view.objectName) ?? `#${vi}`;
6846
7247
  checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
6847
- if (isRec17(view.listViews)) {
7248
+ if (isRec18(view.listViews)) {
6848
7249
  for (const [key, lv] of Object.entries(view.listViews)) {
6849
7250
  checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
6850
7251
  }
@@ -6853,8 +7254,8 @@ function validateChartBindings(stack) {
6853
7254
  const objects = asArray31(stack.objects);
6854
7255
  for (let oi = 0; oi < objects.length; oi++) {
6855
7256
  const obj = objects[oi];
6856
- if (!isRec17(obj) || !isRec17(obj.listViews)) continue;
6857
- const objName = strName14(obj.name) ?? `#${oi}`;
7257
+ if (!isRec18(obj) || !isRec18(obj.listViews)) continue;
7258
+ const objName = strName15(obj.name) ?? `#${oi}`;
6858
7259
  for (const [key, lv] of Object.entries(obj.listViews)) {
6859
7260
  checkListChart(
6860
7261
  lv,
@@ -6866,19 +7267,19 @@ function validateChartBindings(stack) {
6866
7267
  const pages = asArray31(stack.pages);
6867
7268
  for (let pi = 0; pi < pages.length; pi++) {
6868
7269
  const page = pages[pi];
6869
- if (!isRec17(page)) continue;
6870
- const pageName = strName14(page.name) ?? `#${pi}`;
7270
+ if (!isRec18(page)) continue;
7271
+ const pageName = strName15(page.name) ?? `#${pi}`;
6871
7272
  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);
7273
+ const props = isRec18(component.properties) ? component.properties : void 0;
7274
+ if (!props || !strName15(props.dataset)) continue;
7275
+ const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName15(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
7276
+ const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
6876
7277
  check({
6877
- dataset: strName14(props.dataset),
7278
+ dataset: strName15(props.dataset),
6878
7279
  dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
6879
7280
  values: { names: strList3(props.values), path: `${path}.properties.values` },
6880
7281
  series: [...axisRefs, ...seriesRefs],
6881
- where: `page "${pageName}" \xB7 ${strName14(component.type) ?? "chart"}`,
7282
+ where: `page "${pageName}" \xB7 ${strName15(component.type) ?? "chart"}`,
6882
7283
  path: `${path}.properties`
6883
7284
  });
6884
7285
  }
@@ -6891,11 +7292,11 @@ import { createRequire as createRequire4 } from "module";
6891
7292
  var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
6892
7293
  var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
6893
7294
  var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
6894
- var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7295
+ var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6895
7296
  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 }));
7297
+ if (Array.isArray(v)) return v.filter(isRec19);
7298
+ if (isRec19(v)) {
7299
+ return Object.entries(v).filter(([, def]) => isRec19(def)).map(([name, def]) => ({ name, ...def }));
6899
7300
  }
6900
7301
  return [];
6901
7302
  }
@@ -6912,7 +7313,7 @@ function loadAjv() {
6912
7313
  `@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
7314
  );
6914
7315
  }
6915
- const ctor = isRec18(mod) && "default" in mod ? mod.default : mod;
7316
+ const ctor = isRec19(mod) && "default" in mod ? mod.default : mod;
6916
7317
  cachedAjv = ctor;
6917
7318
  return ctor;
6918
7319
  }
@@ -6927,7 +7328,7 @@ function loadAddFormats() {
6927
7328
  `@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
7329
  );
6929
7330
  }
6930
- const plugin = isRec18(mod) && "default" in mod ? mod.default : mod;
7331
+ const plugin = isRec19(mod) && "default" in mod ? mod.default : mod;
6931
7332
  cachedAddFormats = plugin;
6932
7333
  return plugin;
6933
7334
  }
@@ -6957,13 +7358,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
6957
7358
  if (depth >= MAX_RULE_NESTING_DEPTH) return out;
6958
7359
  for (const branch of ["then", "otherwise"]) {
6959
7360
  const nested = rule[branch];
6960
- if (isRec18(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
7361
+ if (isRec19(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
6961
7362
  }
6962
7363
  return out;
6963
7364
  }
6964
7365
  function walkObjectValidationRules(stack) {
6965
7366
  const walked = [];
6966
- if (!isRec18(stack)) return walked;
7367
+ if (!isRec19(stack)) return walked;
6967
7368
  for (const obj of asArray32(stack.objects)) {
6968
7369
  const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
6969
7370
  const validations = obj.validations;
@@ -6998,7 +7399,7 @@ function validateRuleCompilability(stack) {
6998
7399
  });
6999
7400
  }
7000
7401
  }
7001
- if (rule.type === "json_schema" && isRec18(rule.schema)) {
7402
+ if (rule.type === "json_schema" && isRec19(rule.schema)) {
7002
7403
  try {
7003
7404
  createRuntimeAjv().compile(rule.schema);
7004
7405
  } catch (err) {
@@ -7018,7 +7419,7 @@ function validateRuleCompilability(stack) {
7018
7419
 
7019
7420
  // src/validate-rule-schema-formats.ts
7020
7421
  var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
7021
- var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7422
+ var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7022
7423
  var SUBSCHEMA_KEYS = [
7023
7424
  "additionalItems",
7024
7425
  "additionalProperties",
@@ -7042,7 +7443,7 @@ var SUBSCHEMA_MAP_KEYS = [
7042
7443
  var MAX_SCHEMA_WALK_DEPTH = 32;
7043
7444
  var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
7044
7445
  function collectFormatUses(schema, pointer, out, depth) {
7045
- if (!isRec19(schema)) return;
7446
+ if (!isRec20(schema)) return;
7046
7447
  if (typeof schema.format === "string") {
7047
7448
  out.push({ pointer: `${pointer}/format`, name: schema.format });
7048
7449
  }
@@ -7061,7 +7462,7 @@ function collectFormatUses(schema, pointer, out, depth) {
7061
7462
  }
7062
7463
  for (const key of SUBSCHEMA_MAP_KEYS) {
7063
7464
  const value = schema[key];
7064
- if (!isRec19(value)) continue;
7465
+ if (!isRec20(value)) continue;
7065
7466
  for (const [name, entry] of Object.entries(value)) {
7066
7467
  collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
7067
7468
  }
@@ -7069,13 +7470,13 @@ function collectFormatUses(schema, pointer, out, depth) {
7069
7470
  const items = schema.items;
7070
7471
  if (Array.isArray(items)) {
7071
7472
  items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
7072
- } else if (isRec19(items)) {
7473
+ } else if (isRec20(items)) {
7073
7474
  collectFormatUses(items, `${pointer}/items`, out, depth + 1);
7074
7475
  }
7075
7476
  const dependencies = schema.dependencies;
7076
- if (isRec19(dependencies)) {
7477
+ if (isRec20(dependencies)) {
7077
7478
  for (const [name, entry] of Object.entries(dependencies)) {
7078
- if (!isRec19(entry)) continue;
7479
+ if (!isRec20(entry)) continue;
7079
7480
  collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
7080
7481
  }
7081
7482
  }
@@ -7102,9 +7503,9 @@ function nearestRegisteredFormat(name, registered) {
7102
7503
  let best = null;
7103
7504
  let bestDistance = Number.POSITIVE_INFINITY;
7104
7505
  for (const candidate of [...registered].sort()) {
7105
- const distance7 = editDistance2(authored, candidate);
7106
- if (distance7 < bestDistance) {
7107
- bestDistance = distance7;
7506
+ const distance8 = editDistance2(authored, candidate);
7507
+ if (distance8 < bestDistance) {
7508
+ bestDistance = distance8;
7108
7509
  best = candidate;
7109
7510
  }
7110
7511
  }
@@ -7114,7 +7515,7 @@ function validateRuleSchemaFormats(stack) {
7114
7515
  const findings = [];
7115
7516
  const pending = [];
7116
7517
  for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
7117
- if (rule.type !== "json_schema" || !isRec19(rule.schema)) continue;
7518
+ if (rule.type !== "json_schema" || !isRec20(rule.schema)) continue;
7118
7519
  const uses = [];
7119
7520
  collectFormatUses(rule.schema, "", uses, 0);
7120
7521
  for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
@@ -7239,7 +7640,7 @@ function asArray34(v) {
7239
7640
  }
7240
7641
  return [];
7241
7642
  }
7242
- function strName15(v) {
7643
+ function strName16(v) {
7243
7644
  return typeof v === "string" && v.length > 0 ? v : void 0;
7244
7645
  }
7245
7646
  function collectNavExposures(stack) {
@@ -7248,18 +7649,18 @@ function collectNavExposures(stack) {
7248
7649
  for (let ai = 0; ai < apps.length; ai++) {
7249
7650
  const app = apps[ai];
7250
7651
  if (!app || typeof app !== "object") continue;
7251
- const appName = strName15(app.name) ?? `#${ai}`;
7652
+ const appName = strName16(app.name) ?? `#${ai}`;
7252
7653
  const walk = (items, basePath) => {
7253
7654
  const navItems = asArray34(items);
7254
7655
  for (let ni = 0; ni < navItems.length; ni++) {
7255
7656
  const nav = navItems[ni];
7256
7657
  if (!nav || typeof nav !== "object") continue;
7257
7658
  const navPath = `${basePath}[${ni}]`;
7258
- const objectName = strName15(nav.objectName);
7659
+ const objectName = strName16(nav.objectName);
7259
7660
  if (nav.type === "object" && objectName) {
7260
7661
  out.push({
7261
7662
  objectName,
7262
- where: `app "${appName}" \xB7 nav "${strName15(nav.id) ?? `#${ni}`}"`,
7663
+ where: `app "${appName}" \xB7 nav "${strName16(nav.id) ?? `#${ni}`}"`,
7263
7664
  path: `${navPath}.objectName`
7264
7665
  });
7265
7666
  }
@@ -7283,7 +7684,7 @@ function validateNavAccess(stack) {
7283
7684
  if (exposures.length === 0) return findings;
7284
7685
  const ownObjects = /* @__PURE__ */ new Set();
7285
7686
  for (const obj of asArray34(stack.objects)) {
7286
- const n = strName15(obj.name);
7687
+ const n = strName16(obj.name);
7287
7688
  if (n) ownObjects.add(n);
7288
7689
  }
7289
7690
  const readable = /* @__PURE__ */ new Set();
@@ -7316,18 +7717,18 @@ import { expandViewContainer } from "@objectstack/spec";
7316
7717
  import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
7317
7718
  var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
7318
7719
  var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
7319
- function isRec20(v) {
7720
+ function isRec21(v) {
7320
7721
  return !!v && typeof v === "object" && !Array.isArray(v);
7321
7722
  }
7322
7723
  function asArray35(v) {
7323
7724
  if (Array.isArray(v)) return v;
7324
- if (isRec20(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec20(def) ? def : {} }));
7725
+ if (isRec21(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec21(def) ? def : {} }));
7325
7726
  return [];
7326
7727
  }
7327
- function strName16(v) {
7728
+ function strName17(v) {
7328
7729
  return typeof v === "string" && v.length > 0 ? v : void 0;
7329
7730
  }
7330
- function distance5(a, b) {
7731
+ function distance6(a, b) {
7331
7732
  const m = a.length;
7332
7733
  const n = b.length;
7333
7734
  if (m === 0) return n;
@@ -7343,7 +7744,7 @@ function distance5(a, b) {
7343
7744
  }
7344
7745
  return prev[n];
7345
7746
  }
7346
- function suggest6(target, known) {
7747
+ function suggest7(target, known) {
7347
7748
  const names = [...known];
7348
7749
  const segmentMatch = names.find(
7349
7750
  (candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
@@ -7352,7 +7753,7 @@ function suggest6(target, known) {
7352
7753
  let best;
7353
7754
  let bestScore = Infinity;
7354
7755
  for (const candidate of names) {
7355
- const d = distance5(target, candidate);
7756
+ const d = distance6(target, candidate);
7356
7757
  if (d < bestScore) {
7357
7758
  bestScore = d;
7358
7759
  best = candidate;
@@ -7385,33 +7786,33 @@ function collectViewRecord(view, factsFor) {
7385
7786
  const addSections = (container, binding) => {
7386
7787
  if (!binding) return;
7387
7788
  for (const section of asArray35(container.sections)) {
7388
- const sectionName = strName16(section.name);
7789
+ const sectionName = strName17(section.name);
7389
7790
  if (sectionName) factsFor(binding).sections.add(sectionName);
7390
7791
  }
7391
7792
  };
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));
7793
+ const listBinding = isRec21(view.list) ? bindingOf(view.list) : void 0;
7794
+ if (isRec21(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
7795
+ addView(recordObject ?? listBinding, strName17(view.name));
7395
7796
  const named = namedViewKeys(view);
7396
7797
  for (const family of ["listViews", "formViews"]) {
7397
7798
  const container = view[family];
7398
- if (!isRec20(container)) continue;
7799
+ if (!isRec21(container)) continue;
7399
7800
  const registryKeys = family === "listViews" ? named.list : named.form;
7400
7801
  let at = 0;
7401
7802
  for (const sub of Object.values(container)) {
7402
7803
  if (!sub || typeof sub !== "object") continue;
7403
7804
  const registryKey = registryKeys[at++];
7404
- if (!isRec20(sub)) continue;
7805
+ if (!isRec21(sub)) continue;
7405
7806
  const binding = bindingOf(sub) ?? listBinding;
7406
7807
  addView(binding, registryKey);
7407
7808
  addSections(sub, binding);
7408
7809
  }
7409
7810
  }
7410
- if (isRec20(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
7811
+ if (isRec21(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
7411
7812
  addSections(view, recordObject ?? listBinding);
7412
7813
  }
7413
7814
  function defaultListViewKey(object, container) {
7414
- if (!object || !isRec20(container.list)) return void 0;
7815
+ if (!object || !isRec21(container.list)) return void 0;
7415
7816
  const item = expandViewContainer(object, container).find(
7416
7817
  (i) => i.viewKind === "list" && i.isDefault
7417
7818
  );
@@ -7423,7 +7824,7 @@ function namedViewKeys(container) {
7423
7824
  const object = "probe";
7424
7825
  const prefix = `${object}.`;
7425
7826
  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;
7827
+ const countEntries = (v) => isRec21(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
7427
7828
  const listCount = countEntries(container.listViews);
7428
7829
  const formCount = countEntries(container.formViews);
7429
7830
  if (!listCount && !formCount) return { list: [], form: [] };
@@ -7441,14 +7842,14 @@ function readOptions(field) {
7441
7842
  values.add(opt);
7442
7843
  continue;
7443
7844
  }
7444
- if (!isRec20(opt)) continue;
7445
- const value = strName16(opt.value);
7845
+ if (!isRec21(opt)) continue;
7846
+ const value = strName17(opt.value);
7446
7847
  if (!value) continue;
7447
7848
  values.add(value);
7448
- const label2 = strName16(opt.label);
7849
+ const label2 = strName17(opt.label);
7449
7850
  if (label2) byLabel.set(label2.toLowerCase(), value);
7450
7851
  }
7451
- } else if (isRec20(raw)) {
7852
+ } else if (isRec21(raw)) {
7452
7853
  for (const [value, label2] of Object.entries(raw)) {
7453
7854
  values.add(value);
7454
7855
  if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
@@ -7469,23 +7870,23 @@ function buildUniverse(stack) {
7469
7870
  return facts;
7470
7871
  };
7471
7872
  for (const obj of asArray35(stack.objects)) {
7472
- const objectName = strName16(obj.name);
7873
+ const objectName = strName17(obj.name);
7473
7874
  if (!objectName) continue;
7474
7875
  const facts = factsFor(objectName);
7475
7876
  for (const field of asArray35(obj.fields)) {
7476
- const fieldName = strName16(field.name);
7877
+ const fieldName = strName17(field.name);
7477
7878
  if (fieldName) facts.fields.set(fieldName, field);
7478
7879
  }
7479
7880
  for (const action of asArray35(obj.actions)) {
7480
- const actionName = strName16(action.name);
7881
+ const actionName = strName17(action.name);
7481
7882
  if (actionName) facts.actions.set(actionName, action);
7482
7883
  }
7483
7884
  for (const view of asArray35(obj.views)) {
7484
- collectViewRecord({ ...view, object: strName16(view.object) ?? objectName }, factsFor);
7885
+ collectViewRecord({ ...view, object: strName17(view.object) ?? objectName }, factsFor);
7485
7886
  }
7486
7887
  collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
7487
7888
  for (const group of asArray35(obj.fieldGroups)) {
7488
- const key = strName16(group.key) ?? strName16(group.name);
7889
+ const key = strName17(group.key) ?? strName17(group.name);
7489
7890
  if (key) facts.sections.add(key);
7490
7891
  }
7491
7892
  }
@@ -7496,10 +7897,10 @@ function buildUniverse(stack) {
7496
7897
  for (let pi = 0; pi < pages.length; pi++) {
7497
7898
  for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
7498
7899
  if (!walked.objectName) continue;
7499
- const props = isRec20(walked.component.properties) ? walked.component.properties : void 0;
7900
+ const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
7500
7901
  if (!props) continue;
7501
7902
  for (const section of asArray35(props.sections)) {
7502
- const sectionName = strName16(section.name);
7903
+ const sectionName = strName17(section.name);
7503
7904
  if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
7504
7905
  }
7505
7906
  }
@@ -7507,9 +7908,9 @@ function buildUniverse(stack) {
7507
7908
  const globalActions = /* @__PURE__ */ new Map();
7508
7909
  const actionOwners = /* @__PURE__ */ new Map();
7509
7910
  for (const action of asArray35(stack.actions)) {
7510
- const actionName = strName16(action.name);
7911
+ const actionName = strName17(action.name);
7511
7912
  if (!actionName) continue;
7512
- const owner = strName16(action.objectName) ?? strName16(action.object);
7913
+ const owner = strName17(action.objectName) ?? strName17(action.object);
7513
7914
  if (owner) {
7514
7915
  factsFor(owner).actions.set(actionName, action);
7515
7916
  actionOwners.set(actionName, owner);
@@ -7524,19 +7925,19 @@ function buildUniverse(stack) {
7524
7925
  }
7525
7926
  const apps = /* @__PURE__ */ new Map();
7526
7927
  for (const app of asArray35(stack.apps)) {
7527
- const appName = strName16(app.name);
7928
+ const appName = strName17(app.name);
7528
7929
  if (!appName) continue;
7529
7930
  const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
7530
7931
  const walkNav = (items) => {
7531
7932
  for (const item of asArray35(items)) {
7532
- const id = strName16(item.id);
7933
+ const id = strName17(item.id);
7533
7934
  if (id) navIds.add(id);
7534
7935
  if (item.children) walkNav(item.children);
7535
7936
  }
7536
7937
  };
7537
7938
  walkNav(app.navigation);
7538
7939
  for (const area of asArray35(app.areas)) {
7539
- const areaId = strName16(area.id);
7940
+ const areaId = strName17(area.id);
7540
7941
  if (areaId) navIds.add(areaId);
7541
7942
  walkNav(area.navigation);
7542
7943
  }
@@ -7544,20 +7945,20 @@ function buildUniverse(stack) {
7544
7945
  }
7545
7946
  const dashboards = /* @__PURE__ */ new Map();
7546
7947
  for (const dash of asArray35(stack.dashboards)) {
7547
- const dashName = strName16(dash.name);
7948
+ const dashName = strName17(dash.name);
7548
7949
  if (!dashName) continue;
7549
7950
  const widgets = /* @__PURE__ */ new Set();
7550
7951
  for (const widget of asArray35(dash.widgets)) {
7551
- const id = strName16(widget.id) ?? strName16(widget.name);
7952
+ const id = strName17(widget.id) ?? strName17(widget.name);
7552
7953
  if (id) widgets.add(id);
7553
7954
  }
7554
7955
  const actions = /* @__PURE__ */ new Set();
7555
7956
  const headerActions = [
7556
- ...asArray35(isRec20(dash.header) ? dash.header.actions : void 0),
7957
+ ...asArray35(isRec21(dash.header) ? dash.header.actions : void 0),
7557
7958
  ...asArray35(dash.actions)
7558
7959
  ];
7559
7960
  for (const action of headerActions) {
7560
- const key = strName16(action.actionUrl) ?? strName16(action.url) ?? strName16(action.name);
7961
+ const key = strName17(action.actionUrl) ?? strName17(action.url) ?? strName17(action.name);
7561
7962
  if (key) actions.add(key);
7562
7963
  }
7563
7964
  dashboards.set(dashName, { widgets, actions });
@@ -7569,7 +7970,7 @@ function localePath(bundleIndex, locale) {
7569
7970
  }
7570
7971
  function validateTranslationReferences(stack) {
7571
7972
  const findings = [];
7572
- if (!isRec20(stack)) return findings;
7973
+ if (!isRec21(stack)) return findings;
7573
7974
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
7574
7975
  if (bundles.length === 0) return findings;
7575
7976
  const universe = buildUniverse(stack);
@@ -7578,13 +7979,13 @@ function validateTranslationReferences(stack) {
7578
7979
  };
7579
7980
  for (let bi = 0; bi < bundles.length; bi++) {
7580
7981
  const bundle = bundles[bi];
7581
- if (!isRec20(bundle)) continue;
7982
+ if (!isRec21(bundle)) continue;
7582
7983
  for (const [locale, rawData] of Object.entries(bundle)) {
7583
- if (!isRec20(rawData)) continue;
7984
+ if (!isRec21(rawData)) continue;
7584
7985
  const base = localePath(bi, locale);
7585
7986
  const inLocale = `locale "${locale}"`;
7586
7987
  for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
7587
- if (!isRec20(rawNode)) continue;
7988
+ if (!isRec21(rawNode)) continue;
7588
7989
  const objPath = `${base}.objects.${objectName}`;
7589
7990
  const facts = universe.objects.get(objectName);
7590
7991
  if (!facts) {
@@ -7592,7 +7993,7 @@ function validateTranslationReferences(stack) {
7592
7993
  orphan(
7593
7994
  `${inLocale} \xB7 object "${objectName}"`,
7594
7995
  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()),
7996
+ 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
7997
  `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
7998
  );
7598
7999
  continue;
@@ -7605,12 +8006,12 @@ function validateTranslationReferences(stack) {
7605
8006
  orphan(
7606
8007
  `${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
7607
8008
  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()),
8009
+ `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
8010
  `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
8011
  );
7611
8012
  continue;
7612
8013
  }
7613
- if (!isRec20(rawField)) continue;
8014
+ if (!isRec21(rawField)) continue;
7614
8015
  checkOptionKeys(findings, {
7615
8016
  optionMap: rawField.options,
7616
8017
  field,
@@ -7625,7 +8026,7 @@ function validateTranslationReferences(stack) {
7625
8026
  orphan(
7626
8027
  `${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
7627
8028
  `${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),
8029
+ `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
8030
  `Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
7630
8031
  );
7631
8032
  }
@@ -7634,7 +8035,7 @@ function validateTranslationReferences(stack) {
7634
8035
  orphan(
7635
8036
  `${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
7636
8037
  `${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),
8038
+ `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
8039
  `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
8040
  );
7640
8041
  }
@@ -7645,7 +8046,7 @@ function validateTranslationReferences(stack) {
7645
8046
  orphan(
7646
8047
  `${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
7647
8048
  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()),
8049
+ `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
8050
  `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
8051
  );
7651
8052
  continue;
@@ -7667,7 +8068,7 @@ function validateTranslationReferences(stack) {
7667
8068
  orphan(
7668
8069
  `${inLocale} \xB7 global action "${actionName}"`,
7669
8070
  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()),
8071
+ 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
8072
  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
8073
  );
7673
8074
  continue;
@@ -7687,18 +8088,18 @@ function validateTranslationReferences(stack) {
7687
8088
  orphan(
7688
8089
  `${inLocale} \xB7 app "${appName}"`,
7689
8090
  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()),
8091
+ `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
8092
  `Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
7692
8093
  );
7693
8094
  continue;
7694
8095
  }
7695
- if (!isRec20(rawApp)) continue;
8096
+ if (!isRec21(rawApp)) continue;
7696
8097
  for (const navId of Object.keys(asRecord(rawApp.navigation))) {
7697
8098
  if (navIds.has(navId)) continue;
7698
8099
  orphan(
7699
8100
  `${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
7700
8101
  `${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),
8102
+ `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
8103
  `Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
7703
8104
  );
7704
8105
  }
@@ -7710,18 +8111,18 @@ function validateTranslationReferences(stack) {
7710
8111
  orphan(
7711
8112
  `${inLocale} \xB7 dashboard "${dashName}"`,
7712
8113
  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()),
8114
+ `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
8115
  `Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
7715
8116
  );
7716
8117
  continue;
7717
8118
  }
7718
- if (!isRec20(rawDash)) continue;
8119
+ if (!isRec21(rawDash)) continue;
7719
8120
  for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
7720
8121
  if (dash.widgets.has(widgetId)) continue;
7721
8122
  orphan(
7722
8123
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
7723
8124
  `${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),
8125
+ `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
8126
  `Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
7726
8127
  );
7727
8128
  }
@@ -7730,7 +8131,7 @@ function validateTranslationReferences(stack) {
7730
8131
  orphan(
7731
8132
  `${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
7732
8133
  `${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),
8134
+ `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
8135
  `Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
7735
8136
  );
7736
8137
  }
@@ -7740,7 +8141,7 @@ function validateTranslationReferences(stack) {
7740
8141
  return findings;
7741
8142
  }
7742
8143
  function asRecord(v) {
7743
- return isRec20(v) ? v : {};
8144
+ return isRec21(v) ? v : {};
7744
8145
  }
7745
8146
  function checkOptionKeys(findings, ctx) {
7746
8147
  const optionKeys = Object.keys(asRecord(ctx.optionMap));
@@ -7752,7 +8153,7 @@ function checkOptionKeys(findings, ctx) {
7752
8153
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
7753
8154
  where: ctx.where,
7754
8155
  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.`,
8156
+ 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
8157
  hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
7757
8158
  });
7758
8159
  return;
@@ -7765,17 +8166,17 @@ function checkOptionKeys(findings, ctx) {
7765
8166
  rule: TRANSLATION_OPTION_KEY_UNKNOWN,
7766
8167
  where: ctx.where,
7767
8168
  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),
8169
+ 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
8170
  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
8171
  });
7771
8172
  }
7772
8173
  }
7773
8174
  function checkActionParams(findings, ctx) {
7774
- const rawParams = Object.keys(asRecord(isRec20(ctx.rawAction) ? ctx.rawAction.params : void 0));
8175
+ const rawParams = Object.keys(asRecord(isRec21(ctx.rawAction) ? ctx.rawAction.params : void 0));
7775
8176
  if (rawParams.length === 0) return;
7776
8177
  const declared = /* @__PURE__ */ new Set();
7777
8178
  for (const param of asArray35(ctx.action.params)) {
7778
- const name = strName16(param.name) ?? strName16(param.field);
8179
+ const name = strName17(param.name) ?? strName17(param.field);
7779
8180
  if (name) declared.add(name);
7780
8181
  }
7781
8182
  for (const paramName of rawParams) {
@@ -7785,7 +8186,7 @@ function checkActionParams(findings, ctx) {
7785
8186
  rule: TRANSLATION_TARGET_UNKNOWN,
7786
8187
  where: `${ctx.where} \xB7 param "${paramName}"`,
7787
8188
  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),
8189
+ 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
8190
  hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
7790
8191
  });
7791
8192
  }
@@ -7793,14 +8194,14 @@ function checkActionParams(findings, ctx) {
7793
8194
 
7794
8195
  // src/validate-translatable-sections.ts
7795
8196
  var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
7796
- function isRec21(v) {
8197
+ function isRec22(v) {
7797
8198
  return !!v && typeof v === "object" && !Array.isArray(v);
7798
8199
  }
7799
- function strName17(v) {
8200
+ function strName18(v) {
7800
8201
  return typeof v === "string" && v.length > 0 ? v : void 0;
7801
8202
  }
7802
8203
  function viewLabel(view) {
7803
- const name = strName17(view.name);
8204
+ const name = strName18(view.name);
7804
8205
  return name ? `view "${name}"` : "";
7805
8206
  }
7806
8207
  function joinWhere(...parts) {
@@ -7808,7 +8209,7 @@ function joinWhere(...parts) {
7808
8209
  }
7809
8210
  function collectViewSites(view, basePath, label2, sites) {
7810
8211
  const recordObject = viewObjectName(view);
7811
- const listBinding = isRec21(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
8212
+ const listBinding = isRec22(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
7812
8213
  for (const site of viewContainerSites(view, basePath)) {
7813
8214
  sites.push({
7814
8215
  path: `${site.path}.sections`,
@@ -7822,11 +8223,11 @@ function translatedObjectNames(stack) {
7822
8223
  const out = /* @__PURE__ */ new Set();
7823
8224
  const bundles = Array.isArray(stack.translations) ? stack.translations : [];
7824
8225
  for (const bundle of bundles) {
7825
- if (!isRec21(bundle)) continue;
8226
+ if (!isRec22(bundle)) continue;
7826
8227
  for (const data of Object.values(bundle)) {
7827
- if (!isRec21(data) || !isRec21(data.objects)) continue;
8228
+ if (!isRec22(data) || !isRec22(data.objects)) continue;
7828
8229
  for (const [objectName, node] of Object.entries(data.objects)) {
7829
- if (isRec21(node)) out.add(objectName);
8230
+ if (isRec22(node)) out.add(objectName);
7830
8231
  }
7831
8232
  }
7832
8233
  }
@@ -7838,22 +8239,22 @@ function suggestedName(label2) {
7838
8239
  }
7839
8240
  function validateTranslatableSections(stack) {
7840
8241
  const findings = [];
7841
- if (!isRec21(stack)) return findings;
8242
+ if (!isRec22(stack)) return findings;
7842
8243
  const translated = translatedObjectNames(stack);
7843
8244
  if (translated.size === 0) return findings;
7844
8245
  const sites = [];
7845
8246
  for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
7846
- const objectName = strName17(obj.name);
8247
+ const objectName = strName18(obj.name);
7847
8248
  if (!objectName) continue;
7848
8249
  for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
7849
8250
  collectViewSites(
7850
- { ...view, object: strName17(view.object) ?? objectName },
8251
+ { ...view, object: strName18(view.object) ?? objectName },
7851
8252
  path,
7852
8253
  viewLabel(view),
7853
8254
  sites
7854
8255
  );
7855
8256
  }
7856
- if (isRec21(obj.listViews)) {
8257
+ if (isRec22(obj.listViews)) {
7857
8258
  collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
7858
8259
  }
7859
8260
  }
@@ -7861,13 +8262,13 @@ function validateTranslatableSections(stack) {
7861
8262
  collectViewSites(view, path, viewLabel(view), sites);
7862
8263
  }
7863
8264
  for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
7864
- const pageName = strName17(page.name);
8265
+ const pageName = strName18(page.name);
7865
8266
  const pageLabel = pageName ? `page "${pageName}"` : "";
7866
8267
  for (const walked of walkPageComponents(page, pagePath)) {
7867
8268
  if (!walked.objectName) continue;
7868
- const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
8269
+ const props = isRec22(walked.component.properties) ? walked.component.properties : void 0;
7869
8270
  if (!props) continue;
7870
- const type = strName17(walked.component.type) ?? "component";
8271
+ const type = strName18(walked.component.type) ?? "component";
7871
8272
  sites.push({
7872
8273
  path: `${walked.path}.properties.sections`,
7873
8274
  surface: joinWhere(pageLabel, type),
@@ -7882,9 +8283,9 @@ function validateTranslatableSections(stack) {
7882
8283
  if (!Array.isArray(site.sections)) continue;
7883
8284
  for (let i = 0; i < site.sections.length; i++) {
7884
8285
  const section = site.sections[i];
7885
- if (!isRec21(section)) continue;
7886
- if (strName17(section.name)) continue;
7887
- const heading = strName17(section.label);
8286
+ if (!isRec22(section)) continue;
8287
+ if (strName18(section.name)) continue;
8288
+ const heading = strName18(section.label);
7888
8289
  if (!heading) continue;
7889
8290
  const slug = suggestedName(heading);
7890
8291
  findings.push({
@@ -7909,7 +8310,7 @@ function asArray36(v) {
7909
8310
  }
7910
8311
  return [];
7911
8312
  }
7912
- function strName18(v) {
8313
+ function strName19(v) {
7913
8314
  return typeof v === "string" && v.length > 0 ? v : void 0;
7914
8315
  }
7915
8316
  function surfaceOf(v) {
@@ -7920,17 +8321,17 @@ function validateAiSurfaceAffinity(stack) {
7920
8321
  if (!stack || typeof stack !== "object") return findings;
7921
8322
  const skillsByName = /* @__PURE__ */ new Map();
7922
8323
  for (const skill of asArray36(stack.skills)) {
7923
- const n = strName18(skill.name);
8324
+ const n = strName19(skill.name);
7924
8325
  if (n) skillsByName.set(n, skill);
7925
8326
  }
7926
8327
  const agents = asArray36(stack.agents);
7927
8328
  for (let ai = 0; ai < agents.length; ai++) {
7928
8329
  const agent = agents[ai];
7929
- const agentName = strName18(agent.name) ?? `#${ai}`;
8330
+ const agentName = strName19(agent.name) ?? `#${ai}`;
7930
8331
  const agentSurface = surfaceOf(agent.surface);
7931
8332
  const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
7932
8333
  for (let si = 0; si < skillRefs.length; si++) {
7933
- const ref = strName18(skillRefs[si]);
8334
+ const ref = strName19(skillRefs[si]);
7934
8335
  if (!ref) continue;
7935
8336
  const skill = skillsByName.get(ref);
7936
8337
  if (!skill) continue;
@@ -7959,10 +8360,10 @@ function asArray37(v) {
7959
8360
  }
7960
8361
  return [];
7961
8362
  }
7962
- function strName19(v) {
8363
+ function strName20(v) {
7963
8364
  return typeof v === "string" && v.length > 0 ? v : void 0;
7964
8365
  }
7965
- function distance6(a, b) {
8366
+ function distance7(a, b) {
7966
8367
  const m = a.length;
7967
8368
  const n = b.length;
7968
8369
  if (m === 0) return n;
@@ -7978,14 +8379,14 @@ function distance6(a, b) {
7978
8379
  }
7979
8380
  return prev[n];
7980
8381
  }
7981
- function suggest7(target, known) {
8382
+ function suggest8(target, known) {
7982
8383
  for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
7983
8384
  if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
7984
8385
  }
7985
8386
  let best;
7986
8387
  let bestScore = Infinity;
7987
8388
  for (const candidate of known) {
7988
- const d = distance6(target, candidate);
8389
+ const d = distance7(target, candidate);
7989
8390
  if (d < bestScore) {
7990
8391
  bestScore = d;
7991
8392
  best = candidate;
@@ -8000,8 +8401,8 @@ function materialisesAsTool(action) {
8000
8401
  if (!ai || typeof ai !== "object") return false;
8001
8402
  const aiRec = ai;
8002
8403
  if (aiRec.exposed !== true) return false;
8003
- if (!strName19(aiRec.description)) return false;
8004
- const type = strName19(action.type);
8404
+ if (!strName20(aiRec.description)) return false;
8405
+ const type = strName20(action.type);
8005
8406
  if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
8006
8407
  if (type === "script") return Boolean(action.target || action.body);
8007
8408
  return Boolean(action.target);
@@ -8009,12 +8410,12 @@ function materialisesAsTool(action) {
8009
8410
  function collectToolUniverse(stack) {
8010
8411
  const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
8011
8412
  for (const tool of asArray37(stack.tools)) {
8012
- const n = strName19(tool.name);
8413
+ const n = strName20(tool.name);
8013
8414
  if (n) universe.add(n);
8014
8415
  }
8015
8416
  const addActionFamily = (actions) => {
8016
8417
  for (const action of asArray37(actions)) {
8017
- const n = strName19(action.name);
8418
+ const n = strName20(action.name);
8018
8419
  if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
8019
8420
  }
8020
8421
  };
@@ -8028,7 +8429,7 @@ function collectUnexposedActionNames(stack) {
8028
8429
  const names = /* @__PURE__ */ new Set();
8029
8430
  const scan = (actions) => {
8030
8431
  for (const action of asArray37(actions)) {
8031
- const n = strName19(action.name);
8432
+ const n = strName20(action.name);
8032
8433
  if (n && !materialisesAsTool(action)) names.add(n);
8033
8434
  }
8034
8435
  };
@@ -8054,10 +8455,10 @@ function validateAiToolReferences(stack) {
8054
8455
  const skills = asArray37(stack.skills);
8055
8456
  for (let si = 0; si < skills.length; si++) {
8056
8457
  const skill = skills[si];
8057
- const skillName = strName19(skill.name) ?? `#${si}`;
8458
+ const skillName = strName20(skill.name) ?? `#${si}`;
8058
8459
  const refs = Array.isArray(skill.tools) ? skill.tools : [];
8059
8460
  for (let ti = 0; ti < refs.length; ti++) {
8060
- const ref = strName19(refs[ti]);
8461
+ const ref = strName20(refs[ti]);
8061
8462
  if (!ref || resolves(ref)) continue;
8062
8463
  const isPattern = ref.endsWith("*");
8063
8464
  const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
@@ -8066,7 +8467,7 @@ function validateAiToolReferences(stack) {
8066
8467
  rule: AI_SKILL_TOOL_UNRESOLVED,
8067
8468
  where: `skill "${skillName}" \xB7 tools`,
8068
8469
  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),
8470
+ 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
8471
  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
8472
  });
8072
8473
  }
@@ -8084,7 +8485,7 @@ function asArray38(v) {
8084
8485
  }
8085
8486
  return [];
8086
8487
  }
8087
- function strName20(v) {
8488
+ function strName21(v) {
8088
8489
  return typeof v === "string" && v.length > 0 ? v : void 0;
8089
8490
  }
8090
8491
  var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
@@ -8094,7 +8495,7 @@ function validateAiAgentAuthoring(stack) {
8094
8495
  const agents = asArray38(stack.agents);
8095
8496
  for (let ai = 0; ai < agents.length; ai++) {
8096
8497
  const agent = agents[ai];
8097
- const name = strName20(agent.name) ?? `#${ai}`;
8498
+ const name = strName21(agent.name) ?? `#${ai}`;
8098
8499
  const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
8099
8500
  const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
8100
8501
  findings.push({
@@ -8110,9 +8511,9 @@ function validateAiAgentAuthoring(stack) {
8110
8511
  const apps = asArray38(stack.apps);
8111
8512
  for (let appIdx = 0; appIdx < apps.length; appIdx++) {
8112
8513
  const app = apps[appIdx];
8113
- const defaultAgent = strName20(app.defaultAgent);
8514
+ const defaultAgent = strName21(app.defaultAgent);
8114
8515
  if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
8115
- const appName = strName20(app.name) ?? `#${appIdx}`;
8516
+ const appName = strName21(app.name) ?? `#${appIdx}`;
8116
8517
  findings.push({
8117
8518
  severity: "warning",
8118
8519
  rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
@@ -8142,6 +8543,7 @@ function loadTypeScript3() {
8142
8543
  return cachedTs3;
8143
8544
  }
8144
8545
  var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
8546
+ var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
8145
8547
  var HOOK_BODY_WRITE_PATTERNS = [
8146
8548
  {
8147
8549
  id: "input-property-assign",
@@ -8214,13 +8616,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
8214
8616
  "owner",
8215
8617
  "record_type"
8216
8618
  ]);
8217
- var isRec22 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8619
+ function unprovisionedAnchorWriteConsequence() {
8620
+ 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).`;
8621
+ }
8622
+ var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8218
8623
  function asArray39(v) {
8219
- if (Array.isArray(v)) return v.filter((x) => isRec22(x));
8220
- if (isRec22(v)) {
8624
+ if (Array.isArray(v)) return v.filter((x) => isRec23(x));
8625
+ if (isRec23(v)) {
8221
8626
  return Object.entries(v).map(([name, def]) => ({
8222
8627
  name,
8223
- ...isRec22(def) ? def : {}
8628
+ ...isRec23(def) ? def : {}
8224
8629
  }));
8225
8630
  }
8226
8631
  return [];
@@ -8364,14 +8769,16 @@ function validateHookBodyWrites(stack) {
8364
8769
  const hooks = asArray39(stack.hooks);
8365
8770
  if (hooks.length === 0) return findings;
8366
8771
  let objectFields = null;
8772
+ let anchors = null;
8367
8773
  hooks.forEach((hook, hookIndex) => {
8368
8774
  const body = hook.body;
8369
- if (!isRec22(body) || body.language !== "js") return;
8775
+ if (!isRec23(body) || body.language !== "js") return;
8370
8776
  const source = body.source;
8371
8777
  if (typeof source !== "string" || source.trim() === "") return;
8372
8778
  const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
8373
8779
  if (writes.length === 0) return;
8374
8780
  objectFields ?? (objectFields = indexObjectFields2(stack));
8781
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8375
8782
  const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
8376
8783
  const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
8377
8784
  (o) => typeof o === "string" && o.trim() !== ""
@@ -8386,7 +8793,20 @@ function validateHookBodyWrites(stack) {
8386
8793
  if (reported.has(dedupeKey)) continue;
8387
8794
  if (w.object === void 0) {
8388
8795
  if (!inputJudgeable) continue;
8389
- if (IMPLICIT_FIELDS2.has(w.field)) continue;
8796
+ if (IMPLICIT_FIELDS2.has(w.field)) {
8797
+ if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
8798
+ reported.add(dedupeKey);
8799
+ const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
8800
+ findings.push({
8801
+ severity: "warning",
8802
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
8803
+ where,
8804
+ path,
8805
+ message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
8806
+ hint: unprovisionedAnchorHint(anchorObj, w.field)
8807
+ });
8808
+ continue;
8809
+ }
8390
8810
  if (targetSets.some((s) => s.has(w.field))) continue;
8391
8811
  reported.add(dedupeKey);
8392
8812
  const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
@@ -8402,7 +8822,20 @@ function validateHookBodyWrites(stack) {
8402
8822
  } else {
8403
8823
  const known = judgeableFieldsOf(objectFields, w.object);
8404
8824
  if (!known) continue;
8405
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
8825
+ if (known.has(w.field)) continue;
8826
+ if (IMPLICIT_FIELDS2.has(w.field)) {
8827
+ if (!anchors.get(w.object)?.has(w.field)) continue;
8828
+ reported.add(dedupeKey);
8829
+ findings.push({
8830
+ severity: "warning",
8831
+ rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
8832
+ where,
8833
+ path,
8834
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
8835
+ hint: unprovisionedAnchorHint(w.object, w.field)
8836
+ });
8837
+ continue;
8838
+ }
8406
8839
  reported.add(dedupeKey);
8407
8840
  findings.push({
8408
8841
  severity: "warning",
@@ -8431,6 +8864,7 @@ function fixHint(field, declared) {
8431
8864
  import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
8432
8865
  var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
8433
8866
  var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
8867
+ var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
8434
8868
  var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
8435
8869
  var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
8436
8870
  var ACTION_BODY_WRITE_EXCLUSIONS = [
@@ -8447,13 +8881,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
8447
8881
  var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
8448
8882
  var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
8449
8883
  var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
8450
- var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8884
+ var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8451
8885
  function asArray40(v) {
8452
- if (Array.isArray(v)) return v.filter((x) => isRec23(x));
8453
- if (isRec23(v)) {
8886
+ if (Array.isArray(v)) return v.filter((x) => isRec24(x));
8887
+ if (isRec24(v)) {
8454
8888
  return Object.entries(v).map(([name, def]) => ({
8455
8889
  name,
8456
- ...isRec23(def) ? def : {}
8890
+ ...isRec24(def) ? def : {}
8457
8891
  }));
8458
8892
  }
8459
8893
  return [];
@@ -8471,7 +8905,7 @@ function collectActionBodies(stack) {
8471
8905
  const type = typeof action.type === "string" ? action.type : "script";
8472
8906
  if (type !== "script") return;
8473
8907
  const body = action.body;
8474
- if (!isRec23(body) || body.language !== "js") return;
8908
+ if (!isRec24(body) || body.language !== "js") return;
8475
8909
  const source = body.source;
8476
8910
  if (typeof source !== "string" || source.trim() === "") return;
8477
8911
  const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
@@ -8490,10 +8924,11 @@ function collectActionBodies(stack) {
8490
8924
  }
8491
8925
  function validateActionBodyWrites(stack) {
8492
8926
  const findings = [];
8493
- if (!isRec23(stack)) return findings;
8927
+ if (!isRec24(stack)) return findings;
8494
8928
  const sites = collectActionBodies(stack);
8495
8929
  if (sites.length === 0) return findings;
8496
8930
  let objectFields = null;
8931
+ let anchors = null;
8497
8932
  for (const site of sites) {
8498
8933
  if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
8499
8934
  const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
@@ -8518,6 +8953,7 @@ function validateActionBodyWrites(stack) {
8518
8953
  }
8519
8954
  if (writes.length === 0) continue;
8520
8955
  objectFields ?? (objectFields = indexObjectFields2(stack));
8956
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8521
8957
  const reported = /* @__PURE__ */ new Set();
8522
8958
  for (const w of writes) {
8523
8959
  if (w.object === void 0) continue;
@@ -8525,7 +8961,20 @@ function validateActionBodyWrites(stack) {
8525
8961
  if (reported.has(dedupeKey)) continue;
8526
8962
  const known = judgeableFieldsOf(objectFields, w.object);
8527
8963
  if (!known) continue;
8528
- if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
8964
+ if (known.has(w.field)) continue;
8965
+ if (IMPLICIT_FIELDS2.has(w.field)) {
8966
+ if (!anchors.get(w.object)?.has(w.field)) continue;
8967
+ reported.add(dedupeKey);
8968
+ findings.push({
8969
+ severity: "warning",
8970
+ rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
8971
+ where,
8972
+ path: site.path,
8973
+ message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
8974
+ hint: unprovisionedAnchorHint(w.object, w.field)
8975
+ });
8976
+ continue;
8977
+ }
8529
8978
  reported.add(dedupeKey);
8530
8979
  findings.push({
8531
8980
  severity: "warning",
@@ -8547,15 +8996,16 @@ function fixHint2(field, declared) {
8547
8996
  // src/validate-flow-node-writes.ts
8548
8997
  import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec/shared";
8549
8998
  var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
8999
+ var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
8550
9000
  var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
8551
9001
  var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
8552
- var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9002
+ var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8553
9003
  function asArray41(v) {
8554
- if (Array.isArray(v)) return v.filter((x) => isRec24(x));
8555
- if (isRec24(v)) {
9004
+ if (Array.isArray(v)) return v.filter((x) => isRec25(x));
9005
+ if (isRec25(v)) {
8556
9006
  return Object.entries(v).map(([name, def]) => ({
8557
9007
  name,
8558
- ...isRec24(def) ? def : {}
9008
+ ...isRec25(def) ? def : {}
8559
9009
  }));
8560
9010
  }
8561
9011
  return [];
@@ -8568,30 +9018,44 @@ function readLiteralObjectName2(config) {
8568
9018
  var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
8569
9019
  function validateFlowNodeWrites(stack) {
8570
9020
  const findings = [];
8571
- if (!isRec24(stack)) return findings;
9021
+ if (!isRec25(stack)) return findings;
8572
9022
  const flows = asArray41(stack.flows);
8573
9023
  if (flows.length === 0) return findings;
8574
9024
  let objectFields = null;
9025
+ let anchors = null;
8575
9026
  flows.forEach((flow, flowIndex) => {
8576
9027
  const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
8577
9028
  const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
8578
9029
  walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
8579
9030
  if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
8580
- const config = isRec24(node.config) ? node.config : void 0;
9031
+ const config = isRec25(node.config) ? node.config : void 0;
8581
9032
  if (!config) return;
8582
9033
  const fields = config.fields;
8583
- if (!isRec24(fields)) return;
9034
+ if (!isRec25(fields)) return;
8584
9035
  const written = Object.keys(fields);
8585
9036
  if (written.length === 0) return;
8586
9037
  const objectName = readLiteralObjectName2(config);
8587
9038
  if (!objectName) return;
8588
9039
  objectFields ?? (objectFields = indexObjectFields2(stack));
9040
+ anchors ?? (anchors = indexUnprovisionedAnchors(stack));
8589
9041
  const known = judgeableFieldsOf(objectFields, objectName);
8590
9042
  if (!known) return;
8591
9043
  const nodeName = flowNodeLabel(node, walkIndex);
8592
9044
  const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
8593
9045
  for (const fieldName of written) {
8594
- if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
9046
+ if (known.has(fieldName)) continue;
9047
+ if (IMPLICIT_FIELDS2.has(fieldName)) {
9048
+ if (!anchors.get(objectName)?.has(fieldName)) continue;
9049
+ findings.push({
9050
+ severity: "warning",
9051
+ rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
9052
+ where: `flow "${flowName}" \u203A ${nodeWhere}`,
9053
+ path: `${nodePath}.config.fields.${fieldName}`,
9054
+ message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
9055
+ hint: unprovisionedAnchorHint(objectName, fieldName)
9056
+ });
9057
+ continue;
9058
+ }
8595
9059
  if (fieldName.includes(".")) continue;
8596
9060
  findings.push({
8597
9061
  severity: "error",
@@ -8615,6 +9079,13 @@ function fixHint3(field, declared) {
8615
9079
  var REFERENCE_INTEGRITY_RULES = [
8616
9080
  { name: "validateObjectReferences", run: validateObjectReferences },
8617
9081
  { name: "validateSearchableFields", run: validateSearchableFields },
9082
+ // [#9257] The same reading, one axis over: a list view's `sort` is a field
9083
+ // name written in metadata, resolved against the object's declared fields. It
9084
+ // gates (`error`) because the runtime does not tolerate a bad one at all —
9085
+ // `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
9086
+ // both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
9087
+ // the refusal is the whole view, on every load, traced to nothing.
9088
+ { name: "validateSortableFields", run: validateSortableFields },
8618
9089
  { name: "validateActionNameRefs", run: validateActionNameRefs },
8619
9090
  { name: "validatePageFieldBindings", run: validatePageFieldBindings },
8620
9091
  { name: "validateChartBindings", run: validateChartBindings },
@@ -9557,6 +10028,17 @@ function lintViewRefs(stack) {
9557
10028
  }
9558
10029
 
9559
10030
  // src/data-model-rules.ts
10031
+ function objectWhere(obj) {
10032
+ return `object "${obj?.name}"`;
10033
+ }
10034
+ function indexWhere(obj, idx, j, cols) {
10035
+ const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
10036
+ const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
10037
+ return `${objectWhere(obj)} \xB7 index ${label2}`;
10038
+ }
10039
+ function fieldWhere(obj, fieldName) {
10040
+ return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
10041
+ }
9560
10042
  var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
9561
10043
  var NUMERIC_TYPES = /* @__PURE__ */ new Set([
9562
10044
  "number",
@@ -9629,11 +10111,13 @@ function lintUnscopedDeclaredIndexes(objects) {
9629
10111
  for (let j = 0; j < declaredIndexes.length; j++) {
9630
10112
  const idx = declaredIndexes[j];
9631
10113
  if (idx?.unique !== true) continue;
9632
- const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
10114
+ const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
10115
+ const cols = colList.join(", ");
9633
10116
  const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
9634
10117
  issues.push({
9635
10118
  severity: "warning",
9636
10119
  rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
10120
+ where: indexWhere(obj, idx, j, colList),
9637
10121
  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
10122
  path: `objects[${i}].indexes[${j}]`,
9639
10123
  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 +10166,11 @@ function lintUniqueDeclarations(objects) {
9682
10166
  issues.push({
9683
10167
  severity: "warning",
9684
10168
  rule: UNIQUE_DOUBLE_DECLARATION,
10169
+ // More specific than `path` on purpose: this rule's finding is about ONE
10170
+ // column, but its `path` has always been the whole object (`objects[i]`)
10171
+ // because the defect straddles `fields.<name>.unique` and an entry of
10172
+ // `indexes`. `where` can name the column without picking one of the two.
10173
+ where: fieldWhere(obj, name),
9685
10174
  message,
9686
10175
  path: `objects[${i}]`,
9687
10176
  fix
@@ -9710,6 +10199,7 @@ function lintLegacyOrganizationComposites(objects) {
9710
10199
  issues.push({
9711
10200
  severity: "warning",
9712
10201
  rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
10202
+ where: indexWhere(obj, idx, j, cols),
9713
10203
  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
10204
  path: `objects[${i}].indexes[${j}]`,
9715
10205
  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 +10346,7 @@ var CLI_ONLY = ["cli"];
9856
10346
  var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
9857
10347
  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
10348
  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.";
10349
+ 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
10350
  var EXPRESSION_INVALID = "expression-invalid";
9861
10351
  var AUTHORING_RULES = [
9862
10352
  // ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
@@ -9917,8 +10407,14 @@ var AUTHORING_RULES = [
9917
10407
  input: "normalized",
9918
10408
  commands: ALL,
9919
10409
  source: "packages/lint/src/validate-functional-completeness.ts",
9920
- surfaces: CLI_ONLY,
9921
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10410
+ // Runtime publish gate (#4716): the OBJECT write door — the five gating
10411
+ // object rules cross together under the 2026-08-18 adjudication. The
10412
+ // false-positive budget the crossing owed was exempted on a measured
10413
+ // 0 refusals / 75 real object declarations (authored config-file metadata,
10414
+ // so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
10415
+ // advisory-tier object rules deliberately do NOT ride.
10416
+ surfaces: CLI_AND_RUNTIME,
10417
+ runtimeTypes: ["object"],
9922
10418
  run: (stack) => validateFunctionalCompleteness(stack)
9923
10419
  },
9924
10420
  // [#7521, via cloud#1225] A managed object advertising a generic write verb
@@ -9942,8 +10438,11 @@ var AUTHORING_RULES = [
9942
10438
  input: "normalized",
9943
10439
  commands: ALL,
9944
10440
  source: "packages/lint/src/validate-managed-api-methods.ts",
9945
- surfaces: CLI_ONLY,
9946
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10441
+ // Runtime publish gate (#4716): a managed object advertising a verb its
10442
+ // own affordances refuse is exactly the contradiction a Studio/MCP author
10443
+ // can save today — the CLI sweep (#7934) never sees an overlay row.
10444
+ surfaces: CLI_AND_RUNTIME,
10445
+ runtimeTypes: ["object"],
9947
10446
  run: (stack) => validateManagedApiMethods(stack)
9948
10447
  },
9949
10448
  // A view container in `views: []` that registers zero views: nothing appears
@@ -9965,19 +10464,38 @@ var AUTHORING_RULES = [
9965
10464
  },
9966
10465
  // ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
9967
10466
  // chartConfig axis/series must resolve against the declared datasets.
10467
+ //
10468
+ // Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
10469
+ // widget bound to a dataset that resolves to nothing sailed through save AND
10470
+ // publish — this rule provably caught the body and was simply never invoked,
10471
+ // because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
10472
+ // hold a forward reference; publishing refuses it with the key path named.
10473
+ // The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
10474
+ // — without it every legitimate board reads as dangling, the 3-phantom
10475
+ // measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
10476
+ // rule's error ids on the publish gate, not just `widget-dataset-unknown` —
10477
+ // ruled 2026-08-15: they are one coherent "this board cannot render"
10478
+ // reference-integrity class, and the ~6× wider accept-set narrowing was
10479
+ // accepted knowingly rather than splitting the dataset limb into its own
10480
+ // rule (traversal-duplication drift) or adding a per-finding-id surface
10481
+ // filter (registry machinery that weakens "delete a rule from the table and
10482
+ // enforcement stops in the same commit"). Warning-tier ids ride along on the
10483
+ // advisory channel and never block (#4463 P1).
9968
10484
  {
9969
10485
  name: "validateWidgetBindings",
9970
10486
  tier: "gating",
9971
10487
  input: "parsed",
9972
10488
  commands: ALL,
9973
10489
  source: "packages/lint/src/validate-widget-bindings.ts",
9974
- surfaces: CLI_ONLY,
9975
- surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10490
+ surfaces: CLI_AND_RUNTIME,
10491
+ runtimeTypes: ["dashboard"],
9976
10492
  run: (stack) => validateWidgetBindings(stack)
9977
10493
  },
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.
10494
+ // ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
10495
+ // button that renders and refuses (or does nothing) on click: a `script`
10496
+ // target must name a defined action, a `modal` target must name a declared
10497
+ // page (objectstack#6739-A — a modal string target names a PAGE, only).
10498
+ // Unresolved `url` routes stay advisory.
9981
10499
  {
9982
10500
  name: "validateDashboardActionRefs",
9983
10501
  tier: "gating",
@@ -10002,6 +10520,28 @@ var AUTHORING_RULES = [
10002
10520
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10003
10521
  run: (stack) => validateFilterTokens(stack)
10004
10522
  },
10523
+ // #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
10524
+ // name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
10525
+ // no layer: the engine refuses it on a declared temporal field at query time
10526
+ // (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
10527
+ // literal string. This is the authoring-time refusal the ruling shipped
10528
+ // alongside the engine door, judging the filter literal in isolation —
10529
+ // ordering positions only, all three authored filter shapes. Like
10530
+ // `validateEmptyCombinators` it needs NO resolution context, so
10531
+ // RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
10532
+ // for every filter-carrying type the gate already maps: the write path is
10533
+ // the one door an AI author uses, and dashboards/views are where the preset
10534
+ // vocabulary is near enough to reach for.
10535
+ {
10536
+ name: "validatePresetComparands",
10537
+ tier: "gating",
10538
+ input: "parsed",
10539
+ commands: ALL,
10540
+ source: "packages/lint/src/validate-preset-comparands.ts",
10541
+ surfaces: CLI_AND_RUNTIME,
10542
+ runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
10543
+ run: (stack) => validatePresetComparands(stack)
10544
+ },
10005
10545
  // #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
10006
10546
  // `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
10007
10547
  // this rule does not touch it: it refuses the literal SPELLINGS at authoring
@@ -10220,7 +10760,7 @@ var AUTHORING_RULES = [
10220
10760
  commands: ALL,
10221
10761
  source: "packages/lint/src/validate-record-title.ts",
10222
10762
  surfaces: CLI_ONLY,
10223
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10763
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10224
10764
  run: (stack) => validateRecordTitle(stack)
10225
10765
  },
10226
10766
  // ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
@@ -10233,7 +10773,7 @@ var AUTHORING_RULES = [
10233
10773
  commands: ALL,
10234
10774
  source: "packages/lint/src/validate-semantic-roles.ts",
10235
10775
  surfaces: CLI_ONLY,
10236
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10776
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10237
10777
  run: (stack) => validateSemanticRoles(stack)
10238
10778
  },
10239
10779
  // #2578 / #4449 — a form section's field reference that resolves to nothing
@@ -10444,7 +10984,7 @@ var AUTHORING_RULES = [
10444
10984
  commands: ALL,
10445
10985
  source: "packages/lint/src/lint-liveness-properties.ts",
10446
10986
  surfaces: CLI_ONLY,
10447
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
10987
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10448
10988
  run: (stack) => lintLivenessProperties(stack).map((f) => ({
10449
10989
  severity: "warning",
10450
10990
  rule: f.rule,
@@ -10463,8 +11003,12 @@ var AUTHORING_RULES = [
10463
11003
  input: "parsed",
10464
11004
  commands: ALL,
10465
11005
  source: "packages/lint/src/lint-autonumber-formats.ts",
10466
- surfaces: CLI_ONLY,
10467
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11006
+ // Runtime publish gate (#4716): an autonumber referencing a field the
11007
+ // object does not carry is broken from the first record; only the error
11008
+ // arm blocks — the optional-field arm is `warning` and rides the
11009
+ // advisory channel like every other non-error finding (#4463 P1).
11010
+ surfaces: CLI_AND_RUNTIME,
11011
+ runtimeTypes: ["object"],
10468
11012
  run: (stack) => lintAutonumberFormats(stack).map((f) => ({
10469
11013
  severity: f.severity,
10470
11014
  rule: f.rule,
@@ -10505,12 +11049,12 @@ var AUTHORING_RULES = [
10505
11049
  commands: ["validate", "build"],
10506
11050
  source: "packages/lint/src/data-model-rules.ts",
10507
11051
  surfaces: CLI_ONLY,
10508
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11052
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10509
11053
  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
11054
  run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10511
11055
  severity: f.severity === "suggestion" ? "info" : f.severity,
10512
11056
  rule: f.rule,
10513
- where: f.path,
11057
+ where: f.where,
10514
11058
  path: f.path,
10515
11059
  message: f.message,
10516
11060
  hint: f.fix ?? ""
@@ -10526,12 +11070,12 @@ var AUTHORING_RULES = [
10526
11070
  commands: ["validate", "build"],
10527
11071
  source: "packages/lint/src/data-model-rules.ts",
10528
11072
  surfaces: CLI_ONLY,
10529
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11073
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10530
11074
  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
11075
  run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10532
11076
  severity: f.severity === "suggestion" ? "info" : f.severity,
10533
11077
  rule: f.rule,
10534
- where: f.path,
11078
+ where: f.where,
10535
11079
  path: f.path,
10536
11080
  message: f.message,
10537
11081
  hint: f.fix ?? ""
@@ -10548,12 +11092,12 @@ var AUTHORING_RULES = [
10548
11092
  commands: ["validate", "build"],
10549
11093
  source: "packages/lint/src/data-model-rules.ts",
10550
11094
  surfaces: CLI_ONLY,
10551
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11095
+ surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
10552
11096
  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
11097
  run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
10554
11098
  severity: f.severity === "suggestion" ? "info" : f.severity,
10555
11099
  rule: f.rule,
10556
- where: f.path,
11100
+ where: f.where,
10557
11101
  path: f.path,
10558
11102
  message: f.message,
10559
11103
  hint: f.fix ?? ""
@@ -10681,16 +11225,26 @@ var AUTHORING_RULES = [
10681
11225
  surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
10682
11226
  run: (stack) => validateOrgAxisRedLines(stack)
10683
11227
  },
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.
11228
+ // #4698 / #9698 — the "declared but never read" gate, for the two fields of a
11229
+ // sharing rule where the predicate is EXACT rather than inferred.
11230
+ //
11231
+ // - `condition` has a single runtime consumer
11232
+ // (`bootstrapDeclaredSharingRules`) whose only use of the key is
11233
+ // `compileCelToFilter(condition, { variables: {} })`; a condition that
11234
+ // does not lower means the rule is SKIPPED at boot.
11235
+ // - `object` decides whether the grant is refused outright: reconcile hands
11236
+ // each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
11237
+ // `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
11238
+ // `public` or it is a `controlled_by_parent` detail. Both are decidable
11239
+ // from authored metadata; the other arms of that verdict (`owner_id`, the
11240
+ // bypass set, federated anchors) are not, and are excluded by name.
11241
+ //
11242
+ // Either way the grant is declared and does not exist. The lint calls the
11243
+ // same compiler and mirrors the same verdict function, from the same inputs
11244
+ // — the verdict cannot drift from the consumers'. Gating for the ADR-0078
11245
+ // reason `SharingRuleSchema`'s own docblock states: the whole authorable
11246
+ // surface is enforced, and these were the fields where that sentence was not
11247
+ // yet true.
10694
11248
  {
10695
11249
  name: "validateSharingRuleEnforceability",
10696
11250
  tier: "gating",
@@ -10698,7 +11252,7 @@ var AUTHORING_RULES = [
10698
11252
  commands: ALL,
10699
11253
  source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
10700
11254
  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.",
11255
+ 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
11256
  run: (stack) => validateSharingRuleEnforceability(stack)
10703
11257
  },
10704
11258
  // #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
@@ -10737,8 +11291,15 @@ var AUTHORING_RULES = [
10737
11291
  input: "parsed",
10738
11292
  commands: ALL,
10739
11293
  source: "packages/lint/src/validate-rule-compilability.ts",
10740
- surfaces: CLI_ONLY,
10741
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11294
+ // Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
11295
+ // judged snapshot actually carries a `json_schema` validation — so an
11296
+ // ordinary object write (no `json_schema` anywhere) still loads no
11297
+ // compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
11298
+ // The load it does take (~64 ms cold once, ~15 ms warm per publish
11299
+ // carrying such a rule) is the measured, adjudicated price of refusing a
11300
+ // validation rule that would otherwise ship compiled-by-nothing.
11301
+ surfaces: CLI_AND_RUNTIME,
11302
+ runtimeTypes: ["object"],
10742
11303
  run: (stack) => validateRuleCompilability(stack)
10743
11304
  },
10744
11305
  // #5178 — the residual half of #5029, which registering `ajv-formats` does
@@ -10759,8 +11320,12 @@ var AUTHORING_RULES = [
10759
11320
  input: "parsed",
10760
11321
  commands: ALL,
10761
11322
  source: "packages/lint/src/validate-rule-schema-formats.ts",
10762
- surfaces: CLI_ONLY,
10763
- surfaceReason: RUNTIME_OBJECT_WRITES_P2,
11323
+ // Runtime publish gate (#4716): crosses with its compile sibling above —
11324
+ // the two judgements over one artifact stay on one side of the wall
11325
+ // (#7220's family discipline). Same lazy-ajv contract: the registered
11326
+ // format set is only enumerated once a schema actually names a format.
11327
+ surfaces: CLI_AND_RUNTIME,
11328
+ runtimeTypes: ["object"],
10764
11329
  run: (stack) => validateRuleSchemaFormats(stack)
10765
11330
  }
10766
11331
  ];
@@ -10820,7 +11385,20 @@ var TYPE_TO_STACK_KEY = {
10820
11385
  permission: "permissions",
10821
11386
  book: "books"
10822
11387
  };
10823
- var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
11388
+ function narrowObjectsToPackageClosure(objects, scope) {
11389
+ if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
11390
+ const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
11391
+ return objects.filter((entry) => {
11392
+ if (!entry || typeof entry !== "object") return true;
11393
+ const owner = entry[PACKAGE_PROVENANCE_KEY];
11394
+ if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
11395
+ if (reachable.has(owner)) return true;
11396
+ return isSystemObject(entry);
11397
+ });
11398
+ }
11399
+ var PACKAGE_PROVENANCE_KEY = "_packageId";
11400
+ var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
11401
+ var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
10824
11402
  function runtimeAuthoringRulesFor(type) {
10825
11403
  return AUTHORING_RULES.filter(
10826
11404
  (r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
@@ -10846,7 +11424,8 @@ function buildRuntimeWriteSnapshots(args) {
10846
11424
  const itemName = typeof item.name === "string" ? item.name : void 0;
10847
11425
  const baseline = {};
10848
11426
  for (const key of CONTEXT_STACK_KEYS) {
10849
- const collection = args.context?.[key] ?? [];
11427
+ const raw = args.context?.[key] ?? [];
11428
+ const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
10850
11429
  baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
10851
11430
  }
10852
11431
  const candidate = {
@@ -10880,7 +11459,8 @@ function runRuntimeAuthoringRules(args) {
10880
11459
  const snapshots = buildRuntimeWriteSnapshots({
10881
11460
  type: args.type,
10882
11461
  item: args.item,
10883
- ...args.context !== void 0 ? { context: args.context } : {}
11462
+ ...args.context !== void 0 ? { context: args.context } : {},
11463
+ ...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
10884
11464
  });
10885
11465
  if (!snapshots) return empty;
10886
11466
  const ctx = { sduiManifest: args.sduiManifest };
@@ -10897,6 +11477,7 @@ export {
10897
11477
  ACTION_BODY_WRITE_PATTERNS,
10898
11478
  ACTION_BODY_WRITE_PATTERN_IDS,
10899
11479
  ACTION_BODY_WRITE_UNKNOWN_FIELD,
11480
+ ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
10900
11481
  ACTION_NAME_UNDEFINED,
10901
11482
  ACTION_NO_PLACEMENT,
10902
11483
  ACTION_RECORD_WRITE_DISCARDED,
@@ -10942,6 +11523,7 @@ export {
10942
11523
  FIELD_GROUP_UNDECLARED,
10943
11524
  FILTER_EMPTY_COMBINATOR,
10944
11525
  FILTER_EMPTY_NODE,
11526
+ FILTER_PRESET_COMPARAND,
10945
11527
  FILTER_TOKEN_UNKNOWN,
10946
11528
  FLOW_APPROVAL_REVISE_DEAD_END,
10947
11529
  FLOW_APPROVAL_REVISE_DISABLED,
@@ -10959,6 +11541,7 @@ export {
10959
11541
  FLOW_MULTIPLE_DEFAULT_EDGES,
10960
11542
  FLOW_MULTI_WRITE_UNFILTERED,
10961
11543
  FLOW_NODE_WRITE_UNKNOWN_FIELD,
11544
+ FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
10962
11545
  FLOW_PHANTOM_AGGREGATION,
10963
11546
  FLOW_RUNAS_UNSCOPED,
10964
11547
  FLOW_TEMPLATE_FIELD_UNPROVISIONED,
@@ -10980,6 +11563,7 @@ export {
10980
11563
  HOOK_BODY_WRITE_PATTERNS,
10981
11564
  HOOK_BODY_WRITE_PATTERN_IDS,
10982
11565
  HOOK_BODY_WRITE_UNKNOWN_FIELD,
11566
+ HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
10983
11567
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
10984
11568
  LIVENESS_DEAD_PROPERTY,
10985
11569
  LIVENESS_EXPERIMENTAL_PROPERTY,
@@ -11015,6 +11599,7 @@ export {
11015
11599
  RUNTIME_AJV_OPTIONS,
11016
11600
  SEAL_MARKERS,
11017
11601
  SEARCHABLE_FIELD_UNKNOWN,
11602
+ SEARCHABLE_FIELD_UNPROVISIONED,
11018
11603
  SEARCHABLE_FIELD_UNSEARCHABLE,
11019
11604
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
11020
11605
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
@@ -11033,8 +11618,12 @@ export {
11033
11618
  SEED_VALUE_OUTSIDE_STATE_MACHINE,
11034
11619
  SEMANTIC_ROLE_FIELD_UNKNOWN,
11035
11620
  SEMANTIC_ROLE_FIELD_UNPROVISIONED,
11621
+ SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
11622
+ SHARING_RULE_OBJECT_NOT_SHAREABLE,
11036
11623
  SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
11037
11624
  SHARING_RULE_UNLOWERABLE_CONDITION,
11625
+ SORT_FIELD_UNKNOWN,
11626
+ SORT_FIELD_UNSORTABLE,
11038
11627
  STARTUP_OPEN_VOCABULARY_VERDICT,
11039
11628
  STARTUP_VERDICT_ASSERTIVE_WORDING,
11040
11629
  STARTUP_VERDICT_HINT,
@@ -11071,6 +11660,7 @@ export {
11071
11660
  authoringRulesFor,
11072
11661
  buildAccessMatrix,
11073
11662
  buildRuntimeWriteSnapshots,
11663
+ checkSortDeclaration,
11074
11664
  diffAccessMatrix,
11075
11665
  extractHookBodyWriteSet,
11076
11666
  extractHookBodyWrites,
@@ -11085,6 +11675,7 @@ export {
11085
11675
  lintUniqueDeclarations,
11086
11676
  lintUnscopedDeclaredIndexes,
11087
11677
  lintViewRefs,
11678
+ narrowObjectsToPackageClosure,
11088
11679
  nearestRegisteredFormat,
11089
11680
  nullGuardMessage,
11090
11681
  runAuthoringRules,
@@ -11123,6 +11714,7 @@ export {
11123
11714
  validatePageFieldBindings,
11124
11715
  validatePageSourceStyling,
11125
11716
  validatePredicatePathRefs,
11717
+ validatePresetComparands,
11126
11718
  validateReactPageProps,
11127
11719
  validateReactPages,
11128
11720
  validateReadonlyFlowWrites,
@@ -11139,6 +11731,7 @@ export {
11139
11731
  validateSeedStateMachine,
11140
11732
  validateSemanticRoles,
11141
11733
  validateSharingRuleEnforceability,
11734
+ validateSortableFields,
11142
11735
  validateStackExpressions,
11143
11736
  validateTranslatableSections,
11144
11737
  validateTranslationReferences,