@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/CHANGELOG.md +622 -0
- package/dist/index.cjs +894 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +310 -66
- package/dist/index.d.ts +310 -66
- package/dist/index.js +871 -278
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-s3X9D9hm.d.cts} +89 -2
- package/dist/{runtime-H-nDodRy.d.ts → runtime-s3X9D9hm.d.ts} +89 -2
- package/dist/runtime.cjs +986 -409
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +959 -378
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
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,
|
|
589
|
+
function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
|
|
590
590
|
const owner = objectName ? `'${objectName}'` : "this object";
|
|
591
|
-
const hasNote =
|
|
592
|
-
return `${subject} applies \`${
|
|
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
|
|
775
|
+
for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
776
776
|
issues.push({
|
|
777
777
|
where,
|
|
778
|
-
message: nullGuardMessage(subject, objectName,
|
|
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`).
|
|
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)
|
|
862
|
-
// behaviour, never the retired key's fate —
|
|
863
|
-
// over a branch that DELETES the key
|
|
864
|
-
//
|
|
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
|
|
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
|
|
940
|
-
for (const e of res.errors) issues.push({ where:
|
|
941
|
-
for (const w of res.warnings) issues.push({ where:
|
|
942
|
-
warnUnprovisionedAnchors(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
5670
|
-
hint: actionType === "modal" ? `
|
|
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
|
|
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
|
|
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
|
|
6699
|
+
function suggest5(target, known) {
|
|
6299
6700
|
let best;
|
|
6300
6701
|
let bestScore = Infinity;
|
|
6301
6702
|
for (const candidate of known) {
|
|
6302
|
-
const d =
|
|
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 =
|
|
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 =
|
|
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.` +
|
|
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 =
|
|
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 =
|
|
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 "${
|
|
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 =
|
|
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 "${
|
|
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 =
|
|
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 =
|
|
6831
|
+
const actionName = strName12(actionDef?.actionName);
|
|
6431
6832
|
if (nav.type === "action" && actionName) {
|
|
6432
6833
|
check(
|
|
6433
6834
|
actionName,
|
|
6434
|
-
`app "${appName}" \xB7 nav "${
|
|
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 =
|
|
6840
|
+
const runAction = strName12(nav.runAction);
|
|
6440
6841
|
if (nav.type === "object" && runAction) {
|
|
6441
6842
|
check(
|
|
6442
6843
|
runAction,
|
|
6443
|
-
`app "${appName}" \xB7 nav "${
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
6940
|
+
function isRec17(v) {
|
|
6540
6941
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6541
6942
|
}
|
|
6542
|
-
function
|
|
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 =
|
|
6557
|
-
return
|
|
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 (!
|
|
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 (!
|
|
6579
|
-
const pageName =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
7064
|
+
function isRec18(v) {
|
|
6664
7065
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6665
7066
|
}
|
|
6666
|
-
function
|
|
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
|
|
7083
|
+
function suggest6(target, known) {
|
|
6683
7084
|
let best;
|
|
6684
7085
|
let bestScore = Infinity;
|
|
6685
7086
|
for (const c of known) {
|
|
6686
|
-
const d =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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())}.${
|
|
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)}.${
|
|
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)}.${
|
|
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 (!
|
|
6793
|
-
const reportName =
|
|
7193
|
+
if (!isRec18(report)) continue;
|
|
7194
|
+
const reportName = strName15(report.name) ?? `#${ri}`;
|
|
6794
7195
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
6795
|
-
if (!
|
|
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:
|
|
6803
|
-
yAxis:
|
|
6804
|
-
series: asArray31(chart.series).map((s, si) => ({ 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
|
-
|
|
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 (!
|
|
7220
|
+
if (!isRec18(block)) continue;
|
|
6820
7221
|
checkReportChart(
|
|
6821
7222
|
block.chart,
|
|
6822
|
-
|
|
7223
|
+
strName15(block.dataset),
|
|
6823
7224
|
strList3(block.values),
|
|
6824
|
-
`report "${reportName}" \xB7 block "${
|
|
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 (!
|
|
7231
|
+
if (!isRec18(container)) return;
|
|
6831
7232
|
const chart = container.chart;
|
|
6832
|
-
if (!
|
|
7233
|
+
if (!isRec18(chart)) return;
|
|
6833
7234
|
check({
|
|
6834
|
-
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 (!
|
|
6845
|
-
const viewName =
|
|
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 (
|
|
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 (!
|
|
6857
|
-
const objName =
|
|
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 (!
|
|
6870
|
-
const pageName =
|
|
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 =
|
|
6873
|
-
if (!props || !
|
|
6874
|
-
const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name:
|
|
6875
|
-
const seriesRefs = asArray31(props.series).map((s, si) => ({ 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:
|
|
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 ${
|
|
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
|
|
7295
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6895
7296
|
function asArray32(v) {
|
|
6896
|
-
if (Array.isArray(v)) return v.filter(
|
|
6897
|
-
if (
|
|
6898
|
-
return Object.entries(v).filter(([, 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 =
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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" &&
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
7473
|
+
} else if (isRec20(items)) {
|
|
7073
7474
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7074
7475
|
}
|
|
7075
7476
|
const dependencies = schema.dependencies;
|
|
7076
|
-
if (
|
|
7477
|
+
if (isRec20(dependencies)) {
|
|
7077
7478
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7078
|
-
if (!
|
|
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
|
|
7106
|
-
if (
|
|
7107
|
-
bestDistance =
|
|
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" || !
|
|
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
|
|
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 =
|
|
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 =
|
|
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 "${
|
|
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 =
|
|
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
|
|
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 (
|
|
7725
|
+
if (isRec21(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec21(def) ? def : {} }));
|
|
7325
7726
|
return [];
|
|
7326
7727
|
}
|
|
7327
|
-
function
|
|
7728
|
+
function strName17(v) {
|
|
7328
7729
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7329
7730
|
}
|
|
7330
|
-
function
|
|
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
|
|
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 =
|
|
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 =
|
|
7789
|
+
const sectionName = strName17(section.name);
|
|
7389
7790
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
7390
7791
|
}
|
|
7391
7792
|
};
|
|
7392
|
-
const listBinding =
|
|
7393
|
-
if (
|
|
7394
|
-
addView(recordObject ?? listBinding,
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 || !
|
|
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) =>
|
|
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 (!
|
|
7445
|
-
const 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 =
|
|
7849
|
+
const label2 = strName17(opt.label);
|
|
7449
7850
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
7450
7851
|
}
|
|
7451
|
-
} else if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
7911
|
+
const actionName = strName17(action.name);
|
|
7511
7912
|
if (!actionName) continue;
|
|
7512
|
-
const owner =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
7982
|
+
if (!isRec21(bundle)) continue;
|
|
7582
7983
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
7583
|
-
if (!
|
|
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 (!
|
|
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.` +
|
|
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.` +
|
|
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 (!
|
|
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.` +
|
|
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.` +
|
|
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.` +
|
|
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.` +
|
|
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.` +
|
|
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 (!
|
|
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.` +
|
|
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.` +
|
|
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 (!
|
|
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.` +
|
|
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.` +
|
|
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
|
|
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 "${
|
|
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.` +
|
|
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(
|
|
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 =
|
|
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.` +
|
|
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
|
|
8197
|
+
function isRec22(v) {
|
|
7797
8198
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7798
8199
|
}
|
|
7799
|
-
function
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
8226
|
+
if (!isRec22(bundle)) continue;
|
|
7826
8227
|
for (const data of Object.values(bundle)) {
|
|
7827
|
-
if (!
|
|
8228
|
+
if (!isRec22(data) || !isRec22(data.objects)) continue;
|
|
7828
8229
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
7829
|
-
if (
|
|
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 (!
|
|
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 =
|
|
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:
|
|
8251
|
+
{ ...view, object: strName18(view.object) ?? objectName },
|
|
7851
8252
|
path,
|
|
7852
8253
|
viewLabel(view),
|
|
7853
8254
|
sites
|
|
7854
8255
|
);
|
|
7855
8256
|
}
|
|
7856
|
-
if (
|
|
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 =
|
|
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 =
|
|
8269
|
+
const props = isRec22(walked.component.properties) ? walked.component.properties : void 0;
|
|
7869
8270
|
if (!props) continue;
|
|
7870
|
-
const type =
|
|
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 (!
|
|
7886
|
-
if (
|
|
7887
|
-
const heading =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
8363
|
+
function strName20(v) {
|
|
7963
8364
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7964
8365
|
}
|
|
7965
|
-
function
|
|
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
|
|
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 =
|
|
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 (!
|
|
8004
|
-
const 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.` +
|
|
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
|
|
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 =
|
|
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 =
|
|
8514
|
+
const defaultAgent = strName21(app.defaultAgent);
|
|
8114
8515
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
8115
|
-
const appName =
|
|
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
|
-
|
|
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) =>
|
|
8220
|
-
if (
|
|
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
|
-
...
|
|
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 (!
|
|
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))
|
|
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 (
|
|
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
|
|
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) =>
|
|
8453
|
-
if (
|
|
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
|
-
...
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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
|
|
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) =>
|
|
8555
|
-
if (
|
|
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
|
-
...
|
|
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 (!
|
|
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 =
|
|
9031
|
+
const config = isRec25(node.config) ? node.config : void 0;
|
|
8581
9032
|
if (!config) return;
|
|
8582
9033
|
const fields = config.fields;
|
|
8583
|
-
if (!
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
9921
|
-
|
|
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
|
-
|
|
9946
|
-
|
|
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:
|
|
9975
|
-
|
|
10490
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10491
|
+
runtimeTypes: ["dashboard"],
|
|
9976
10492
|
run: (stack) => validateWidgetBindings(stack)
|
|
9977
10493
|
},
|
|
9978
|
-
// ADR-0049 / #3367 — a header
|
|
9979
|
-
//
|
|
9980
|
-
//
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
10467
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
|
10685
|
-
// predicate is EXACT rather than inferred.
|
|
10686
|
-
//
|
|
10687
|
-
//
|
|
10688
|
-
//
|
|
10689
|
-
//
|
|
10690
|
-
//
|
|
10691
|
-
//
|
|
10692
|
-
//
|
|
10693
|
-
//
|
|
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.
|
|
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
|
-
|
|
10741
|
-
|
|
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
|
-
|
|
10763
|
-
|
|
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
|
-
|
|
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
|
|
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,
|