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