@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
|
@@ -35,7 +35,13 @@ interface AuthoringFinding {
|
|
|
35
35
|
rule: string;
|
|
36
36
|
/** Human-readable location, e.g. `object "leave_request"`. */
|
|
37
37
|
where: string;
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Config path, e.g. `objects[3].sharingModel`. Positional as RULES emit it;
|
|
40
|
+
* on the runtime gate's wire surface the top-level collection index of a
|
|
41
|
+
* collection-resident finding is rewritten to the entry's NAME
|
|
42
|
+
* (`objects.acme_invoice.sharingModel`) — see `nameKeyFindingPath` in
|
|
43
|
+
* `runtime-gate.ts` (#10064).
|
|
44
|
+
*/
|
|
39
45
|
path: string;
|
|
40
46
|
/** What is wrong. */
|
|
41
47
|
message: string;
|
|
@@ -117,6 +123,19 @@ type AuthoringRuleInputTier = 'normalized' | 'parsed';
|
|
|
117
123
|
interface AuthoringRuleContext {
|
|
118
124
|
/** ADR-0080 SDUI component manifest, when the project ships one. */
|
|
119
125
|
sduiManifest?: unknown;
|
|
126
|
+
/**
|
|
127
|
+
* [#9313] The singular metadata type of the per-write snapshot being judged
|
|
128
|
+
* — set by the runtime publish gate (`runtime-gate.ts`) on every gated
|
|
129
|
+
* write, ABSENT on the three CLI commands (`runAuthoringRules` never sets
|
|
130
|
+
* it). Exists for the one entry that is itself a registry: the
|
|
131
|
+
* reference-integrity suite dispatches its MEMBERS by this
|
|
132
|
+
* (`ReferenceIntegrityRule.runtimeTypes`), because the entry-level
|
|
133
|
+
* `runtimeTypes` can only say which writes reach the suite, not which
|
|
134
|
+
* members can judge a partial per-write snapshot without inventing
|
|
135
|
+
* findings. No other rule reads it, and none should without the same
|
|
136
|
+
* argument.
|
|
137
|
+
*/
|
|
138
|
+
runtimeWriteType?: string;
|
|
120
139
|
}
|
|
121
140
|
interface AuthoringRule {
|
|
122
141
|
/** The exported function's name — the id the wiring guard asserts on. */
|
|
@@ -35,7 +35,13 @@ interface AuthoringFinding {
|
|
|
35
35
|
rule: string;
|
|
36
36
|
/** Human-readable location, e.g. `object "leave_request"`. */
|
|
37
37
|
where: string;
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Config path, e.g. `objects[3].sharingModel`. Positional as RULES emit it;
|
|
40
|
+
* on the runtime gate's wire surface the top-level collection index of a
|
|
41
|
+
* collection-resident finding is rewritten to the entry's NAME
|
|
42
|
+
* (`objects.acme_invoice.sharingModel`) — see `nameKeyFindingPath` in
|
|
43
|
+
* `runtime-gate.ts` (#10064).
|
|
44
|
+
*/
|
|
39
45
|
path: string;
|
|
40
46
|
/** What is wrong. */
|
|
41
47
|
message: string;
|
|
@@ -117,6 +123,19 @@ type AuthoringRuleInputTier = 'normalized' | 'parsed';
|
|
|
117
123
|
interface AuthoringRuleContext {
|
|
118
124
|
/** ADR-0080 SDUI component manifest, when the project ships one. */
|
|
119
125
|
sduiManifest?: unknown;
|
|
126
|
+
/**
|
|
127
|
+
* [#9313] The singular metadata type of the per-write snapshot being judged
|
|
128
|
+
* — set by the runtime publish gate (`runtime-gate.ts`) on every gated
|
|
129
|
+
* write, ABSENT on the three CLI commands (`runAuthoringRules` never sets
|
|
130
|
+
* it). Exists for the one entry that is itself a registry: the
|
|
131
|
+
* reference-integrity suite dispatches its MEMBERS by this
|
|
132
|
+
* (`ReferenceIntegrityRule.runtimeTypes`), because the entry-level
|
|
133
|
+
* `runtimeTypes` can only say which writes reach the suite, not which
|
|
134
|
+
* members can judge a partial per-write snapshot without inventing
|
|
135
|
+
* findings. No other rule reads it, and none should without the same
|
|
136
|
+
* argument.
|
|
137
|
+
*/
|
|
138
|
+
runtimeWriteType?: string;
|
|
120
139
|
}
|
|
121
140
|
interface AuthoringRule {
|
|
122
141
|
/** The exported function's name — the id the wiring guard asserts on. */
|
package/dist/runtime.cjs
CHANGED
|
@@ -2190,6 +2190,26 @@ function validateSearchableFields(stack) {
|
|
|
2190
2190
|
if (!isRec3(view)) continue;
|
|
2191
2191
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
2192
2192
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
2193
|
+
if (view.viewKind === "list" && !isRec3(view.config)) {
|
|
2194
|
+
check(
|
|
2195
|
+
view.searchableFields,
|
|
2196
|
+
listViewObject(view) ?? viewObject,
|
|
2197
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2198
|
+
`views[${vi}].searchableFields`,
|
|
2199
|
+
"list-view searchableFields",
|
|
2200
|
+
"narrowing"
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
if (view.viewKind === "list" && isRec3(view.config)) {
|
|
2204
|
+
check(
|
|
2205
|
+
view.config.searchableFields,
|
|
2206
|
+
listViewObject(view.config) ?? viewObject,
|
|
2207
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2208
|
+
`views[${vi}].config.searchableFields`,
|
|
2209
|
+
"list-view searchableFields",
|
|
2210
|
+
"narrowing"
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2193
2213
|
if (isRec3(view.list)) {
|
|
2194
2214
|
check(
|
|
2195
2215
|
view.list.searchableFields,
|
|
@@ -2225,6 +2245,7 @@ function listViewObject(listView) {
|
|
|
2225
2245
|
var import_data8 = require("@objectstack/spec/data");
|
|
2226
2246
|
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
2227
2247
|
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
2248
|
+
var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
|
|
2228
2249
|
function isRec4(v) {
|
|
2229
2250
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2230
2251
|
}
|
|
@@ -2287,7 +2308,7 @@ function distance3(a, b) {
|
|
|
2287
2308
|
}
|
|
2288
2309
|
return prev[n];
|
|
2289
2310
|
}
|
|
2290
|
-
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
|
|
2311
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
|
|
2291
2312
|
const findings = [];
|
|
2292
2313
|
if (declared === void 0 || declared === null) return findings;
|
|
2293
2314
|
if (!objectName) return findings;
|
|
@@ -2295,10 +2316,23 @@ function checkSortDeclaration(declared, objectName, fieldsByObject, where, path,
|
|
|
2295
2316
|
const target = fieldsByObject.get(objectName);
|
|
2296
2317
|
if (!target) return findings;
|
|
2297
2318
|
const known = target.names;
|
|
2319
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2298
2320
|
for (const key of readSortKeys(declared)) {
|
|
2299
2321
|
const name = key.field;
|
|
2300
2322
|
const head = name.split(".")[0];
|
|
2301
|
-
if (SYSTEM_FIELDS.has(head))
|
|
2323
|
+
if (SYSTEM_FIELDS.has(head)) {
|
|
2324
|
+
if (!name.includes(".") && anchors?.has(head)) {
|
|
2325
|
+
findings.push({
|
|
2326
|
+
severity: "warning",
|
|
2327
|
+
rule: SORT_FIELD_UNPROVISIONED,
|
|
2328
|
+
where,
|
|
2329
|
+
path: `${path}${key.at}`,
|
|
2330
|
+
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.`,
|
|
2331
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2302
2336
|
if (!known.has(head)) {
|
|
2303
2337
|
const dotted = name.includes(".");
|
|
2304
2338
|
findings.push({
|
|
@@ -2331,9 +2365,18 @@ function validateSortableFields(stack) {
|
|
|
2331
2365
|
if (!isRec4(stack)) return findings;
|
|
2332
2366
|
const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
2333
2367
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2368
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2334
2369
|
const check = (declared, objectName, where, path, subject) => {
|
|
2335
2370
|
findings.push(
|
|
2336
|
-
...checkSortDeclaration(
|
|
2371
|
+
...checkSortDeclaration(
|
|
2372
|
+
declared,
|
|
2373
|
+
objectName,
|
|
2374
|
+
fieldsByObject,
|
|
2375
|
+
where,
|
|
2376
|
+
path,
|
|
2377
|
+
subject,
|
|
2378
|
+
unprovisionedAnchors
|
|
2379
|
+
)
|
|
2337
2380
|
);
|
|
2338
2381
|
};
|
|
2339
2382
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
@@ -2362,6 +2405,24 @@ function validateSortableFields(stack) {
|
|
|
2362
2405
|
if (!isRec4(view)) continue;
|
|
2363
2406
|
const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
|
|
2364
2407
|
const viewObject = strName4(view.objectName) ?? strName4(view.object);
|
|
2408
|
+
if (view.viewKind === "list" && !isRec4(view.config)) {
|
|
2409
|
+
check(
|
|
2410
|
+
view.sort,
|
|
2411
|
+
listViewObject2(view) ?? viewObject,
|
|
2412
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2413
|
+
`views[${vi}].sort`,
|
|
2414
|
+
"list-view sort"
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
if (view.viewKind === "list" && isRec4(view.config)) {
|
|
2418
|
+
check(
|
|
2419
|
+
view.config.sort,
|
|
2420
|
+
listViewObject2(view.config) ?? viewObject,
|
|
2421
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2422
|
+
`views[${vi}].config.sort`,
|
|
2423
|
+
"list-view sort"
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2365
2426
|
if (isRec4(view.list)) {
|
|
2366
2427
|
check(
|
|
2367
2428
|
view.list.sort,
|
|
@@ -4542,6 +4603,38 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4542
4603
|
// src/validate-hook-body-writes.ts
|
|
4543
4604
|
var import_node_module = require("module");
|
|
4544
4605
|
var import_shared = require("@objectstack/spec/shared");
|
|
4606
|
+
|
|
4607
|
+
// src/checked-parse.ts
|
|
4608
|
+
function createSourceFileChecked(tsc, fileName, source, options) {
|
|
4609
|
+
const sourceFile = tsc.createSourceFile(
|
|
4610
|
+
fileName,
|
|
4611
|
+
source,
|
|
4612
|
+
options.target,
|
|
4613
|
+
options.setParentNodes,
|
|
4614
|
+
options.scriptKind
|
|
4615
|
+
);
|
|
4616
|
+
const diagnostics = sourceFile.parseDiagnostics;
|
|
4617
|
+
if (!diagnostics || diagnostics.length === 0) return { sourceFile };
|
|
4618
|
+
const first = diagnostics[0];
|
|
4619
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
|
|
4620
|
+
const offset = options.synthesizedLinesBefore ?? 0;
|
|
4621
|
+
return {
|
|
4622
|
+
sourceFile,
|
|
4623
|
+
failure: {
|
|
4624
|
+
message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
|
|
4625
|
+
line: Math.max(1, line + 1 - offset),
|
|
4626
|
+
column: character + 1,
|
|
4627
|
+
count: diagnostics.length
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
4631
|
+
function describeParseFailure(failure) {
|
|
4632
|
+
const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
|
|
4633
|
+
return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
|
|
4634
|
+
}
|
|
4635
|
+
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.";
|
|
4636
|
+
|
|
4637
|
+
// src/validate-hook-body-writes.ts
|
|
4545
4638
|
var import_meta = {};
|
|
4546
4639
|
var cachedTs = null;
|
|
4547
4640
|
function loadTypeScript() {
|
|
@@ -4557,6 +4650,7 @@ function loadTypeScript() {
|
|
|
4557
4650
|
return cachedTs;
|
|
4558
4651
|
}
|
|
4559
4652
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
4653
|
+
var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
|
|
4560
4654
|
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
4561
4655
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
4562
4656
|
{
|
|
@@ -4656,23 +4750,23 @@ function judgeableFieldsOf(index, objectName) {
|
|
|
4656
4750
|
if (!declared || declared.size === 0) return void 0;
|
|
4657
4751
|
return declared;
|
|
4658
4752
|
}
|
|
4659
|
-
function extractHookBodyWrites(source) {
|
|
4660
|
-
return extractHookBodyWriteSet(source).writes;
|
|
4661
|
-
}
|
|
4662
4753
|
function extractHookBodyWriteSet(source) {
|
|
4663
4754
|
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
4664
4755
|
return { writes: [], ctxRecordEscapes: false };
|
|
4665
4756
|
}
|
|
4666
4757
|
const tsc = loadTypeScript();
|
|
4667
|
-
const sf =
|
|
4758
|
+
const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
|
|
4759
|
+
tsc,
|
|
4668
4760
|
"hook-body.ts",
|
|
4669
4761
|
`async function __body(ctx) {
|
|
4670
4762
|
${source}
|
|
4671
4763
|
}`,
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4764
|
+
{
|
|
4765
|
+
target: tsc.ScriptTarget.Latest,
|
|
4766
|
+
setParentNodes: false,
|
|
4767
|
+
scriptKind: tsc.ScriptKind.TS,
|
|
4768
|
+
synthesizedLinesBefore: 1
|
|
4769
|
+
}
|
|
4676
4770
|
);
|
|
4677
4771
|
const writes = [];
|
|
4678
4772
|
const recordRefs = [];
|
|
@@ -4769,7 +4863,8 @@ ${source}
|
|
|
4769
4863
|
visit(sf);
|
|
4770
4864
|
return {
|
|
4771
4865
|
writes,
|
|
4772
|
-
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
4866
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
|
|
4867
|
+
...parseFailure ? { parseFailure } : {}
|
|
4773
4868
|
};
|
|
4774
4869
|
}
|
|
4775
4870
|
function validateHookBodyWrites(stack) {
|
|
@@ -4783,11 +4878,22 @@ function validateHookBodyWrites(stack) {
|
|
|
4783
4878
|
if (!isRec15(body) || body.language !== "js") return;
|
|
4784
4879
|
const source = body.source;
|
|
4785
4880
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4786
|
-
const
|
|
4881
|
+
const extracted = extractHookBodyWriteSet(source);
|
|
4882
|
+
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4883
|
+
if (extracted.parseFailure) {
|
|
4884
|
+
findings.push({
|
|
4885
|
+
severity: "warning",
|
|
4886
|
+
rule: HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
4887
|
+
where: `hook "${hookName}" \u203A body`,
|
|
4888
|
+
path: `hooks[${hookIndex}].body.source`,
|
|
4889
|
+
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.`,
|
|
4890
|
+
hint: PARSE_FAILURE_HINT
|
|
4891
|
+
});
|
|
4892
|
+
}
|
|
4893
|
+
const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
4787
4894
|
if (writes.length === 0) return;
|
|
4788
4895
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4789
4896
|
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4790
|
-
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4791
4897
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
4792
4898
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
4793
4899
|
);
|
|
@@ -4872,6 +4978,7 @@ function fixHint(field, declared) {
|
|
|
4872
4978
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
4873
4979
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
4874
4980
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
4981
|
+
var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
|
|
4875
4982
|
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
4876
4983
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
4877
4984
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
@@ -4929,11 +5036,21 @@ function validateActionBodyWrites(stack) {
|
|
|
4929
5036
|
let anchors = null;
|
|
4930
5037
|
for (const site of sites) {
|
|
4931
5038
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
4932
|
-
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
5039
|
+
const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
|
|
4933
5040
|
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
4934
5041
|
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
4935
|
-
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4936
5042
|
const where = `action "${site.name}" \u203A body`;
|
|
5043
|
+
if (parseFailure) {
|
|
5044
|
+
findings.push({
|
|
5045
|
+
severity: "warning",
|
|
5046
|
+
rule: ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
5047
|
+
where,
|
|
5048
|
+
path: site.path,
|
|
5049
|
+
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.`,
|
|
5050
|
+
hint: PARSE_FAILURE_HINT
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4937
5054
|
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
4938
5055
|
const reportedFields = /* @__PURE__ */ new Set();
|
|
4939
5056
|
for (const w of recordWrites) {
|
|
@@ -5306,6 +5423,7 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
5306
5423
|
};
|
|
5307
5424
|
return perPosition(init.expression);
|
|
5308
5425
|
}
|
|
5426
|
+
var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
|
|
5309
5427
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
5310
5428
|
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
5311
5429
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
@@ -5634,11 +5752,20 @@ function validateReactPageProps(stack) {
|
|
|
5634
5752
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
5635
5753
|
const name = String(page.name ?? `#${p}`);
|
|
5636
5754
|
const tsc = loadTypeScript2();
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5755
|
+
const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
|
|
5756
|
+
target: tsc.ScriptTarget.Latest,
|
|
5757
|
+
setParentNodes: true,
|
|
5758
|
+
scriptKind: tsc.ScriptKind.TSX
|
|
5759
|
+
});
|
|
5760
|
+
if (failure) {
|
|
5761
|
+
findings.push({
|
|
5762
|
+
severity: "warning",
|
|
5763
|
+
rule: REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
5764
|
+
where: `page "${name}"`,
|
|
5765
|
+
path: `pages[${p}].source`,
|
|
5766
|
+
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.`,
|
|
5767
|
+
hint: PARSE_FAILURE_HINT
|
|
5768
|
+
});
|
|
5642
5769
|
}
|
|
5643
5770
|
const locals = localComponentNames(tsc, sf);
|
|
5644
5771
|
const visit = (node) => {
|
|
@@ -5741,16 +5868,26 @@ function validateReactPageProps(stack) {
|
|
|
5741
5868
|
}
|
|
5742
5869
|
|
|
5743
5870
|
// src/reference-integrity-suite.ts
|
|
5871
|
+
var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
|
|
5744
5872
|
var REFERENCE_INTEGRITY_RULES = [
|
|
5745
5873
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5746
|
-
|
|
5874
|
+
// [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
|
|
5875
|
+
// both judge a LIST VIEW's field references, and a standalone list view is
|
|
5876
|
+
// written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
|
|
5877
|
+
// an MCP/AI author has. Their walks read the flattened overlay shape that
|
|
5878
|
+
// door carries (see each rule's `views[]` self rung), and they resolve only
|
|
5879
|
+
// against `stack.objects`, which the per-write snapshot DOES carry — so the
|
|
5880
|
+
// crossing has no missing-collection false-positive channel. Measured over
|
|
5881
|
+
// the shipped view corpus before crossing (0 refusals; population in the
|
|
5882
|
+
// #9313 PR).
|
|
5883
|
+
{ name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
|
|
5747
5884
|
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
5748
5885
|
// name written in metadata, resolved against the object's declared fields. It
|
|
5749
5886
|
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
5750
5887
|
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
5751
5888
|
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
5752
5889
|
// the refusal is the whole view, on every load, traced to nothing.
|
|
5753
|
-
{ name: "validateSortableFields", run: validateSortableFields },
|
|
5890
|
+
{ name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
|
|
5754
5891
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
5755
5892
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5756
5893
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -5856,9 +5993,11 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5856
5993
|
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
5857
5994
|
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
5858
5995
|
];
|
|
5859
|
-
function validateReferenceIntegrity(stack) {
|
|
5996
|
+
function validateReferenceIntegrity(stack, options) {
|
|
5860
5997
|
const findings = [];
|
|
5998
|
+
const writeType = options?.runtimeWriteType;
|
|
5861
5999
|
for (const rule of REFERENCE_INTEGRITY_RULES) {
|
|
6000
|
+
if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
|
|
5862
6001
|
findings.push(...rule.run(stack));
|
|
5863
6002
|
}
|
|
5864
6003
|
return findings;
|
|
@@ -9996,9 +10135,31 @@ var AUTHORING_RULES = [
|
|
|
9996
10135
|
// collection in the snapshot and return nothing, and the two that would
|
|
9997
10136
|
// load `typescript` need a hook/action/react body the snapshot never
|
|
9998
10137
|
// carries — which is what `runtime-lazy-deps.test.ts` pins.
|
|
10138
|
+
//
|
|
10139
|
+
// [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
|
|
10140
|
+
// taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
|
|
10141
|
+
// dispatch the suite, and the suite's own per-member `runtimeTypes`
|
|
10142
|
+
// (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
|
|
10143
|
+
// that snapshot — the gate passes the written type through
|
|
10144
|
+
// `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
|
|
10145
|
+
// members that judge a list view's field references and resolve only
|
|
10146
|
+
// against the `objects` collection the snapshot carries
|
|
10147
|
+
// (`validateSearchableFields`, `validateSortableFields`). The whole suite
|
|
10148
|
+
// is NOT the right granularity for this door, measured not assumed, and
|
|
10149
|
+
// the crossing fails differently per body shape — both ways wrong:
|
|
10150
|
+
// `validateActionNameRefs` (error-tier) resolves `views[].list` /
|
|
10151
|
+
// `views[].listViews.*` action names against `stack.actions`, a collection
|
|
10152
|
+
// no per-write snapshot carries, so on a CONTAINER view write it would
|
|
10153
|
+
// refuse every stack-level action the body names (measured:
|
|
10154
|
+
// `action-name-undefined` on the snapshot shape, clean on the full stack)
|
|
10155
|
+
// — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
|
|
10156
|
+
// type the gate has. On a FLATTENED overlay it has no rung at all, so the
|
|
10157
|
+
// crossing would be a silent no-op that reads as coverage — the very shape
|
|
10158
|
+
// #9313 was filed about. Flow snapshots are unchanged: every member keeps
|
|
10159
|
+
// the default `flow` declaration.
|
|
9999
10160
|
surfaces: CLI_AND_RUNTIME,
|
|
10000
|
-
runtimeTypes: ["flow"],
|
|
10001
|
-
run: (stack) => validateReferenceIntegrity(stack)
|
|
10161
|
+
runtimeTypes: ["flow", "view"],
|
|
10162
|
+
run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
|
|
10002
10163
|
},
|
|
10003
10164
|
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
10004
10165
|
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
@@ -10827,6 +10988,20 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10827
10988
|
};
|
|
10828
10989
|
return { baseline, candidate };
|
|
10829
10990
|
}
|
|
10991
|
+
var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
|
|
10992
|
+
var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10993
|
+
var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
|
|
10994
|
+
function nameKeyFindingPath(path, candidate) {
|
|
10995
|
+
const m = TOP_LEVEL_INDEX.exec(path);
|
|
10996
|
+
if (!m) return path;
|
|
10997
|
+
const [, stackKey, index, rest] = m;
|
|
10998
|
+
if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
|
|
10999
|
+
const collection = candidate[stackKey];
|
|
11000
|
+
const entry = collection?.[Number(index)];
|
|
11001
|
+
const name = entry && typeof entry === "object" ? entry.name : void 0;
|
|
11002
|
+
if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
|
|
11003
|
+
return `${stackKey}.${name}${rest}`;
|
|
11004
|
+
}
|
|
10830
11005
|
function runRules(rules, stack, ctx) {
|
|
10831
11006
|
const findings = [];
|
|
10832
11007
|
for (const rule of rules) {
|
|
@@ -10856,9 +11031,12 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10856
11031
|
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
10857
11032
|
});
|
|
10858
11033
|
if (!snapshots) return empty;
|
|
10859
|
-
const ctx = { sduiManifest: args.sduiManifest };
|
|
11034
|
+
const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
|
|
10860
11035
|
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10861
|
-
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)))
|
|
11036
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
|
|
11037
|
+
const path = nameKeyFindingPath(f.path, snapshots.candidate);
|
|
11038
|
+
return path === f.path ? f : { ...f, path };
|
|
11039
|
+
});
|
|
10862
11040
|
return {
|
|
10863
11041
|
errors: added.filter((f) => f.severity === "error"),
|
|
10864
11042
|
advisories: added.filter((f) => f.severity !== "error"),
|