@openclaw/plugin-inspector 0.3.12 → 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 CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
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.
8
+
9
+ ## 0.3.13 - 2026-06-09
10
+
11
+ ### Changed
12
+
13
+ - Add `authorRemediation.summary` and `authorRemediation.docsUrl` guidance to author-facing compatibility issues and Markdown reports.
14
+ - Add `--author-facing` for `check`, `ci`, and `batch` reports while keeping default output complete for internal coverage findings.
15
+ - Replace the recent `--include-inspector-gaps` option with a clear error pointing to `--author-facing`.
4
16
 
5
17
  ## 0.3.12 - 2026-06-09
6
18
 
package/README.md CHANGED
@@ -187,7 +187,7 @@ Common options:
187
187
  | `--mock-sdk` / `--sdk mock` | Use generated SDK and external-package mocks for runtime capture. |
188
188
  | `--real-sdk` / `--sdk real` | Use installed real SDK dependencies instead of mocks. |
189
189
  | `--allow-execute` | Permit commands that import plugin code. |
190
- | `--include-inspector-gaps` | Include maintainer-facing scanner coverage gaps in `check`, `ci`, and `batch` output. |
190
+ | `--author-facing` | Limit `check`, `ci`, and `batch` reports to findings with `authorRemediation` guidance. |
191
191
  | `--json` | Print machine-readable JSON to stdout. |
192
192
  | `--sarif [path]` | Write SARIF from `check` or `inspect`; `ci` enables this by default. |
193
193
  | `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
@@ -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 imports. |
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. |
@@ -307,9 +307,13 @@ Important report sections:
307
307
  | `logs` | Informational inventory and coverage rows. |
308
308
  | `decisions` | Maintainer-facing follow-up or compatibility-policy decisions. |
309
309
 
310
- Author-facing `check`, `ci`, and `batch` output hides inspector gaps by default
311
- because they are scanner coverage backlog, not plugin-author fixes. Pass
312
- `--include-inspector-gaps` for Crabpot-style maintainer coverage reports.
310
+ Default `check`, `ci`, and `batch` reports include both author-facing and
311
+ internal findings. Pass `--author-facing` when producing plugin-author output;
312
+ that filtered view includes only findings with `authorRemediation.summary` and
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.
313
317
 
314
318
  ## CI Policy And Shared Reporting Primitives
315
319
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
package/src/advanced.js CHANGED
@@ -77,6 +77,7 @@ export {
77
77
  classifyIssueFinding,
78
78
  deprecatedCompatRecords,
79
79
  issueId,
80
+ isAuthorFacingFinding,
80
81
  issueMetadata,
81
82
  issueMetadataByCode,
82
83
  knownIssueCodes,
package/src/api.js CHANGED
@@ -43,8 +43,8 @@ export async function loadPluginConfig(options = {}) {
43
43
  export async function inspectPluginRoot(options = {}) {
44
44
  const config = await loadPluginConfig(options);
45
45
  return inspectCompatibilityFixtureSet(config, {
46
+ authorFacing: options.authorFacing,
46
47
  generatedAt: options.generatedAt,
47
- includeInspectorGaps: options.includeInspectorGaps,
48
48
  openclawPath: options.openclawPath,
49
49
  executionResults: options.executionResults,
50
50
  targetOpenClaw: options.targetOpenClaw,
@@ -59,8 +59,8 @@ export async function inspectFixtureSetConfig(options = {}) {
59
59
  export async function inspectCompatibilityFixtureSetConfig(options = {}) {
60
60
  const config = await loadFixtureSetConfig(options);
61
61
  return inspectCompatibilityFixtureSet(config, {
62
+ authorFacing: options.authorFacing,
62
63
  generatedAt: options.generatedAt,
63
- includeInspectorGaps: options.includeInspectorGaps,
64
64
  openclawPath: options.openclawPath,
65
65
  executionResults: options.executionResults,
66
66
  targetOpenClaw: options.targetOpenClaw,
@@ -114,8 +114,8 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
114
114
  const report =
115
115
  options.report ??
116
116
  (await inspectCompatibilityFixtureSet(config, {
117
+ authorFacing: options.authorFacing,
117
118
  generatedAt: options.generatedAt,
118
- includeInspectorGaps: options.includeInspectorGaps,
119
119
  openclawPath: options.openclawPath,
120
120
  executionResults: options.executionResults,
121
121
  targetOpenClaw: options.targetOpenClaw,
@@ -147,8 +147,8 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
147
147
  const report =
148
148
  options.report ??
149
149
  (await inspectCompatibilityFixtureSet(config, {
150
+ authorFacing: options.authorFacing,
150
151
  generatedAt: options.generatedAt,
151
- includeInspectorGaps: options.includeInspectorGaps,
152
152
  openclawPath: options.openclawPath,
153
153
  executionResults: options.executionResults,
154
154
  targetOpenClaw: options.targetOpenClaw,
package/src/batch.js CHANGED
@@ -108,9 +108,9 @@ async function inspectBatchPlugin(pluginRoot, options) {
108
108
  try {
109
109
  const { report } = await runPluginCheck({
110
110
  allowExecution: options.allowExecution,
111
+ authorFacing: options.authorFacing,
111
112
  capture: options.capture,
112
113
  configPath: options.configPath,
113
- includeInspectorGaps: options.includeInspectorGaps,
114
114
  mockSdk: options.mockSdk,
115
115
  openclawPath: options.openclawPath,
116
116
  outDir: options.outDir,
@@ -148,17 +148,20 @@ async function inspectBatchPlugin(pluginRoot, options) {
148
148
  }
149
149
 
150
150
  function normalizeReportFindings(report) {
151
- return [
152
- ...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
153
- ...(report.issues ?? []).map((finding) =>
154
- normalizeFinding(
155
- finding,
156
- finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
157
- ),
151
+ const issueFindings = (report.issues ?? []).map((finding) =>
152
+ normalizeFinding(
153
+ finding,
154
+ finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
158
155
  ),
156
+ );
157
+ const issueKeys = new Set(issueFindings.map(findingKey));
158
+ const rawFindings = [
159
+ ...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
159
160
  ...(report.warnings ?? []).map((finding) => normalizeFinding(finding, "warning")),
160
161
  ...(report.suggestions ?? []).map((finding) => normalizeFinding(finding, "warning")),
161
- ];
162
+ ].filter((finding) => !issueKeys.has(findingKey(finding)));
163
+
164
+ return [...issueFindings, ...rawFindings];
162
165
  }
163
166
 
164
167
  function normalizeFinding(finding, kind) {
@@ -169,9 +172,18 @@ function normalizeFinding(finding, kind) {
169
172
  issueClass: finding.issueClass,
170
173
  message: finding.message ?? finding.title ?? "See plugin report.",
171
174
  evidence: finding.evidence,
175
+ ...(finding.authorRemediation ? { authorRemediation: finding.authorRemediation } : {}),
172
176
  };
173
177
  }
174
178
 
179
+ function findingKey(finding) {
180
+ return [
181
+ finding.fixture ?? "",
182
+ finding.code ?? "",
183
+ ...(Array.isArray(finding.evidence) ? finding.evidence : []),
184
+ ].join("\n");
185
+ }
186
+
175
187
  function findingFrequencyRows(entries) {
176
188
  const byCode = new Map();
177
189
  for (const entry of entries) {
package/src/cli.js CHANGED
@@ -64,7 +64,7 @@ async function runBatch(commandArgs) {
64
64
  const json = commandArgs.includes("--json");
65
65
  const check = commandArgs.includes("--check");
66
66
  const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
67
- const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
67
+ const authorFacing = readAuthorFacingFlag(commandArgs);
68
68
  if (!inputDir) {
69
69
  throw new Error("batch requires a folder of plugin roots");
70
70
  }
@@ -73,7 +73,7 @@ async function runBatch(commandArgs) {
73
73
  outDir,
74
74
  openclawPath,
75
75
  concurrency,
76
- includeInspectorGaps,
76
+ authorFacing,
77
77
  keepPluginReports,
78
78
  });
79
79
 
@@ -110,12 +110,12 @@ async function runCheck(commandArgs) {
110
110
  const mockSdk = readMockSdkFlag(commandArgs);
111
111
  const allowExecution = readAllowExecutionFlag(commandArgs);
112
112
  const ciOutputs = readCiOutputFlags(commandArgs);
113
- const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
113
+ const authorFacing = readAuthorFacingFlag(commandArgs);
114
114
  const { report, paths } = await runPluginCheck({
115
115
  allowExecution,
116
+ authorFacing,
116
117
  capture,
117
118
  configPath,
118
- includeInspectorGaps,
119
119
  mockSdk,
120
120
  openclawPath,
121
121
  outDir,
@@ -168,12 +168,18 @@ async function runInit(commandArgs) {
168
168
  async function runReport(command, commandArgs) {
169
169
  const configPath = readFlag(commandArgs, "--config");
170
170
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
171
+ const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
171
172
  const check = commandArgs.includes("--check") || command === "ci";
172
173
  const json = commandArgs.includes("--json");
173
174
  const ciOutputs = readCiOutputFlags(commandArgs);
175
+ const authorFacing = readAuthorFacingFlag(commandArgs);
174
176
  const config = await loadInspectorConfig(configPath);
175
- const report = await inspectFixtureSet(config);
176
- const paths = await writeReport(report, { outDir });
177
+ const report = authorFacing
178
+ ? await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath })
179
+ : await inspectFixtureSet(config);
180
+ const paths = authorFacing
181
+ ? await writeCompatibilityReport(report, { cwd: config.rootDir, outDir })
182
+ : await writeReport(report, { outDir });
177
183
  await writeCiOutputArtifacts(report, {
178
184
  ...ciOutputs,
179
185
  cwd: path.dirname(paths.jsonPath),
@@ -201,12 +207,12 @@ async function runCi(commandArgs) {
201
207
  const mockSdk = readMockSdkFlag(commandArgs);
202
208
  const allowExecution = readAllowExecutionFlag(commandArgs);
203
209
  const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
204
- const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
210
+ const authorFacing = readAuthorFacingFlag(commandArgs);
205
211
  const { report, reportDir } = await runCiCompatibilityReport({
206
212
  allowExecution,
213
+ authorFacing,
207
214
  capture,
208
215
  configPath,
209
- includeInspectorGaps,
210
216
  mockSdk,
211
217
  openclawPath,
212
218
  outDir,
@@ -245,9 +251,9 @@ async function runCi(commandArgs) {
245
251
 
246
252
  async function runCiCompatibilityReport({
247
253
  allowExecution,
254
+ authorFacing,
248
255
  capture,
249
256
  configPath,
250
- includeInspectorGaps,
251
257
  mockSdk,
252
258
  openclawPath,
253
259
  outDir,
@@ -255,7 +261,7 @@ async function runCiCompatibilityReport({
255
261
  }) {
256
262
  if (configPath) {
257
263
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
258
- const report = await inspectCompatibilityFixtureSet(config, { includeInspectorGaps, openclawPath });
264
+ const report = await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath });
259
265
  await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
260
266
  return {
261
267
  report,
@@ -265,8 +271,8 @@ async function runCiCompatibilityReport({
265
271
 
266
272
  const { report } = await runPluginCheck({
267
273
  allowExecution,
274
+ authorFacing,
268
275
  capture,
269
- includeInspectorGaps,
270
276
  mockSdk,
271
277
  openclawPath,
272
278
  outDir,
@@ -373,6 +379,15 @@ function readAllowExecutionFlag(commandArgs) {
373
379
  return commandArgs.includes("--allow-execute");
374
380
  }
375
381
 
382
+ function readAuthorFacingFlag(commandArgs) {
383
+ if (commandArgs.includes("--include-inspector-gaps")) {
384
+ throw new Error(
385
+ "--include-inspector-gaps has been replaced by --author-facing; default output now includes internal findings.",
386
+ );
387
+ }
388
+ return commandArgs.includes("--author-facing");
389
+ }
390
+
376
391
  function renderCiTextSummary(summary) {
377
392
  return [
378
393
  `Status: ${summary.status.toUpperCase()}`,
@@ -437,18 +452,18 @@ function printHelp() {
437
452
 
438
453
  Usage:
439
454
  plugin-inspector
440
- plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json]
455
+ plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json]
441
456
  plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
442
457
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
443
- plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
444
- plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--include-inspector-gaps] [--check] [--json]
445
- plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
446
- plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json] [--no-sarif] [--no-junit]
458
+ plugin-inspector report --config <path> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json]
459
+ plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--author-facing] [--check] [--json]
460
+ plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
461
+ plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json] [--no-sarif] [--no-junit]
447
462
  plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
448
463
 
449
464
  Default check runs from the current plugin root and writes reports/ unless --out is set.
450
465
  CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
451
466
  Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
452
- Inspector coverage gaps are hidden by default; pass --include-inspector-gaps for maintainer coverage reports.
467
+ Default output includes author-facing and internal findings; pass --author-facing to show only findings with author remediation docs.
453
468
  `);
454
469
  }
@@ -250,10 +250,23 @@ function issueBlock(issue, options) {
250
250
  ` - state: ${issueState(issue)}`,
251
251
  " - evidence:",
252
252
  ...evidenceList(issue.evidence, options).map((item) => ` - ${item}`),
253
+ ...remediationList(issue),
253
254
  ...runtimeCoverageList(issue, options),
254
255
  ].join("\n");
255
256
  }
256
257
 
258
+ function remediationList(issue) {
259
+ const remediation = issue.authorRemediation;
260
+ if (!remediation?.summary) {
261
+ return [];
262
+ }
263
+ return [
264
+ " - author remediation:",
265
+ ` - ${remediation.summary}`,
266
+ ` - docs: ${remediation.docsUrl}`,
267
+ ];
268
+ }
269
+
257
270
  function issueState(issue) {
258
271
  const flags = [
259
272
  issue.status,
@@ -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/index.js CHANGED
@@ -217,7 +217,7 @@ export {
217
217
  validateImportLoopProfile,
218
218
  writeImportLoopProfile,
219
219
  } from "./import-loop-profile.js";
220
- export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
220
+ export { classifyIssueFinding, issueId, isAuthorFacingFinding, knownIssueCodes } from "./issues.js";
221
221
  export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
222
222
  export { openClawTargetPathCandidates, readOpenClawTargetSurface } from "./openclaw-target.js";
223
223
  export {
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([
@@ -35,9 +36,9 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
35
36
  config,
36
37
  inspections,
37
38
  failures,
39
+ authorFacing: options.authorFacing,
38
40
  generatedAt: options.generatedAt,
39
41
  executionResults: options.executionResults,
40
- includeInspectorGaps: options.includeInspectorGaps,
41
42
  targetOpenClaw,
42
43
  buildFixtureReport: ({ fixture, inspection }) =>
43
44
  buildCompatibilityFixtureReport({
@@ -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,10 +41,17 @@ 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
  ]);
47
48
 
49
+ const authorRemediationDocsUrl = (code) => `https://docs.openclaw.ai/clawhub/plugin-validation-fixes#${code}`;
50
+
51
+ const authorRemediation = (summary) => ({ summary });
52
+
53
+ const migrationRemediation = authorRemediation;
54
+
48
55
  export const issueMetadataByCode = {
49
56
  "before-tool-call-probe": {
50
57
  severity: "P1",
@@ -63,6 +70,13 @@ export const issueMetadataByCode = {
63
70
  owner: "core",
64
71
  decision: "core-compat-adapter",
65
72
  title: "channelEnvVars legacy manifest metadata must stay covered",
73
+ authorRemediation: migrationRemediation(
74
+ "Move legacy channel environment variable metadata into the current setup/config metadata while keeping the old field until your supported OpenClaw range no longer needs it.",
75
+ [
76
+ "Mirror each channel environment variable into the current setup or provider configuration metadata.",
77
+ "Keep channelEnvVars only as backwards compatibility for older OpenClaw versions you still support.",
78
+ ],
79
+ ),
66
80
  },
67
81
  "conversation-access-hook": {
68
82
  severity: "P1",
@@ -75,12 +89,40 @@ export const issueMetadataByCode = {
75
89
  owner: "core",
76
90
  decision: "core-compat-adapter",
77
91
  title: "legacy before_agent_start hook compatibility is still used",
92
+ authorRemediation: migrationRemediation(
93
+ "Replace the legacy before_agent_start hook with the current prompt/model hooks.",
94
+ [
95
+ "Move model-selection work to before_model_resolve when possible.",
96
+ "Move prompt mutation work to before_prompt_build.",
97
+ "Keep before_agent_start only if your declared compatibility range still includes OpenClaw versions that require it.",
98
+ ],
99
+ ),
78
100
  },
79
101
  "legacy-root-sdk-import": {
80
102
  severity: "P2",
81
103
  owner: "core",
82
104
  decision: "core-compat-adapter",
83
105
  title: "root plugin SDK barrel is still used by fixtures",
106
+ authorRemediation: migrationRemediation(
107
+ "Prefer focused public plugin SDK subpath imports instead of the legacy root barrel.",
108
+ [
109
+ "Replace imports from openclaw/plugin-sdk with the documented subpath for the API you use.",
110
+ "Keep the root import only while supporting older OpenClaw versions that do not expose the subpath.",
111
+ ],
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
+ ),
84
126
  },
85
127
  "sdk-export-missing": {
86
128
  severity: "P1",
@@ -93,12 +135,26 @@ export const issueMetadataByCode = {
93
135
  owner: "plugin",
94
136
  decision: "plugin-upstream-fix",
95
137
  title: "plugin imports reserved bundled-plugin SDK compatibility subpaths",
138
+ authorRemediation: authorRemediation(
139
+ "Stop importing reserved bundled-plugin SDK compatibility paths.",
140
+ [
141
+ "Replace reserved OpenClaw internal SDK imports with documented public openclaw/plugin-sdk subpaths.",
142
+ "If no public API exists for the behavior, vendor a plugin-local helper or request a public OpenClaw API.",
143
+ ],
144
+ ),
96
145
  },
97
146
  "security-manifest-schema-unavailable": {
98
147
  severity: "P3",
99
148
  owner: "plugin",
100
149
  decision: "plugin-upstream-fix",
101
150
  title: "plugin security manifest references an unavailable schema",
151
+ authorRemediation: authorRemediation(
152
+ "Remove or update the unsupported security manifest schema reference.",
153
+ [
154
+ "Delete the schema URL from openclaw.security.json if it is advisory-only.",
155
+ "Use a documented versioned schema once OpenClaw publishes one.",
156
+ ],
157
+ ),
102
158
  },
103
159
  "missing-compat-record": {
104
160
  severity: "P1",
@@ -117,18 +173,37 @@ export const issueMetadataByCode = {
117
173
  owner: "plugin",
118
174
  decision: "plugin-upstream-fix",
119
175
  title: "manifest display name is missing",
176
+ authorRemediation: authorRemediation(
177
+ "Add a display name to the plugin manifest.",
178
+ ["Set a non-empty name field in openclaw.plugin.json."],
179
+ '{\n "name": "My Plugin"\n}',
180
+ ),
120
181
  },
121
182
  "manifest-unknown-contracts": {
122
183
  severity: "P1",
123
184
  owner: "plugin",
124
185
  decision: "plugin-upstream-fix",
125
186
  title: "manifest declares unsupported contract keys",
187
+ authorRemediation: authorRemediation(
188
+ "Remove unsupported manifest contract keys or move them to a documented OpenClaw contract field.",
189
+ [
190
+ "Compare the contracts object to the OpenClaw manifest fields supported by your target version.",
191
+ "Delete custom contract keys unless OpenClaw has a versioned schema for them.",
192
+ ],
193
+ ),
126
194
  },
127
195
  "manifest-unknown-fields": {
128
196
  severity: "P2",
129
197
  owner: "plugin",
130
198
  decision: "plugin-upstream-fix",
131
199
  title: "manifest uses unsupported top-level fields",
200
+ authorRemediation: authorRemediation(
201
+ "Move unsupported top-level manifest fields into supported package metadata or remove them.",
202
+ [
203
+ "Keep openclaw.plugin.json limited to fields supported by the target OpenClaw manifest schema.",
204
+ "Move package-level metadata into package.json openclaw metadata when that field is supported.",
205
+ ],
206
+ ),
132
207
  },
133
208
  "package-build-artifact-entrypoint": {
134
209
  severity: "P2",
@@ -147,72 +222,159 @@ export const issueMetadataByCode = {
147
222
  owner: "plugin",
148
223
  decision: "plugin-upstream-fix",
149
224
  title: "OpenClaw package entrypoint is missing",
225
+ authorRemediation: authorRemediation(
226
+ "Publish the entrypoint declared in OpenClaw package metadata or update the metadata to point at an existing file.",
227
+ [
228
+ "Check package.json openclaw.extensions and openclaw.runtimeExtensions.",
229
+ "Ensure the referenced file exists in the published artifact, usually under dist/ after build.",
230
+ ],
231
+ ),
150
232
  },
151
233
  "package-install-metadata-incomplete": {
152
234
  severity: "P2",
153
235
  owner: "plugin",
154
236
  decision: "plugin-upstream-fix",
155
237
  title: "OpenClaw package install metadata is incomplete",
238
+ authorRemediation: authorRemediation(
239
+ "Complete the OpenClaw install metadata so ClawHub can identify the install target.",
240
+ [
241
+ "Fill package.json openclaw.install with the supported release target.",
242
+ "Align clawhubSpec, npmSpec, and defaultChoice with the package you publish.",
243
+ ],
244
+ ),
156
245
  },
157
246
  "package-json-missing": {
158
247
  severity: "P2",
159
248
  owner: "plugin",
160
249
  decision: "plugin-upstream-fix",
161
250
  title: "package metadata is missing",
251
+ authorRemediation: authorRemediation(
252
+ "Add a package.json to the plugin package.",
253
+ [
254
+ "Include the package name and version.",
255
+ "Add an openclaw metadata block describing extensions, compatibility, and install details.",
256
+ ],
257
+ ),
162
258
  },
163
259
  "package-manifest-version-drift": {
164
260
  severity: "P2",
165
261
  owner: "plugin",
166
262
  decision: "plugin-upstream-fix",
167
263
  title: "package and manifest versions drift",
264
+ authorRemediation: authorRemediation(
265
+ "Align the plugin version declared in package.json and openclaw.plugin.json.",
266
+ [
267
+ "Use the same version in both files, or remove stale manifest version metadata if package.json is authoritative.",
268
+ "Republish with a new package version after changing published metadata.",
269
+ ],
270
+ ),
168
271
  },
169
272
  "package-min-host-version-drift": {
170
273
  severity: "P2",
171
274
  owner: "plugin",
172
275
  decision: "plugin-upstream-fix",
173
276
  title: "OpenClaw package minimum host version drifts from build target",
277
+ authorRemediation: authorRemediation(
278
+ "Set the package minimum host version to the OpenClaw version range the plugin was built and tested against.",
279
+ [
280
+ "Update package.json openclaw.install.minHostVersion or compatibility metadata.",
281
+ "Keep it semver-compatible with the target OpenClaw build version.",
282
+ ],
283
+ ),
174
284
  },
175
285
  "package-npm-pack-entrypoint-missing": {
176
286
  severity: "P1",
177
287
  owner: "plugin",
178
288
  decision: "plugin-upstream-fix",
179
289
  title: "advertised npm artifact is missing OpenClaw entrypoints",
290
+ authorRemediation: authorRemediation(
291
+ "Include the declared OpenClaw entrypoints in the npm-packed artifact.",
292
+ [
293
+ "Run npm pack locally and inspect the tarball contents.",
294
+ "Update package.json files so dist files and manifests are included.",
295
+ "Build before packing if the entrypoint is generated.",
296
+ ],
297
+ ),
180
298
  },
181
299
  "package-npm-pack-metadata-missing": {
182
300
  severity: "P2",
183
301
  owner: "plugin",
184
302
  decision: "plugin-upstream-fix",
185
303
  title: "advertised npm artifact is missing OpenClaw metadata",
304
+ authorRemediation: authorRemediation(
305
+ "Include OpenClaw metadata files in the npm-packed artifact.",
306
+ [
307
+ "Run npm pack locally and inspect package.json and OpenClaw manifest files.",
308
+ "Update package.json files so required metadata is not excluded.",
309
+ ],
310
+ ),
186
311
  },
187
312
  "package-npm-pack-unavailable": {
188
313
  severity: "P1",
189
314
  owner: "plugin",
190
315
  decision: "plugin-upstream-fix",
191
316
  title: "advertised npm artifact cannot be packed",
317
+ authorRemediation: authorRemediation(
318
+ "Make the package packable before publishing it through ClawHub.",
319
+ [
320
+ "Remove private:true if this package is intended to publish.",
321
+ "Ensure package.json has a valid name and version.",
322
+ "Fix package scripts or files entries that make npm pack fail.",
323
+ ],
324
+ ),
192
325
  },
193
326
  "package-openclaw-entry-missing": {
194
327
  severity: "P2",
195
328
  owner: "plugin",
196
329
  decision: "plugin-upstream-fix",
197
330
  title: "OpenClaw package entrypoint metadata is missing",
331
+ authorRemediation: authorRemediation(
332
+ "Declare the plugin runtime entrypoint in package.json OpenClaw metadata.",
333
+ [
334
+ "Add openclaw.extensions for extension entrypoints.",
335
+ "Add openclaw.runtimeExtensions when the plugin has runtime-side code.",
336
+ ],
337
+ ),
198
338
  },
199
339
  "package-openclaw-metadata-missing": {
200
340
  severity: "P2",
201
341
  owner: "plugin",
202
342
  decision: "plugin-upstream-fix",
203
343
  title: "OpenClaw package metadata is missing",
344
+ authorRemediation: authorRemediation(
345
+ "Add the package.json openclaw metadata block.",
346
+ [
347
+ "Describe extension entrypoints, plugin API compatibility, and install metadata.",
348
+ "Keep package metadata in sync with openclaw.plugin.json when both files are present.",
349
+ ],
350
+ ),
204
351
  },
205
352
  "package-openclaw-unsupported-metadata": {
206
353
  severity: "P2",
207
354
  owner: "plugin",
208
355
  decision: "plugin-upstream-fix",
209
356
  title: "package declares unsupported OpenClaw metadata",
357
+ authorRemediation: authorRemediation(
358
+ "Remove unsupported OpenClaw package metadata fields.",
359
+ [
360
+ "Delete openclaw.bundle and other fields not accepted by the current package schema.",
361
+ "Move bundle-specific data to documented manifest fields when available.",
362
+ ],
363
+ ),
210
364
  },
211
365
  "package-plugin-api-compat-missing": {
212
366
  severity: "P2",
213
367
  owner: "plugin",
214
368
  decision: "plugin-upstream-fix",
215
369
  title: "plugin API compatibility range is missing",
370
+ authorRemediation: authorRemediation(
371
+ "Declare the OpenClaw plugin API range this package supports.",
372
+ [
373
+ "Add package.json `openclaw.compat.pluginApi` with the OpenClaw plugin API range you tested.",
374
+ "If known, include the OpenClaw build/version used to produce the package metadata.",
375
+ ],
376
+ '"openclaw": {\n "compat": {\n "pluginApi": ">=0.1.0"\n }\n}',
377
+ ),
216
378
  },
217
379
  "package-typescript-source-entrypoint": {
218
380
  severity: "P2",
@@ -225,6 +387,13 @@ export const issueMetadataByCode = {
225
387
  owner: "core",
226
388
  decision: "core-compat-adapter",
227
389
  title: "providerAuthEnvVars legacy manifest metadata must stay covered",
390
+ authorRemediation: migrationRemediation(
391
+ "Move legacy provider authentication environment variables into current provider setup metadata.",
392
+ [
393
+ "Mirror providerAuthEnvVars into setup.providers[].envVars or the current provider-choice metadata.",
394
+ "Keep the legacy field only while supporting older OpenClaw versions that still read it.",
395
+ ],
396
+ ),
228
397
  },
229
398
  "registration-capture-gap": {
230
399
  severity: "P2",
@@ -255,6 +424,13 @@ export const issueMetadataByCode = {
255
424
  owner: "plugin",
256
425
  decision: "plugin-upstream-fix",
257
426
  title: "plugin ships an unsupported security manifest",
427
+ authorRemediation: authorRemediation(
428
+ "Remove unsupported security manifest files until OpenClaw documents a versioned security manifest schema.",
429
+ [
430
+ "Delete openclaw.security.json if it is advisory-only and not consumed by OpenClaw.",
431
+ "Reintroduce it only when the schema and ClawHub behavior are documented.",
432
+ ],
433
+ ),
258
434
  },
259
435
  };
260
436
 
@@ -284,6 +460,14 @@ export function buildIssues({ breakages = [], warnings = [], suggestions = [], t
284
460
  evidence: finding.evidence ?? [],
285
461
  compatRecord: finding.compatRecord ?? null,
286
462
  runtimeCoverage: finding.runtimeCoverage ?? null,
463
+ ...(finding.authorRemediation
464
+ ? {
465
+ authorRemediation: {
466
+ summary: finding.authorRemediation.summary,
467
+ docsUrl: authorRemediationDocsUrl(finding.code),
468
+ },
469
+ }
470
+ : {}),
287
471
  }));
288
472
  }
289
473
 
@@ -305,9 +489,18 @@ export function issueMetadata(finding, targetOpenClaw) {
305
489
  decision: "inspector-follow-up",
306
490
  title: finding.message,
307
491
  };
492
+ const authorMetadata = metadata.authorRemediation
493
+ ? {
494
+ authorRemediation: {
495
+ summary: metadata.authorRemediation.summary,
496
+ docsUrl: authorRemediationDocsUrl(finding.code),
497
+ },
498
+ }
499
+ : {};
308
500
  return {
309
501
  ...finding,
310
502
  ...metadata,
503
+ ...authorMetadata,
311
504
  ...classifyIssueFinding(finding, targetOpenClaw, metadata),
312
505
  };
313
506
  }
@@ -337,6 +530,10 @@ export function isInspectorGapFinding(finding, targetOpenClaw) {
337
530
  return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
338
531
  }
339
532
 
533
+ export function isAuthorFacingFinding(finding, targetOpenClaw) {
534
+ return Boolean(issueMetadata(finding, targetOpenClaw).authorRemediation);
535
+ }
536
+
340
537
  export function summarizeIssueClasses(issues) {
341
538
  const summary = {
342
539
  "compat-gap": 0,
@@ -362,7 +559,16 @@ function issueClassFor(code, options) {
362
559
  if (code === "missing-compat-record") {
363
560
  return "compat-gap";
364
561
  }
365
- if (options.deprecated || ["channel-env-vars", "legacy-before-agent-start", "legacy-root-sdk-import", "provider-auth-env-vars"].includes(code)) {
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
+ ) {
366
572
  return "deprecation-warning";
367
573
  }
368
574
  if (
package/src/report.js CHANGED
@@ -3,7 +3,7 @@ import { renderMarkdownTable, writeArtifacts, writeJsonMarkdownArtifacts } from
3
3
  import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } from "./compatibility-report.js";
4
4
  import { buildContractProbes } from "./contract-probes.js";
5
5
  import { classifyCompatibilityFixture } from "./fixture-summary.js";
6
- import { buildIssues, isInspectorGapFinding, summarizeIssueClasses } from "./issues.js";
6
+ import { buildIssues, isAuthorFacingFinding, summarizeIssueClasses } from "./issues.js";
7
7
  import { sanitizeReportArtifact } from "./report-sanitizer.js";
8
8
  import { applyRuntimeExecutionCoverage } from "./runtime-reconciliation.js";
9
9
 
@@ -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,
@@ -141,14 +142,16 @@ export async function buildCompatibilityReport(options = {}) {
141
142
  decisions,
142
143
  });
143
144
 
145
+ const visibleBreakages = filterVisibleFindings(breakages, targetOpenClaw, options);
144
146
  const visibleWarnings = filterVisibleFindings(warnings, targetOpenClaw, options);
145
147
  const visibleSuggestions = filterVisibleFindings(suggestions, targetOpenClaw, options);
148
+ const visibleDecisions = options.authorFacing === true ? [] : decisions;
146
149
  const runtimeCoverage = applyRuntimeExecutionCoverage({
147
150
  findings: [...visibleWarnings, ...visibleSuggestions],
148
151
  executionResults: options.executionResults,
149
152
  });
150
153
  const issues = buildIssues({
151
- breakages,
154
+ breakages: visibleBreakages,
152
155
  warnings: visibleWarnings,
153
156
  suggestions: visibleSuggestions,
154
157
  targetOpenClaw,
@@ -166,14 +169,14 @@ export async function buildCompatibilityReport(options = {}) {
166
169
  return {
167
170
  generatedAt: options.generatedAt ?? "deterministic",
168
171
  targetOpenClaw,
169
- status: breakages.length === 0 ? "pass" : "fail",
172
+ status: visibleBreakages.length === 0 ? "pass" : "fail",
170
173
  summary: {
171
174
  fixtureCount: fixtureReports.length,
172
175
  highPriorityFixtures: fixtureReports.filter((fixture) => fixture.priority === "high").length,
173
- breakageCount: breakages.length,
176
+ breakageCount: visibleBreakages.length,
174
177
  warningCount: visibleWarnings.length,
175
178
  suggestionCount: visibleSuggestions.length,
176
- decisionCount: decisions.length,
179
+ decisionCount: visibleDecisions.length,
177
180
  logCount: logs.length,
178
181
  issueCount: issues.length,
179
182
  openIssueCount: openIssues.length,
@@ -195,21 +198,21 @@ export async function buildCompatibilityReport(options = {}) {
195
198
  contractProbeCount: contractProbes.length,
196
199
  },
197
200
  fixtures: fixtureReports,
198
- breakages,
201
+ breakages: visibleBreakages,
199
202
  warnings: visibleWarnings,
200
203
  suggestions: visibleSuggestions,
201
204
  issues,
202
205
  contractProbes,
203
206
  logs,
204
- decisions,
207
+ decisions: visibleDecisions,
205
208
  };
206
209
  }
207
210
 
208
211
  function filterVisibleFindings(findings, targetOpenClaw, options) {
209
- if (options.includeInspectorGaps === true) {
212
+ if (options.authorFacing !== true) {
210
213
  return findings;
211
214
  }
212
- return findings.filter((finding) => !isInspectorGapFinding(finding, targetOpenClaw));
215
+ return findings.filter((finding) => isAuthorFacingFinding(finding, targetOpenClaw));
213
216
  }
214
217
 
215
218
  export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggestions, logs, decisions }) {
@@ -475,6 +478,7 @@ function defaultCompatibilityFixtureReport({ fixture, inspection }) {
475
478
  packages: [],
476
479
  sdkImports: inspection.sdkImports.map((sdkImport) => sdkImport.specifier).filter(Boolean),
477
480
  sdkImportDetails: inspection.sdkImports,
481
+ sdkDeprecations: inspection.sdkDeprecations,
478
482
  };
479
483
  }
480
484
 
@@ -493,6 +497,7 @@ function normalizeInspection(inspection, fixture) {
493
497
  packageErrors: [],
494
498
  packageEntrypoints: [],
495
499
  sdkImports: [],
500
+ sdkDeprecations: [],
496
501
  sourceFiles: [],
497
502
  ...inspection,
498
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
+ }