@openclaw/plugin-inspector 0.3.13 → 0.3.14
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 +5 -1
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/fixture-summary.js +30 -0
- package/src/inspector.js +17 -0
- package/src/issues.js +24 -1
- package/src/report.js +3 -0
- package/src/sdk-deprecation-rules.js +369 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.3.14 - 2026-06-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Flag deprecated `loadSessionStore(...)` whole-store session helper usage as an author-facing deprecation warning while keeping speculative transcript-identity migration rules out of default inspection. Thanks @jalehman.
|
|
4
8
|
|
|
5
9
|
## 0.3.13 - 2026-06-09
|
|
6
10
|
|
package/README.md
CHANGED
|
@@ -299,7 +299,7 @@ Important report sections:
|
|
|
299
299
|
| `status` | `pass` unless hard breakages exist. |
|
|
300
300
|
| `summary` | Counts for fixtures, breakages, warnings, suggestions, issues, issue classes, and contract probes. |
|
|
301
301
|
| `targetOpenClaw` | Status and public compatibility data read from the optional OpenClaw checkout. |
|
|
302
|
-
| `fixtures` | Per-plugin metadata, hooks, registrations, manifest contracts, package data, and SDK
|
|
302
|
+
| `fixtures` | Per-plugin metadata, hooks, registrations, manifest contracts, package data, SDK imports, and SDK deprecation evidence. |
|
|
303
303
|
| `breakages` | Blocking compatibility failures. |
|
|
304
304
|
| `warnings` / `suggestions` | Non-blocking compatibility findings. |
|
|
305
305
|
| `issues` | Normalized issue rows with severity and class. |
|
|
@@ -311,6 +311,9 @@ Default `check`, `ci`, and `batch` reports include both author-facing and
|
|
|
311
311
|
internal findings. Pass `--author-facing` when producing plugin-author output;
|
|
312
312
|
that filtered view includes only findings with `authorRemediation.summary` and
|
|
313
313
|
`authorRemediation.docsUrl`.
|
|
314
|
+
Current author-facing deprecation warnings include deprecated SDK helpers such
|
|
315
|
+
as `loadSessionStore(...)` when a plugin still depends on the legacy whole-store
|
|
316
|
+
session shape.
|
|
314
317
|
|
|
315
318
|
## CI Policy And Shared Reporting Primitives
|
|
316
319
|
|
package/package.json
CHANGED
package/src/fixture-summary.js
CHANGED
|
@@ -49,6 +49,7 @@ export async function buildCompatibilityFixtureReport({ fixture, inspection, che
|
|
|
49
49
|
packages: packageSummaries,
|
|
50
50
|
sdkImports,
|
|
51
51
|
sdkImportDetails: inspection.sdkImports ?? [],
|
|
52
|
+
sdkDeprecations: inspection.sdkDeprecations ?? [],
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -496,6 +497,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
496
497
|
logs.push(...packageContracts.logs);
|
|
497
498
|
decisions.push(...packageContracts.decisions);
|
|
498
499
|
classifySecurityManifestCoverage({ fixture, fixtureReport, warnings, decisions });
|
|
500
|
+
classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings, decisions });
|
|
499
501
|
|
|
500
502
|
for (const pluginManifest of fixtureReport.pluginManifests) {
|
|
501
503
|
const providerAuthKeys = Object.keys(pluginManifest.providerAuthEnvVars ?? {});
|
|
@@ -702,6 +704,34 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
702
704
|
return { warnings, suggestions, logs, decisions };
|
|
703
705
|
}
|
|
704
706
|
|
|
707
|
+
function classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings, decisions }) {
|
|
708
|
+
const grouped = new Map();
|
|
709
|
+
for (const finding of fixtureReport.sdkDeprecations ?? inspection.sdkDeprecations ?? []) {
|
|
710
|
+
const existing = grouped.get(finding.code) ?? [];
|
|
711
|
+
existing.push(finding);
|
|
712
|
+
grouped.set(finding.code, existing);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (const [code, findings] of grouped) {
|
|
716
|
+
const first = findings[0];
|
|
717
|
+
warnings.push({
|
|
718
|
+
fixture: fixture.id,
|
|
719
|
+
code,
|
|
720
|
+
level: "warning",
|
|
721
|
+
message: first.message,
|
|
722
|
+
evidence: findings.map((finding) => `${finding.surface} @ ${finding.ref}`),
|
|
723
|
+
});
|
|
724
|
+
decisions.push({
|
|
725
|
+
fixture: fixture.id,
|
|
726
|
+
decision: "core-compat-adapter",
|
|
727
|
+
seam: "session-store",
|
|
728
|
+
action:
|
|
729
|
+
"Keep loadSessionStore compatibility active while plugin authors migrate to row-scoped session helpers.",
|
|
730
|
+
evidence: findings.map((finding) => finding.ref).join(", "),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
705
735
|
function classifySecurityManifestCoverage({ fixture, fixtureReport, warnings, decisions }) {
|
|
706
736
|
for (const securityManifest of fixtureReport.securityManifests ?? []) {
|
|
707
737
|
warnings.push({
|
package/src/inspector.js
CHANGED
|
@@ -10,6 +10,7 @@ import { fixtureCheckoutPath, fixtureSourceRoot } from "./config.js";
|
|
|
10
10
|
import { buildCompatibilityFixtureReport } from "./fixture-summary.js";
|
|
11
11
|
import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
12
12
|
import { buildCompatibilityReport, buildReport } from "./report.js";
|
|
13
|
+
import { inspectSdkDeprecations } from "./sdk-deprecation-rules.js";
|
|
13
14
|
|
|
14
15
|
const execFileAsync = promisify(execFile);
|
|
15
16
|
const registrationEquivalents = new Map([
|
|
@@ -104,6 +105,7 @@ export async function inspectPlugin(fixture, options = {}) {
|
|
|
104
105
|
const hookDetails = [];
|
|
105
106
|
const registrationDetails = [];
|
|
106
107
|
const sdkImportDetails = [];
|
|
108
|
+
const sdkDeprecationDetails = [];
|
|
107
109
|
|
|
108
110
|
for (const filePath of files) {
|
|
109
111
|
const text = await readFile(filePath, "utf8");
|
|
@@ -121,6 +123,9 @@ export async function inspectPlugin(fixture, options = {}) {
|
|
|
121
123
|
for (const sdkImport of sourceInspection.sdkImports) {
|
|
122
124
|
sdkImportDetails.push(sdkImport);
|
|
123
125
|
}
|
|
126
|
+
for (const sdkDeprecation of sourceInspection.sdkDeprecations) {
|
|
127
|
+
sdkDeprecationDetails.push(sdkDeprecation);
|
|
128
|
+
}
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
const manifestInspection = await readManifestContracts(config, checkoutPath, sourceRoot);
|
|
@@ -140,6 +145,7 @@ export async function inspectPlugin(fixture, options = {}) {
|
|
|
140
145
|
packageErrors: packageInspection.errors,
|
|
141
146
|
packageEntrypoints: packageInspection.entrypoints,
|
|
142
147
|
sdkImports: uniqueDetails(sdkImportDetails),
|
|
148
|
+
sdkDeprecations: uniqueSdkDeprecations(sdkDeprecationDetails),
|
|
143
149
|
sourceFiles: files.map((filePath) => path.relative(config.rootDir ?? process.cwd(), filePath)).sort(),
|
|
144
150
|
};
|
|
145
151
|
}
|
|
@@ -160,11 +166,13 @@ export function inspectSourceText(text, filePath = "source.js") {
|
|
|
160
166
|
filePath,
|
|
161
167
|
"specifier",
|
|
162
168
|
);
|
|
169
|
+
const sdkDeprecations = inspectSdkDeprecations(searchableText, filePath);
|
|
163
170
|
|
|
164
171
|
return {
|
|
165
172
|
hooks,
|
|
166
173
|
registrations,
|
|
167
174
|
sdkImports,
|
|
175
|
+
sdkDeprecations,
|
|
168
176
|
};
|
|
169
177
|
}
|
|
170
178
|
|
|
@@ -353,6 +361,7 @@ function emptyInspection(fixture, status) {
|
|
|
353
361
|
packageErrors: [],
|
|
354
362
|
packageEntrypoints: [],
|
|
355
363
|
sdkImports: [],
|
|
364
|
+
sdkDeprecations: [],
|
|
356
365
|
sourceFiles: [],
|
|
357
366
|
};
|
|
358
367
|
}
|
|
@@ -554,3 +563,11 @@ function uniqueDetails(details) {
|
|
|
554
563
|
}
|
|
555
564
|
return [...byKey.values()];
|
|
556
565
|
}
|
|
566
|
+
|
|
567
|
+
function uniqueSdkDeprecations(details) {
|
|
568
|
+
const byKey = new Map();
|
|
569
|
+
for (const detail of [...details].sort((left, right) => left.ref.localeCompare(right.ref))) {
|
|
570
|
+
byKey.set(`${detail.code}:${detail.surface}:${detail.ref}`, detail);
|
|
571
|
+
}
|
|
572
|
+
return [...byKey.values()];
|
|
573
|
+
}
|
package/src/issues.js
CHANGED
|
@@ -41,6 +41,7 @@ export const knownIssueCodes = new Set([
|
|
|
41
41
|
"runtime-tool-capture",
|
|
42
42
|
"reserved-sdk-import",
|
|
43
43
|
"security-manifest-schema-unavailable",
|
|
44
|
+
"sdk-load-session-store",
|
|
44
45
|
"sdk-export-missing",
|
|
45
46
|
"unrecognized-security-manifest",
|
|
46
47
|
]);
|
|
@@ -110,6 +111,19 @@ export const issueMetadataByCode = {
|
|
|
110
111
|
],
|
|
111
112
|
),
|
|
112
113
|
},
|
|
114
|
+
"sdk-load-session-store": {
|
|
115
|
+
severity: "P2",
|
|
116
|
+
owner: "core",
|
|
117
|
+
decision: "core-compat-adapter",
|
|
118
|
+
title: "deprecated whole-store session helper is still used",
|
|
119
|
+
authorRemediation: migrationRemediation(
|
|
120
|
+
"Replace deprecated loadSessionStore whole-store access with row-scoped session helpers.",
|
|
121
|
+
[
|
|
122
|
+
"Use getSessionEntry(...) or listSessionEntries(...) for reads instead of cloning the whole session store.",
|
|
123
|
+
"Use patchSessionEntry(...) or upsertSessionEntry(...) for writes instead of mutating and saving a whole-store object.",
|
|
124
|
+
],
|
|
125
|
+
),
|
|
126
|
+
},
|
|
113
127
|
"sdk-export-missing": {
|
|
114
128
|
severity: "P1",
|
|
115
129
|
owner: "core",
|
|
@@ -545,7 +559,16 @@ function issueClassFor(code, options) {
|
|
|
545
559
|
if (code === "missing-compat-record") {
|
|
546
560
|
return "compat-gap";
|
|
547
561
|
}
|
|
548
|
-
if (
|
|
562
|
+
if (
|
|
563
|
+
options.deprecated ||
|
|
564
|
+
[
|
|
565
|
+
"channel-env-vars",
|
|
566
|
+
"legacy-before-agent-start",
|
|
567
|
+
"legacy-root-sdk-import",
|
|
568
|
+
"provider-auth-env-vars",
|
|
569
|
+
"sdk-load-session-store",
|
|
570
|
+
].includes(code)
|
|
571
|
+
) {
|
|
549
572
|
return "deprecation-warning";
|
|
550
573
|
}
|
|
551
574
|
if (
|
package/src/report.js
CHANGED
|
@@ -26,6 +26,7 @@ export function buildReport({ config, inspections, failures = [], generatedAt =
|
|
|
26
26
|
registrations: inspection.registrations,
|
|
27
27
|
manifestContracts: inspection.manifestContracts,
|
|
28
28
|
sdkImports: inspection.sdkImports,
|
|
29
|
+
sdkDeprecations: inspection.sdkDeprecations,
|
|
29
30
|
sourceFiles: inspection.sourceFiles,
|
|
30
31
|
manifestFiles: inspection.manifestFiles,
|
|
31
32
|
packageFiles: inspection.packageFiles,
|
|
@@ -477,6 +478,7 @@ function defaultCompatibilityFixtureReport({ fixture, inspection }) {
|
|
|
477
478
|
packages: [],
|
|
478
479
|
sdkImports: inspection.sdkImports.map((sdkImport) => sdkImport.specifier).filter(Boolean),
|
|
479
480
|
sdkImportDetails: inspection.sdkImports,
|
|
481
|
+
sdkDeprecations: inspection.sdkDeprecations,
|
|
480
482
|
};
|
|
481
483
|
}
|
|
482
484
|
|
|
@@ -495,6 +497,7 @@ function normalizeInspection(inspection, fixture) {
|
|
|
495
497
|
packageErrors: [],
|
|
496
498
|
packageEntrypoints: [],
|
|
497
499
|
sdkImports: [],
|
|
500
|
+
sdkDeprecations: [],
|
|
498
501
|
sourceFiles: [],
|
|
499
502
|
...inspection,
|
|
500
503
|
};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
const loadSessionStoreReplacement =
|
|
2
|
+
"getSessionEntry(...) / listSessionEntries(...) for reads and patchSessionEntry(...) / upsertSessionEntry(...) for writes";
|
|
3
|
+
|
|
4
|
+
const loadSessionStoreSpecifiers = new Set([
|
|
5
|
+
"openclaw/plugin-sdk/config-runtime",
|
|
6
|
+
"openclaw/plugin-sdk/session-store-runtime",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const pluginSdkDeprecationRules = [
|
|
10
|
+
{
|
|
11
|
+
code: "sdk-load-session-store",
|
|
12
|
+
title: "deprecated whole-store session helper is still used",
|
|
13
|
+
replacement: loadSessionStoreReplacement,
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function inspectSdkDeprecations(text, filePath = "source.js", rules = pluginSdkDeprecationRules) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
|
|
20
|
+
for (const rule of rules) {
|
|
21
|
+
if (rule.code === "sdk-load-session-store") {
|
|
22
|
+
collectLoadSessionStoreDeprecations(findings, { text, filePath, rule });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return uniqueFindings(findings)
|
|
27
|
+
.sort((left, right) => left.offset - right.offset || left.surface.localeCompare(right.surface))
|
|
28
|
+
.map(({ offset, ...finding }) => finding);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function collectLoadSessionStoreDeprecations(findings, context) {
|
|
32
|
+
collectNamedImportDeprecations(findings, context);
|
|
33
|
+
collectNamedReexportDeprecations(findings, context);
|
|
34
|
+
collectNamedRequireDeprecations(findings, context);
|
|
35
|
+
collectNamespaceUsageDeprecations(findings, context);
|
|
36
|
+
collectNamespaceRequireDeprecations(findings, context);
|
|
37
|
+
collectRuntimeUsageDeprecations(findings, context);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collectNamedImportDeprecations(findings, context) {
|
|
41
|
+
const regex =
|
|
42
|
+
/\bimport\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?{([^}]+)}\s*from\s*["'`]([^"'`]+)["'`]/g;
|
|
43
|
+
for (const match of context.text.matchAll(regex)) {
|
|
44
|
+
const specifier = match[2];
|
|
45
|
+
if (!loadSessionStoreSpecifiers.has(specifier)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const binding of parseNamedBindings(match[1])) {
|
|
49
|
+
if (binding.exported !== "loadSessionStore") {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
findings.push(
|
|
53
|
+
buildFinding(context.rule, {
|
|
54
|
+
surface: `${specifier} import`,
|
|
55
|
+
sourceText: context.text,
|
|
56
|
+
filePath: context.filePath,
|
|
57
|
+
offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectNamedReexportDeprecations(findings, context) {
|
|
65
|
+
const regex = /\bexport\s*{([^}]+)}\s*from\s*["'`]([^"'`]+)["'`]/g;
|
|
66
|
+
for (const match of context.text.matchAll(regex)) {
|
|
67
|
+
const specifier = match[2];
|
|
68
|
+
if (!loadSessionStoreSpecifiers.has(specifier)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const binding of parseNamedBindings(match[1])) {
|
|
72
|
+
if (binding.exported !== "loadSessionStore") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
findings.push(
|
|
76
|
+
buildFinding(context.rule, {
|
|
77
|
+
surface: `${specifier} re-export`,
|
|
78
|
+
sourceText: context.text,
|
|
79
|
+
filePath: context.filePath,
|
|
80
|
+
offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function collectNamedRequireDeprecations(findings, context) {
|
|
88
|
+
const regex = /\b(?:const|let|var)\s+{([^}]+)}\s*=\s*require\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
|
89
|
+
for (const match of context.text.matchAll(regex)) {
|
|
90
|
+
const specifier = match[2];
|
|
91
|
+
if (!loadSessionStoreSpecifiers.has(specifier)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
for (const binding of parseNamedBindings(match[1], { aliasSeparator: ":" })) {
|
|
95
|
+
if (binding.exported !== "loadSessionStore") {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
findings.push(
|
|
99
|
+
buildFinding(context.rule, {
|
|
100
|
+
surface: `${specifier} require`,
|
|
101
|
+
sourceText: context.text,
|
|
102
|
+
filePath: context.filePath,
|
|
103
|
+
offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function collectMemberCallDeprecations(findings, context, options) {
|
|
111
|
+
forEachMethodCall(context.text, "loadSessionStore", (offset) => {
|
|
112
|
+
// Normalize transparent parentheses and optional-chained member links before matching.
|
|
113
|
+
const receiver = readNormalizedCallReceiver(context.text, offset);
|
|
114
|
+
if (!receiver || !options.receiverMatcher(receiver)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
findings.push(
|
|
118
|
+
buildFinding(context.rule, {
|
|
119
|
+
surface: options.surface,
|
|
120
|
+
sourceText: context.text,
|
|
121
|
+
filePath: context.filePath,
|
|
122
|
+
offset,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function forEachMethodCall(text, methodName, visit) {
|
|
129
|
+
let start = 0;
|
|
130
|
+
while (start < text.length) {
|
|
131
|
+
const offset = text.indexOf(methodName, start);
|
|
132
|
+
if (offset === -1) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
start = offset + methodName.length;
|
|
136
|
+
if (!isIdentifierBoundary(text, offset - 1) || !isIdentifierBoundary(text, offset + methodName.length)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!hasCallSuffix(text, offset + methodName.length)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
visit(offset);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readNormalizedCallReceiver(text, methodOffset) {
|
|
147
|
+
let cursor = skipWhitespaceBackward(text, methodOffset);
|
|
148
|
+
const operator = readMemberAccessOperator(text, cursor);
|
|
149
|
+
if (!operator) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
cursor = operator.start;
|
|
153
|
+
const receiverStart = findReceiverStart(text, cursor);
|
|
154
|
+
if (receiverStart === cursor) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return normalizeReceiverExpression(text.slice(receiverStart, cursor));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readMemberAccessOperator(text, endOffset) {
|
|
161
|
+
if (endOffset >= 2 && text.slice(endOffset - 2, endOffset) === "?.") {
|
|
162
|
+
return { kind: "optional", start: endOffset - 2 };
|
|
163
|
+
}
|
|
164
|
+
if (endOffset >= 1 && text[endOffset - 1] === ".") {
|
|
165
|
+
return { kind: "direct", start: endOffset - 1 };
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function findReceiverStart(text, endOffset) {
|
|
171
|
+
let cursor = endOffset;
|
|
172
|
+
let parenDepth = 0;
|
|
173
|
+
while (cursor > 0) {
|
|
174
|
+
const char = text[cursor - 1];
|
|
175
|
+
if (char === ")") {
|
|
176
|
+
parenDepth += 1;
|
|
177
|
+
cursor -= 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (char === "(") {
|
|
181
|
+
if (parenDepth === 0) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
parenDepth -= 1;
|
|
185
|
+
cursor -= 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (parenDepth > 0) {
|
|
189
|
+
cursor -= 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (isReceiverCharacter(char)) {
|
|
193
|
+
cursor -= 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
return cursor;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeReceiverExpression(rawReceiver) {
|
|
202
|
+
let normalized = rawReceiver.replace(/\s+/g, "");
|
|
203
|
+
while (normalized.startsWith("(") && normalized.endsWith(")") && wrapsWholeExpression(normalized)) {
|
|
204
|
+
normalized = normalized.slice(1, -1);
|
|
205
|
+
}
|
|
206
|
+
return normalized.replaceAll("?.", ".");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function wrapsWholeExpression(value) {
|
|
210
|
+
let depth = 0;
|
|
211
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
212
|
+
const char = value[index];
|
|
213
|
+
if (char === "(") {
|
|
214
|
+
depth += 1;
|
|
215
|
+
} else if (char === ")") {
|
|
216
|
+
depth -= 1;
|
|
217
|
+
if (depth === 0 && index < value.length - 1) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return depth === 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasCallSuffix(text, startOffset) {
|
|
226
|
+
let cursor = skipWhitespaceForward(text, startOffset);
|
|
227
|
+
if (text.slice(cursor, cursor + 2) === "?.") {
|
|
228
|
+
cursor = skipWhitespaceForward(text, cursor + 2);
|
|
229
|
+
}
|
|
230
|
+
return text[cursor] === "(";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function skipWhitespaceForward(text, startOffset) {
|
|
234
|
+
let cursor = startOffset;
|
|
235
|
+
while (cursor < text.length && /\s/.test(text[cursor])) {
|
|
236
|
+
cursor += 1;
|
|
237
|
+
}
|
|
238
|
+
return cursor;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function skipWhitespaceBackward(text, endOffset) {
|
|
242
|
+
let cursor = endOffset;
|
|
243
|
+
while (cursor > 0 && /\s/.test(text[cursor - 1])) {
|
|
244
|
+
cursor -= 1;
|
|
245
|
+
}
|
|
246
|
+
return cursor;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isReceiverCharacter(char) {
|
|
250
|
+
return /[A-Za-z0-9_$?.]/.test(char);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isIdentifierBoundary(text, offset) {
|
|
254
|
+
if (offset < 0 || offset >= text.length) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return !/[A-Za-z0-9_$]/.test(text[offset]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isRuntimeSessionReceiver(receiver) {
|
|
261
|
+
return /^(?:[A-Za-z_$][A-Za-z0-9_$]*|this)\.runtime\.agent\.session$/.test(receiver);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function collectNamespaceUsageDeprecations(findings, context) {
|
|
265
|
+
const regex = /\bimport\s+(?:type\s+)?\*\s+as\s+([A-Za-z_$][\w$]*)\s*from\s*["'`]([^"'`]+)["'`]/g;
|
|
266
|
+
for (const match of context.text.matchAll(regex)) {
|
|
267
|
+
const local = match[1];
|
|
268
|
+
const specifier = match[2];
|
|
269
|
+
if (!loadSessionStoreSpecifiers.has(specifier)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
collectMemberCallDeprecations(findings, context, {
|
|
273
|
+
receiverMatcher: (receiver) => receiver === local,
|
|
274
|
+
surface: `${specifier} namespace access`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function collectNamespaceRequireDeprecations(findings, context) {
|
|
280
|
+
const regex = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
|
281
|
+
for (const match of context.text.matchAll(regex)) {
|
|
282
|
+
const local = match[1];
|
|
283
|
+
const specifier = match[2];
|
|
284
|
+
if (!loadSessionStoreSpecifiers.has(specifier)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
collectMemberCallDeprecations(findings, context, {
|
|
288
|
+
receiverMatcher: (receiver) => receiver === local,
|
|
289
|
+
surface: `${specifier} require namespace access`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function collectRuntimeUsageDeprecations(findings, context) {
|
|
295
|
+
collectMemberCallDeprecations(findings, context, {
|
|
296
|
+
receiverMatcher: isRuntimeSessionReceiver,
|
|
297
|
+
surface: "api.runtime.agent.session",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parseNamedBindings(rawBindings, options = {}) {
|
|
302
|
+
const aliasSeparator = options.aliasSeparator ?? "as";
|
|
303
|
+
return rawBindings
|
|
304
|
+
.split(",")
|
|
305
|
+
.map((binding) => binding.trim())
|
|
306
|
+
.filter(Boolean)
|
|
307
|
+
.map((binding) => binding.replace(/^type\s+/, "").trim())
|
|
308
|
+
.map((binding) => parseBindingAlias(binding, aliasSeparator))
|
|
309
|
+
.filter((binding) => binding.exported && binding.local);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseBindingAlias(binding, aliasSeparator) {
|
|
313
|
+
if (aliasSeparator === ":") {
|
|
314
|
+
const separatorIndex = binding.indexOf(":");
|
|
315
|
+
if (separatorIndex === -1) {
|
|
316
|
+
return {
|
|
317
|
+
exported: binding.trim(),
|
|
318
|
+
local: binding.trim(),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
exported: binding.slice(0, separatorIndex).trim(),
|
|
323
|
+
local: binding.slice(separatorIndex + 1).trim(),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const tokens = binding.trim().split(/\s+/);
|
|
328
|
+
if (tokens.length === 3 && tokens[1] === "as") {
|
|
329
|
+
return {
|
|
330
|
+
exported: tokens[0],
|
|
331
|
+
local: tokens[2],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
exported: binding.trim(),
|
|
337
|
+
local: binding.trim(),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildFinding(rule, details) {
|
|
342
|
+
const refLine = lineForOffset(details.sourceText, details.offset);
|
|
343
|
+
return {
|
|
344
|
+
code: rule.code,
|
|
345
|
+
surface: details.surface,
|
|
346
|
+
replacement: rule.replacement,
|
|
347
|
+
ref: `${details.filePath}:${refLine}`,
|
|
348
|
+
message: `loadSessionStore keeps the legacy whole-store session shape; use ${rule.replacement}.`,
|
|
349
|
+
offset: details.offset,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function uniqueFindings(findings) {
|
|
354
|
+
const byKey = new Map();
|
|
355
|
+
for (const finding of findings) {
|
|
356
|
+
byKey.set(`${finding.code}:${finding.surface}:${finding.ref}`, finding);
|
|
357
|
+
}
|
|
358
|
+
return [...byKey.values()];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function lineForOffset(text, offset) {
|
|
362
|
+
let line = 1;
|
|
363
|
+
for (let index = 0; index < offset; index += 1) {
|
|
364
|
+
if (text.charCodeAt(index) === 10) {
|
|
365
|
+
line += 1;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return line;
|
|
369
|
+
}
|