@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/runtime.d.cts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-
|
|
1
|
+
export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-cV-l_vtC.cjs';
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-
|
|
1
|
+
export { d as AuthoringFinding, j as AuthoringSeverity, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, v as stackKeyForType } from './runtime-cV-l_vtC.js';
|
package/dist/runtime.js
CHANGED
|
@@ -2185,6 +2185,26 @@ function validateSearchableFields(stack) {
|
|
|
2185
2185
|
if (!isRec3(view)) continue;
|
|
2186
2186
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
2187
2187
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
2188
|
+
if (view.viewKind === "list" && !isRec3(view.config)) {
|
|
2189
|
+
check(
|
|
2190
|
+
view.searchableFields,
|
|
2191
|
+
listViewObject(view) ?? viewObject,
|
|
2192
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2193
|
+
`views[${vi}].searchableFields`,
|
|
2194
|
+
"list-view searchableFields",
|
|
2195
|
+
"narrowing"
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
if (view.viewKind === "list" && isRec3(view.config)) {
|
|
2199
|
+
check(
|
|
2200
|
+
view.config.searchableFields,
|
|
2201
|
+
listViewObject(view.config) ?? viewObject,
|
|
2202
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2203
|
+
`views[${vi}].config.searchableFields`,
|
|
2204
|
+
"list-view searchableFields",
|
|
2205
|
+
"narrowing"
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2188
2208
|
if (isRec3(view.list)) {
|
|
2189
2209
|
check(
|
|
2190
2210
|
view.list.searchableFields,
|
|
@@ -2220,6 +2240,7 @@ function listViewObject(listView) {
|
|
|
2220
2240
|
import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
|
|
2221
2241
|
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
2222
2242
|
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
2243
|
+
var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
|
|
2223
2244
|
function isRec4(v) {
|
|
2224
2245
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2225
2246
|
}
|
|
@@ -2282,7 +2303,7 @@ function distance3(a, b) {
|
|
|
2282
2303
|
}
|
|
2283
2304
|
return prev[n];
|
|
2284
2305
|
}
|
|
2285
|
-
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
|
|
2306
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
|
|
2286
2307
|
const findings = [];
|
|
2287
2308
|
if (declared === void 0 || declared === null) return findings;
|
|
2288
2309
|
if (!objectName) return findings;
|
|
@@ -2290,10 +2311,23 @@ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path,
|
|
|
2290
2311
|
const target = fieldsByObject.get(objectName);
|
|
2291
2312
|
if (!target) return findings;
|
|
2292
2313
|
const known = target.names;
|
|
2314
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2293
2315
|
for (const key of readSortKeys(declared)) {
|
|
2294
2316
|
const name = key.field;
|
|
2295
2317
|
const head = name.split(".")[0];
|
|
2296
|
-
if (SYSTEM_FIELDS.has(head))
|
|
2318
|
+
if (SYSTEM_FIELDS.has(head)) {
|
|
2319
|
+
if (!name.includes(".") && anchors?.has(head)) {
|
|
2320
|
+
findings.push({
|
|
2321
|
+
severity: "warning",
|
|
2322
|
+
rule: SORT_FIELD_UNPROVISIONED,
|
|
2323
|
+
where,
|
|
2324
|
+
path: `${path}${key.at}`,
|
|
2325
|
+
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.`,
|
|
2326
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2297
2331
|
if (!known.has(head)) {
|
|
2298
2332
|
const dotted = name.includes(".");
|
|
2299
2333
|
findings.push({
|
|
@@ -2326,9 +2360,18 @@ function validateSortableFields(stack) {
|
|
|
2326
2360
|
if (!isRec4(stack)) return findings;
|
|
2327
2361
|
const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
2328
2362
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2363
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2329
2364
|
const check = (declared, objectName, where, path, subject) => {
|
|
2330
2365
|
findings.push(
|
|
2331
|
-
...checkSortDeclaration(
|
|
2366
|
+
...checkSortDeclaration(
|
|
2367
|
+
declared,
|
|
2368
|
+
objectName,
|
|
2369
|
+
fieldsByObject,
|
|
2370
|
+
where,
|
|
2371
|
+
path,
|
|
2372
|
+
subject,
|
|
2373
|
+
unprovisionedAnchors
|
|
2374
|
+
)
|
|
2332
2375
|
);
|
|
2333
2376
|
};
|
|
2334
2377
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
@@ -2357,6 +2400,24 @@ function validateSortableFields(stack) {
|
|
|
2357
2400
|
if (!isRec4(view)) continue;
|
|
2358
2401
|
const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
|
|
2359
2402
|
const viewObject = strName4(view.objectName) ?? strName4(view.object);
|
|
2403
|
+
if (view.viewKind === "list" && !isRec4(view.config)) {
|
|
2404
|
+
check(
|
|
2405
|
+
view.sort,
|
|
2406
|
+
listViewObject2(view) ?? viewObject,
|
|
2407
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2408
|
+
`views[${vi}].sort`,
|
|
2409
|
+
"list-view sort"
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
if (view.viewKind === "list" && isRec4(view.config)) {
|
|
2413
|
+
check(
|
|
2414
|
+
view.config.sort,
|
|
2415
|
+
listViewObject2(view.config) ?? viewObject,
|
|
2416
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2417
|
+
`views[${vi}].config.sort`,
|
|
2418
|
+
"list-view sort"
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2360
2421
|
if (isRec4(view.list)) {
|
|
2361
2422
|
check(
|
|
2362
2423
|
view.list.sort,
|
|
@@ -4537,6 +4598,38 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4537
4598
|
// src/validate-hook-body-writes.ts
|
|
4538
4599
|
import { createRequire } from "module";
|
|
4539
4600
|
import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
|
|
4601
|
+
|
|
4602
|
+
// src/checked-parse.ts
|
|
4603
|
+
function createSourceFileChecked(tsc, fileName, source, options) {
|
|
4604
|
+
const sourceFile = tsc.createSourceFile(
|
|
4605
|
+
fileName,
|
|
4606
|
+
source,
|
|
4607
|
+
options.target,
|
|
4608
|
+
options.setParentNodes,
|
|
4609
|
+
options.scriptKind
|
|
4610
|
+
);
|
|
4611
|
+
const diagnostics = sourceFile.parseDiagnostics;
|
|
4612
|
+
if (!diagnostics || diagnostics.length === 0) return { sourceFile };
|
|
4613
|
+
const first = diagnostics[0];
|
|
4614
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
|
|
4615
|
+
const offset = options.synthesizedLinesBefore ?? 0;
|
|
4616
|
+
return {
|
|
4617
|
+
sourceFile,
|
|
4618
|
+
failure: {
|
|
4619
|
+
message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
|
|
4620
|
+
line: Math.max(1, line + 1 - offset),
|
|
4621
|
+
column: character + 1,
|
|
4622
|
+
count: diagnostics.length
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
function describeParseFailure(failure) {
|
|
4627
|
+
const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
|
|
4628
|
+
return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
|
|
4629
|
+
}
|
|
4630
|
+
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.";
|
|
4631
|
+
|
|
4632
|
+
// src/validate-hook-body-writes.ts
|
|
4540
4633
|
var cachedTs = null;
|
|
4541
4634
|
function loadTypeScript() {
|
|
4542
4635
|
if (cachedTs) return cachedTs;
|
|
@@ -4551,6 +4644,7 @@ function loadTypeScript() {
|
|
|
4551
4644
|
return cachedTs;
|
|
4552
4645
|
}
|
|
4553
4646
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
4647
|
+
var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
|
|
4554
4648
|
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
4555
4649
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
4556
4650
|
{
|
|
@@ -4650,23 +4744,23 @@ function judgeableFieldsOf(index, objectName) {
|
|
|
4650
4744
|
if (!declared || declared.size === 0) return void 0;
|
|
4651
4745
|
return declared;
|
|
4652
4746
|
}
|
|
4653
|
-
function extractHookBodyWrites(source) {
|
|
4654
|
-
return extractHookBodyWriteSet(source).writes;
|
|
4655
|
-
}
|
|
4656
4747
|
function extractHookBodyWriteSet(source) {
|
|
4657
4748
|
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
4658
4749
|
return { writes: [], ctxRecordEscapes: false };
|
|
4659
4750
|
}
|
|
4660
4751
|
const tsc = loadTypeScript();
|
|
4661
|
-
const sf =
|
|
4752
|
+
const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
|
|
4753
|
+
tsc,
|
|
4662
4754
|
"hook-body.ts",
|
|
4663
4755
|
`async function __body(ctx) {
|
|
4664
4756
|
${source}
|
|
4665
4757
|
}`,
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4758
|
+
{
|
|
4759
|
+
target: tsc.ScriptTarget.Latest,
|
|
4760
|
+
setParentNodes: false,
|
|
4761
|
+
scriptKind: tsc.ScriptKind.TS,
|
|
4762
|
+
synthesizedLinesBefore: 1
|
|
4763
|
+
}
|
|
4670
4764
|
);
|
|
4671
4765
|
const writes = [];
|
|
4672
4766
|
const recordRefs = [];
|
|
@@ -4763,7 +4857,8 @@ ${source}
|
|
|
4763
4857
|
visit(sf);
|
|
4764
4858
|
return {
|
|
4765
4859
|
writes,
|
|
4766
|
-
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
4860
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
|
|
4861
|
+
...parseFailure ? { parseFailure } : {}
|
|
4767
4862
|
};
|
|
4768
4863
|
}
|
|
4769
4864
|
function validateHookBodyWrites(stack) {
|
|
@@ -4777,11 +4872,22 @@ function validateHookBodyWrites(stack) {
|
|
|
4777
4872
|
if (!isRec15(body) || body.language !== "js") return;
|
|
4778
4873
|
const source = body.source;
|
|
4779
4874
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4780
|
-
const
|
|
4875
|
+
const extracted = extractHookBodyWriteSet(source);
|
|
4876
|
+
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4877
|
+
if (extracted.parseFailure) {
|
|
4878
|
+
findings.push({
|
|
4879
|
+
severity: "warning",
|
|
4880
|
+
rule: HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
4881
|
+
where: `hook "${hookName}" \u203A body`,
|
|
4882
|
+
path: `hooks[${hookIndex}].body.source`,
|
|
4883
|
+
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.`,
|
|
4884
|
+
hint: PARSE_FAILURE_HINT
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
4781
4888
|
if (writes.length === 0) return;
|
|
4782
4889
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4783
4890
|
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4784
|
-
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4785
4891
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
4786
4892
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
4787
4893
|
);
|
|
@@ -4866,6 +4972,7 @@ function fixHint(field, declared) {
|
|
|
4866
4972
|
import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
|
|
4867
4973
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
4868
4974
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
4975
|
+
var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
|
|
4869
4976
|
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
4870
4977
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
4871
4978
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
@@ -4923,11 +5030,21 @@ function validateActionBodyWrites(stack) {
|
|
|
4923
5030
|
let anchors = null;
|
|
4924
5031
|
for (const site of sites) {
|
|
4925
5032
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
4926
|
-
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
5033
|
+
const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
|
|
4927
5034
|
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
4928
5035
|
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
4929
|
-
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4930
5036
|
const where = `action "${site.name}" \u203A body`;
|
|
5037
|
+
if (parseFailure) {
|
|
5038
|
+
findings.push({
|
|
5039
|
+
severity: "warning",
|
|
5040
|
+
rule: ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
5041
|
+
where,
|
|
5042
|
+
path: site.path,
|
|
5043
|
+
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.`,
|
|
5044
|
+
hint: PARSE_FAILURE_HINT
|
|
5045
|
+
});
|
|
5046
|
+
}
|
|
5047
|
+
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4931
5048
|
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
4932
5049
|
const reportedFields = /* @__PURE__ */ new Set();
|
|
4933
5050
|
for (const w of recordWrites) {
|
|
@@ -5307,6 +5424,7 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
5307
5424
|
};
|
|
5308
5425
|
return perPosition(init.expression);
|
|
5309
5426
|
}
|
|
5427
|
+
var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
|
|
5310
5428
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
5311
5429
|
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
5312
5430
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
@@ -5635,11 +5753,20 @@ function validateReactPageProps(stack) {
|
|
|
5635
5753
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
5636
5754
|
const name = String(page.name ?? `#${p}`);
|
|
5637
5755
|
const tsc = loadTypeScript2();
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5756
|
+
const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
|
|
5757
|
+
target: tsc.ScriptTarget.Latest,
|
|
5758
|
+
setParentNodes: true,
|
|
5759
|
+
scriptKind: tsc.ScriptKind.TSX
|
|
5760
|
+
});
|
|
5761
|
+
if (failure) {
|
|
5762
|
+
findings.push({
|
|
5763
|
+
severity: "warning",
|
|
5764
|
+
rule: REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
5765
|
+
where: `page "${name}"`,
|
|
5766
|
+
path: `pages[${p}].source`,
|
|
5767
|
+
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.`,
|
|
5768
|
+
hint: PARSE_FAILURE_HINT
|
|
5769
|
+
});
|
|
5643
5770
|
}
|
|
5644
5771
|
const locals = localComponentNames(tsc, sf);
|
|
5645
5772
|
const visit = (node) => {
|
|
@@ -5742,16 +5869,26 @@ function validateReactPageProps(stack) {
|
|
|
5742
5869
|
}
|
|
5743
5870
|
|
|
5744
5871
|
// src/reference-integrity-suite.ts
|
|
5872
|
+
var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
|
|
5745
5873
|
var REFERENCE_INTEGRITY_RULES = [
|
|
5746
5874
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5747
|
-
|
|
5875
|
+
// [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
|
|
5876
|
+
// both judge a LIST VIEW's field references, and a standalone list view is
|
|
5877
|
+
// written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
|
|
5878
|
+
// an MCP/AI author has. Their walks read the flattened overlay shape that
|
|
5879
|
+
// door carries (see each rule's `views[]` self rung), and they resolve only
|
|
5880
|
+
// against `stack.objects`, which the per-write snapshot DOES carry — so the
|
|
5881
|
+
// crossing has no missing-collection false-positive channel. Measured over
|
|
5882
|
+
// the shipped view corpus before crossing (0 refusals; population in the
|
|
5883
|
+
// #9313 PR).
|
|
5884
|
+
{ name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
|
|
5748
5885
|
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
5749
5886
|
// name written in metadata, resolved against the object's declared fields. It
|
|
5750
5887
|
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
5751
5888
|
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
5752
5889
|
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
5753
5890
|
// the refusal is the whole view, on every load, traced to nothing.
|
|
5754
|
-
{ name: "validateSortableFields", run: validateSortableFields },
|
|
5891
|
+
{ name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
|
|
5755
5892
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
5756
5893
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5757
5894
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -5857,9 +5994,11 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5857
5994
|
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
5858
5995
|
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
5859
5996
|
];
|
|
5860
|
-
function validateReferenceIntegrity(stack) {
|
|
5997
|
+
function validateReferenceIntegrity(stack, options) {
|
|
5861
5998
|
const findings = [];
|
|
5999
|
+
const writeType = options?.runtimeWriteType;
|
|
5862
6000
|
for (const rule of REFERENCE_INTEGRITY_RULES) {
|
|
6001
|
+
if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
|
|
5863
6002
|
findings.push(...rule.run(stack));
|
|
5864
6003
|
}
|
|
5865
6004
|
return findings;
|
|
@@ -10016,9 +10155,31 @@ var AUTHORING_RULES = [
|
|
|
10016
10155
|
// collection in the snapshot and return nothing, and the two that would
|
|
10017
10156
|
// load `typescript` need a hook/action/react body the snapshot never
|
|
10018
10157
|
// carries — which is what `runtime-lazy-deps.test.ts` pins.
|
|
10158
|
+
//
|
|
10159
|
+
// [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
|
|
10160
|
+
// taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
|
|
10161
|
+
// dispatch the suite, and the suite's own per-member `runtimeTypes`
|
|
10162
|
+
// (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
|
|
10163
|
+
// that snapshot — the gate passes the written type through
|
|
10164
|
+
// `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
|
|
10165
|
+
// members that judge a list view's field references and resolve only
|
|
10166
|
+
// against the `objects` collection the snapshot carries
|
|
10167
|
+
// (`validateSearchableFields`, `validateSortableFields`). The whole suite
|
|
10168
|
+
// is NOT the right granularity for this door, measured not assumed, and
|
|
10169
|
+
// the crossing fails differently per body shape — both ways wrong:
|
|
10170
|
+
// `validateActionNameRefs` (error-tier) resolves `views[].list` /
|
|
10171
|
+
// `views[].listViews.*` action names against `stack.actions`, a collection
|
|
10172
|
+
// no per-write snapshot carries, so on a CONTAINER view write it would
|
|
10173
|
+
// refuse every stack-level action the body names (measured:
|
|
10174
|
+
// `action-name-undefined` on the snapshot shape, clean on the full stack)
|
|
10175
|
+
// — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
|
|
10176
|
+
// type the gate has. On a FLATTENED overlay it has no rung at all, so the
|
|
10177
|
+
// crossing would be a silent no-op that reads as coverage — the very shape
|
|
10178
|
+
// #9313 was filed about. Flow snapshots are unchanged: every member keeps
|
|
10179
|
+
// the default `flow` declaration.
|
|
10019
10180
|
surfaces: CLI_AND_RUNTIME,
|
|
10020
|
-
runtimeTypes: ["flow"],
|
|
10021
|
-
run: (stack) => validateReferenceIntegrity(stack)
|
|
10181
|
+
runtimeTypes: ["flow", "view"],
|
|
10182
|
+
run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
|
|
10022
10183
|
},
|
|
10023
10184
|
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
10024
10185
|
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
@@ -10847,6 +11008,20 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10847
11008
|
};
|
|
10848
11009
|
return { baseline, candidate };
|
|
10849
11010
|
}
|
|
11011
|
+
var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
|
|
11012
|
+
var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11013
|
+
var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
|
|
11014
|
+
function nameKeyFindingPath(path, candidate) {
|
|
11015
|
+
const m = TOP_LEVEL_INDEX.exec(path);
|
|
11016
|
+
if (!m) return path;
|
|
11017
|
+
const [, stackKey, index, rest] = m;
|
|
11018
|
+
if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
|
|
11019
|
+
const collection = candidate[stackKey];
|
|
11020
|
+
const entry = collection?.[Number(index)];
|
|
11021
|
+
const name = entry && typeof entry === "object" ? entry.name : void 0;
|
|
11022
|
+
if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
|
|
11023
|
+
return `${stackKey}.${name}${rest}`;
|
|
11024
|
+
}
|
|
10850
11025
|
function runRules(rules, stack, ctx) {
|
|
10851
11026
|
const findings = [];
|
|
10852
11027
|
for (const rule of rules) {
|
|
@@ -10876,9 +11051,12 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10876
11051
|
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
10877
11052
|
});
|
|
10878
11053
|
if (!snapshots) return empty;
|
|
10879
|
-
const ctx = { sduiManifest: args.sduiManifest };
|
|
11054
|
+
const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
|
|
10880
11055
|
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10881
|
-
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)))
|
|
11056
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
|
|
11057
|
+
const path = nameKeyFindingPath(f.path, snapshots.candidate);
|
|
11058
|
+
return path === f.path ? f : { ...f, path };
|
|
11059
|
+
});
|
|
10882
11060
|
return {
|
|
10883
11061
|
errors: added.filter((f) => f.severity === "error"),
|
|
10884
11062
|
advisories: added.filter((f) => f.severity !== "error"),
|