@objectstack/lint 17.1.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 +229 -0
- package/dist/index.cjs +236 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -14
- package/dist/index.d.ts +218 -14
- package/dist/index.js +229 -31
- package/dist/index.js.map +1 -1
- package/dist/{runtime-s3X9D9hm.d.cts → runtime-cV-l_vtC.d.cts} +20 -1
- package/dist/{runtime-s3X9D9hm.d.ts → runtime-cV-l_vtC.d.ts} +20 -1
- package/dist/runtime.cjs +206 -28
- 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 +206 -28
- package/dist/runtime.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1016,6 +1016,38 @@ function validateStackExpressions(stack) {
|
|
|
1016
1016
|
|
|
1017
1017
|
// src/lint-startup-registry-verdict.ts
|
|
1018
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
|
|
1019
1051
|
var cachedTs = null;
|
|
1020
1052
|
function loadTypeScript() {
|
|
1021
1053
|
if (cachedTs) return cachedTs;
|
|
@@ -1031,6 +1063,7 @@ function loadTypeScript() {
|
|
|
1031
1063
|
}
|
|
1032
1064
|
var STARTUP_OPEN_VOCABULARY_VERDICT = "startup-open-vocabulary-verdict";
|
|
1033
1065
|
var STARTUP_VERDICT_ASSERTIVE_WORDING = "startup-verdict-assertive-wording";
|
|
1066
|
+
var STARTUP_SOURCE_UNPARSEABLE = "startup-source-unparseable";
|
|
1034
1067
|
var OPEN_VOCABULARY_PROBES = /* @__PURE__ */ new Map([
|
|
1035
1068
|
[
|
|
1036
1069
|
"getRegisteredNodeTypes",
|
|
@@ -1259,13 +1292,22 @@ function findStartupRegistryVerdicts(source, options = {}) {
|
|
|
1259
1292
|
if (!mentionsAny) return [];
|
|
1260
1293
|
const t = loadTypeScript();
|
|
1261
1294
|
const fileLabel = options.file ?? "source";
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1295
|
+
const { sourceFile: sf, failure } = createSourceFileChecked(t, fileLabel, source, {
|
|
1296
|
+
target: t.ScriptTarget.Latest,
|
|
1297
|
+
setParentNodes: true,
|
|
1298
|
+
scriptKind: t.ScriptKind.TS
|
|
1299
|
+
});
|
|
1268
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
|
+
}
|
|
1269
1311
|
const moduleBindings = moduleLevelMutableBindings(t, sf);
|
|
1270
1312
|
const functionBodies = indexFunctionBodies(t, sf);
|
|
1271
1313
|
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
@@ -2811,6 +2853,26 @@ function validateSearchableFields(stack) {
|
|
|
2811
2853
|
if (!isRec4(view)) continue;
|
|
2812
2854
|
const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
2813
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
|
+
}
|
|
2814
2876
|
if (isRec4(view.list)) {
|
|
2815
2877
|
check(
|
|
2816
2878
|
view.list.searchableFields,
|
|
@@ -3257,6 +3319,7 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
3257
3319
|
};
|
|
3258
3320
|
return perPosition(init.expression);
|
|
3259
3321
|
}
|
|
3322
|
+
var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
|
|
3260
3323
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
3261
3324
|
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
3262
3325
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
@@ -3585,11 +3648,20 @@ function validateReactPageProps(stack) {
|
|
|
3585
3648
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
3586
3649
|
const name = String(page.name ?? `#${p}`);
|
|
3587
3650
|
const tsc = loadTypeScript2();
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
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
|
+
});
|
|
3593
3665
|
}
|
|
3594
3666
|
const locals = localComponentNames(tsc, sf);
|
|
3595
3667
|
const visit = (node) => {
|
|
@@ -6499,6 +6571,7 @@ function validateNavObjectServability(stack) {
|
|
|
6499
6571
|
import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
|
|
6500
6572
|
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
6501
6573
|
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
6574
|
+
var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
|
|
6502
6575
|
function isRec16(v) {
|
|
6503
6576
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6504
6577
|
}
|
|
@@ -6561,7 +6634,7 @@ function distance3(a, b) {
|
|
|
6561
6634
|
}
|
|
6562
6635
|
return prev[n];
|
|
6563
6636
|
}
|
|
6564
|
-
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
|
|
6637
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
|
|
6565
6638
|
const findings = [];
|
|
6566
6639
|
if (declared === void 0 || declared === null) return findings;
|
|
6567
6640
|
if (!objectName) return findings;
|
|
@@ -6569,10 +6642,23 @@ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path,
|
|
|
6569
6642
|
const target = fieldsByObject.get(objectName);
|
|
6570
6643
|
if (!target) return findings;
|
|
6571
6644
|
const known = target.names;
|
|
6645
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
6572
6646
|
for (const key of readSortKeys(declared)) {
|
|
6573
6647
|
const name = key.field;
|
|
6574
6648
|
const head = name.split(".")[0];
|
|
6575
|
-
if (SYSTEM_FIELDS.has(head))
|
|
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
|
+
}
|
|
6576
6662
|
if (!known.has(head)) {
|
|
6577
6663
|
const dotted = name.includes(".");
|
|
6578
6664
|
findings.push({
|
|
@@ -6605,9 +6691,18 @@ function validateSortableFields(stack) {
|
|
|
6605
6691
|
if (!isRec16(stack)) return findings;
|
|
6606
6692
|
const objects = Array.isArray(stack.objects) ? stack.objects : isRec16(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
6607
6693
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
6694
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
6608
6695
|
const check = (declared, objectName, where, path, subject) => {
|
|
6609
6696
|
findings.push(
|
|
6610
|
-
...checkSortDeclaration(
|
|
6697
|
+
...checkSortDeclaration(
|
|
6698
|
+
declared,
|
|
6699
|
+
objectName,
|
|
6700
|
+
fieldsByObject,
|
|
6701
|
+
where,
|
|
6702
|
+
path,
|
|
6703
|
+
subject,
|
|
6704
|
+
unprovisionedAnchors
|
|
6705
|
+
)
|
|
6611
6706
|
);
|
|
6612
6707
|
};
|
|
6613
6708
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
@@ -6636,6 +6731,24 @@ function validateSortableFields(stack) {
|
|
|
6636
6731
|
if (!isRec16(view)) continue;
|
|
6637
6732
|
const viewLabel2 = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
|
|
6638
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
|
+
}
|
|
6639
6752
|
if (isRec16(view.list)) {
|
|
6640
6753
|
check(
|
|
6641
6754
|
view.list.sort,
|
|
@@ -8543,6 +8656,7 @@ function loadTypeScript3() {
|
|
|
8543
8656
|
return cachedTs3;
|
|
8544
8657
|
}
|
|
8545
8658
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
8659
|
+
var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
|
|
8546
8660
|
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
8547
8661
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
8548
8662
|
{
|
|
@@ -8656,15 +8770,18 @@ function extractHookBodyWriteSet(source) {
|
|
|
8656
8770
|
return { writes: [], ctxRecordEscapes: false };
|
|
8657
8771
|
}
|
|
8658
8772
|
const tsc = loadTypeScript3();
|
|
8659
|
-
const sf =
|
|
8773
|
+
const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
|
|
8774
|
+
tsc,
|
|
8660
8775
|
"hook-body.ts",
|
|
8661
8776
|
`async function __body(ctx) {
|
|
8662
8777
|
${source}
|
|
8663
8778
|
}`,
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8779
|
+
{
|
|
8780
|
+
target: tsc.ScriptTarget.Latest,
|
|
8781
|
+
setParentNodes: false,
|
|
8782
|
+
scriptKind: tsc.ScriptKind.TS,
|
|
8783
|
+
synthesizedLinesBefore: 1
|
|
8784
|
+
}
|
|
8668
8785
|
);
|
|
8669
8786
|
const writes = [];
|
|
8670
8787
|
const recordRefs = [];
|
|
@@ -8761,7 +8878,8 @@ ${source}
|
|
|
8761
8878
|
visit(sf);
|
|
8762
8879
|
return {
|
|
8763
8880
|
writes,
|
|
8764
|
-
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
8881
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
|
|
8882
|
+
...parseFailure ? { parseFailure } : {}
|
|
8765
8883
|
};
|
|
8766
8884
|
}
|
|
8767
8885
|
function validateHookBodyWrites(stack) {
|
|
@@ -8775,11 +8893,22 @@ function validateHookBodyWrites(stack) {
|
|
|
8775
8893
|
if (!isRec23(body) || body.language !== "js") return;
|
|
8776
8894
|
const source = body.source;
|
|
8777
8895
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
8778
|
-
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));
|
|
8779
8909
|
if (writes.length === 0) return;
|
|
8780
8910
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
8781
8911
|
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
8782
|
-
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
8783
8912
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
8784
8913
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
8785
8914
|
);
|
|
@@ -8864,6 +8993,7 @@ function fixHint(field, declared) {
|
|
|
8864
8993
|
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
8865
8994
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
8866
8995
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
8996
|
+
var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
|
|
8867
8997
|
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
8868
8998
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
8869
8999
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
@@ -8931,11 +9061,21 @@ function validateActionBodyWrites(stack) {
|
|
|
8931
9061
|
let anchors = null;
|
|
8932
9062
|
for (const site of sites) {
|
|
8933
9063
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
8934
|
-
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
9064
|
+
const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
|
|
8935
9065
|
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
8936
9066
|
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
8937
|
-
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
8938
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;
|
|
8939
9079
|
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
8940
9080
|
const reportedFields = /* @__PURE__ */ new Set();
|
|
8941
9081
|
for (const w of recordWrites) {
|
|
@@ -9076,16 +9216,26 @@ function fixHint3(field, declared) {
|
|
|
9076
9216
|
}
|
|
9077
9217
|
|
|
9078
9218
|
// src/reference-integrity-suite.ts
|
|
9219
|
+
var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
|
|
9079
9220
|
var REFERENCE_INTEGRITY_RULES = [
|
|
9080
9221
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
9081
|
-
|
|
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 },
|
|
9082
9232
|
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
9083
9233
|
// name written in metadata, resolved against the object's declared fields. It
|
|
9084
9234
|
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
9085
9235
|
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
9086
9236
|
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
9087
9237
|
// the refusal is the whole view, on every load, traced to nothing.
|
|
9088
|
-
{ name: "validateSortableFields", run: validateSortableFields },
|
|
9238
|
+
{ name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
|
|
9089
9239
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
9090
9240
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
9091
9241
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -9191,9 +9341,11 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
9191
9341
|
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
9192
9342
|
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
9193
9343
|
];
|
|
9194
|
-
function validateReferenceIntegrity(stack) {
|
|
9344
|
+
function validateReferenceIntegrity(stack, options) {
|
|
9195
9345
|
const findings = [];
|
|
9346
|
+
const writeType = options?.runtimeWriteType;
|
|
9196
9347
|
for (const rule of REFERENCE_INTEGRITY_RULES) {
|
|
9348
|
+
if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
|
|
9197
9349
|
findings.push(...rule.run(stack));
|
|
9198
9350
|
}
|
|
9199
9351
|
return findings;
|
|
@@ -10585,9 +10737,31 @@ var AUTHORING_RULES = [
|
|
|
10585
10737
|
// collection in the snapshot and return nothing, and the two that would
|
|
10586
10738
|
// load `typescript` need a hook/action/react body the snapshot never
|
|
10587
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.
|
|
10588
10762
|
surfaces: CLI_AND_RUNTIME,
|
|
10589
|
-
runtimeTypes: ["flow"],
|
|
10590
|
-
run: (stack) => validateReferenceIntegrity(stack)
|
|
10763
|
+
runtimeTypes: ["flow", "view"],
|
|
10764
|
+
run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
|
|
10591
10765
|
},
|
|
10592
10766
|
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
10593
10767
|
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
@@ -11434,6 +11608,20 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
11434
11608
|
};
|
|
11435
11609
|
return { baseline, candidate };
|
|
11436
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
|
+
}
|
|
11437
11625
|
function runRules(rules, stack, ctx) {
|
|
11438
11626
|
const findings = [];
|
|
11439
11627
|
for (const rule of rules) {
|
|
@@ -11463,9 +11651,12 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11463
11651
|
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
11464
11652
|
});
|
|
11465
11653
|
if (!snapshots) return empty;
|
|
11466
|
-
const ctx = { sduiManifest: args.sduiManifest };
|
|
11654
|
+
const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
|
|
11467
11655
|
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
11468
|
-
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
|
+
});
|
|
11469
11660
|
return {
|
|
11470
11661
|
errors: added.filter((f) => f.severity === "error"),
|
|
11471
11662
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -11473,6 +11664,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11473
11664
|
};
|
|
11474
11665
|
}
|
|
11475
11666
|
export {
|
|
11667
|
+
ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
11476
11668
|
ACTION_BODY_WRITE_EXCLUSIONS,
|
|
11477
11669
|
ACTION_BODY_WRITE_PATTERNS,
|
|
11478
11670
|
ACTION_BODY_WRITE_PATTERN_IDS,
|
|
@@ -11559,6 +11751,7 @@ export {
|
|
|
11559
11751
|
FLOW_WRITE_NODE_TYPES_DEFERRED,
|
|
11560
11752
|
FORM_COLSPAN_ABSOLUTE,
|
|
11561
11753
|
FORM_FIELD_UNKNOWN,
|
|
11754
|
+
HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
11562
11755
|
HOOK_BODY_WRITE_EXCLUSIONS,
|
|
11563
11756
|
HOOK_BODY_WRITE_PATTERNS,
|
|
11564
11757
|
HOOK_BODY_WRITE_PATTERN_IDS,
|
|
@@ -11582,6 +11775,7 @@ export {
|
|
|
11582
11775
|
PAGE_FIELD_UNKNOWN,
|
|
11583
11776
|
PAGE_FIELD_UNPROVISIONED,
|
|
11584
11777
|
PAGE_SOURCE_CLASSNAME,
|
|
11778
|
+
PARSE_FAILURE_HINT,
|
|
11585
11779
|
PREDICATE_PATH_UNRESOLVED,
|
|
11586
11780
|
PREDICATE_PATH_UNROOTED,
|
|
11587
11781
|
PREDICATE_RHS_PATH_SHAPED,
|
|
@@ -11592,6 +11786,7 @@ export {
|
|
|
11592
11786
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
11593
11787
|
REACT_CHART_FIELD_UNKNOWN,
|
|
11594
11788
|
REACT_CHART_FIELD_UNPROVISIONED,
|
|
11789
|
+
REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
11595
11790
|
REFERENCE_INTEGRITY_RULES,
|
|
11596
11791
|
RLS_PREDICATE_OVER_BUDGET,
|
|
11597
11792
|
RLS_PREDICATE_UNENFORCEABLE,
|
|
@@ -11623,8 +11818,10 @@ export {
|
|
|
11623
11818
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
11624
11819
|
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
11625
11820
|
SORT_FIELD_UNKNOWN,
|
|
11821
|
+
SORT_FIELD_UNPROVISIONED,
|
|
11626
11822
|
SORT_FIELD_UNSORTABLE,
|
|
11627
11823
|
STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
11824
|
+
STARTUP_SOURCE_UNPARSEABLE,
|
|
11628
11825
|
STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
11629
11826
|
STARTUP_VERDICT_HINT,
|
|
11630
11827
|
STYLE_CLASSNAME_TAILWIND,
|
|
@@ -11661,6 +11858,7 @@ export {
|
|
|
11661
11858
|
buildAccessMatrix,
|
|
11662
11859
|
buildRuntimeWriteSnapshots,
|
|
11663
11860
|
checkSortDeclaration,
|
|
11861
|
+
describeParseFailure,
|
|
11664
11862
|
diffAccessMatrix,
|
|
11665
11863
|
extractHookBodyWriteSet,
|
|
11666
11864
|
extractHookBodyWrites,
|