@openclaw/plugin-inspector 0.0.0 → 0.1.1

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.
@@ -0,0 +1,288 @@
1
+ import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
2
+ import { slugForArtifact } from "./path-utils.js";
3
+ import {
4
+ defaultSyntheticHookContexts,
5
+ defaultSyntheticHookEvents,
6
+ defaultSyntheticRegistrationArguments,
7
+ } from "./synthetic-probes.js";
8
+
9
+ export const defaultRegistrationAssertions = {
10
+ defineChannelPluginEntry: ["channel id is stable", "setup/config schema can be read", "message envelope metadata is preserved"],
11
+ definePluginEntry: ["entrypoint register function is callable", "entrypoint metadata is preserved"],
12
+ registerChannel: ["channel id is stable", "inbound/outbound envelope shape is captured", "sender metadata is preserved"],
13
+ registerCli: ["command name is stable", "argument schema is captured"],
14
+ registerCommand: ["command id is stable", "interactive command payload is captured"],
15
+ registerContextEngine: ["context engine id is stable", "factory metadata is captured"],
16
+ registerGatewayMethod: ["method name is stable", "request and response schema are captured"],
17
+ registerHttpRoute: ["route method and path are captured", "auth policy metadata is captured"],
18
+ registerInteractiveHandler: ["handler id is stable", "interaction payload and response shape are captured"],
19
+ registerHook: ["legacy hook name is stable", "handler metadata is captured"],
20
+ registerMemoryPromptSection: ["memory prompt section id is stable", "render metadata is captured"],
21
+ registerMemoryRuntime: ["memory runtime id is stable", "runtime factory metadata is captured"],
22
+ registerService: ["service id is stable", "start/stop lifecycle handlers are captured"],
23
+ registerSpeechProvider: ["provider id is stable", "speech request overrides are captured"],
24
+ registerTool: ["tool name is stable", "input schema is captured", "result shape metadata is captured"],
25
+ };
26
+
27
+ export const defaultRegistrationArguments = defaultSyntheticRegistrationArguments;
28
+
29
+ export const defaultHookAssertions = {
30
+ agent_end: ["final conversation payload is redacted as expected", "agent id and run metadata are present"],
31
+ before_agent_start: ["legacy startup hook payload is accepted", "migration metadata can map to prompt/model hooks"],
32
+ before_prompt_build: ["prompt mutation result is preserved", "agent and conversation metadata are present"],
33
+ before_tool_call: ["block/allow return shapes are preserved", "terminal and approval metadata are present"],
34
+ inbound_claim: ["claim payload preserves channel/source identity", "routing metadata is present"],
35
+ llm_input: ["model input payload is redacted as expected", "model and agent metadata are present"],
36
+ llm_output: ["model output payload is redacted as expected", "model and agent metadata are present"],
37
+ subagent_delivery_target: ["target routing result is preserved", "parent/subagent metadata are present"],
38
+ subagent_ended: ["subagent completion payload is preserved", "status metadata is present"],
39
+ subagent_spawned: ["spawn payload is preserved", "parent/subagent metadata are present"],
40
+ };
41
+
42
+ export const defaultHookEvents = defaultSyntheticHookEvents;
43
+
44
+ export const defaultHookContexts = defaultSyntheticHookContexts;
45
+
46
+ export function buildContractCapture(options = {}) {
47
+ const report = options.report;
48
+ if (!report) {
49
+ throw new TypeError("buildContractCapture requires a compatibility report");
50
+ }
51
+
52
+ const registrationAssertions = options.registrationAssertions ?? defaultRegistrationAssertions;
53
+ const registrationArguments = options.registrationArguments ?? defaultRegistrationArguments;
54
+ const hookAssertions = options.hookAssertions ?? defaultHookAssertions;
55
+ const hookEvents = options.hookEvents ?? defaultHookEvents;
56
+ const hookContexts = options.hookContexts ?? defaultHookContexts;
57
+ const capturedRegistrars = new Set(report.targetOpenClaw.capturedRegistrars ?? []);
58
+ const sdkExports = new Set(report.targetOpenClaw.sdkExports ?? []);
59
+
60
+ const fixtures = report.fixtures.map((fixture) => ({
61
+ id: fixture.id,
62
+ priority: fixture.priority,
63
+ registrations: fixture.registrationDetails.map((registration) => ({
64
+ id: `registration.${registration.name}:${fixture.id}:${slugForArtifact(registration.ref)}`,
65
+ fixture: fixture.id,
66
+ registrar: registration.name,
67
+ ref: registration.ref,
68
+ support: capturedRegistrars.has(registration.name) ? "target-captured" : "inspector-shim-required",
69
+ assertions: registrationAssertions[registration.name] ?? ["registration arguments are captured"],
70
+ syntheticArguments: registrationArguments[registration.name] ?? [{}],
71
+ })),
72
+ hooks: fixture.hookDetails.map((hook) => ({
73
+ id: `hook.${hook.name}:${fixture.id}:${slugForArtifact(hook.ref)}`,
74
+ fixture: fixture.id,
75
+ hook: hook.name,
76
+ ref: hook.ref,
77
+ support: "synthetic-event-required",
78
+ assertions: hookAssertions[hook.name] ?? ["hook payload and return value are captured"],
79
+ syntheticEvent: hookEvents[hook.name] ?? { hook: hook.name, fixture: fixture.id },
80
+ syntheticContext: hookContexts[hook.name] ?? { hook: hook.name, fixture: fixture.id },
81
+ })),
82
+ sdkImports: fixture.sdkImportDetails.map((sdkImport) => ({
83
+ id: `sdk.${sdkImport.specifier}:${fixture.id}:${slugForArtifact(sdkImport.ref)}`,
84
+ fixture: fixture.id,
85
+ specifier: sdkImport.specifier,
86
+ ref: sdkImport.ref,
87
+ support: sdkExports.has(sdkImport.specifier) ? "target-exported" : "compat-alias-required",
88
+ assertions: ["package export exists", "cold import resolves without plugin credentials"],
89
+ })),
90
+ packageEntrypoints: packageEntrypoints(fixture),
91
+ }));
92
+
93
+ const issueProbes = report.contractProbes.map((probe) => ({
94
+ id: probe.id,
95
+ fixture: probe.fixture,
96
+ priority: probe.priority,
97
+ target: probe.target,
98
+ evidence: probe.evidence,
99
+ assertions: assertionsForProbeTarget(probe.target),
100
+ }));
101
+
102
+ const allRegistrations = fixtures.flatMap((fixture) => fixture.registrations);
103
+ const allHooks = fixtures.flatMap((fixture) => fixture.hooks);
104
+ const allSdkImports = fixtures.flatMap((fixture) => fixture.sdkImports);
105
+ const allPackageEntrypoints = fixtures.flatMap((fixture) => fixture.packageEntrypoints);
106
+
107
+ return {
108
+ generatedAt: report.generatedAt,
109
+ targetOpenClaw: {
110
+ status: report.targetOpenClaw.status,
111
+ configuredPath: report.targetOpenClaw.configuredPath,
112
+ capturedRegistrarCount: report.targetOpenClaw.capturedRegistrarCount ?? 0,
113
+ sdkExportCount: report.targetOpenClaw.sdkExportCount ?? 0,
114
+ },
115
+ summary: {
116
+ fixtureCount: fixtures.length,
117
+ registrationCount: allRegistrations.length,
118
+ hookCount: allHooks.length,
119
+ sdkImportCount: allSdkImports.length,
120
+ packageEntrypointCount: allPackageEntrypoints.length,
121
+ issueProbeCount: issueProbes.length,
122
+ inspectorShimRequiredCount: allRegistrations.filter((item) => item.support === "inspector-shim-required").length,
123
+ compatAliasRequiredCount: allSdkImports.filter((item) => item.support === "compat-alias-required").length,
124
+ },
125
+ fixtures,
126
+ issueProbes,
127
+ };
128
+ }
129
+
130
+ export function validateContractCapture(capture) {
131
+ const errors = [];
132
+
133
+ for (const fixture of capture.fixtures) {
134
+ for (const section of ["registrations", "hooks", "sdkImports", "packageEntrypoints"]) {
135
+ for (const item of fixture[section]) {
136
+ if (!item.ref && section !== "packageEntrypoints") {
137
+ errors.push(`${item.id}: missing source reference`);
138
+ }
139
+ if (!Array.isArray(item.assertions) || item.assertions.length === 0) {
140
+ errors.push(`${item.id}: missing capture assertions`);
141
+ }
142
+ if (section === "registrations" && !Array.isArray(item.syntheticArguments)) {
143
+ errors.push(`${item.id}: missing synthetic registration arguments`);
144
+ }
145
+ if (section === "hooks" && (!item.syntheticEvent || typeof item.syntheticEvent !== "object")) {
146
+ errors.push(`${item.id}: missing synthetic hook event`);
147
+ }
148
+ if (section === "hooks" && (!item.syntheticContext || typeof item.syntheticContext !== "object")) {
149
+ errors.push(`${item.id}: missing synthetic hook context`);
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ for (const probe of capture.issueProbes) {
156
+ if (!Array.isArray(probe.evidence) || probe.evidence.length === 0) {
157
+ errors.push(`${probe.id}: missing probe evidence`);
158
+ }
159
+ if (!Array.isArray(probe.assertions) || probe.assertions.length === 0) {
160
+ errors.push(`${probe.id}: missing probe assertions`);
161
+ }
162
+ }
163
+
164
+ return errors;
165
+ }
166
+
167
+ export async function writeContractCapture(capture, options = {}) {
168
+ return writeJsonMarkdownArtifacts({
169
+ jsonPath: options.jsonPath,
170
+ markdownPath: options.markdownPath,
171
+ json: capture,
172
+ markdown: renderContractCaptureMarkdown(capture, options),
173
+ });
174
+ }
175
+
176
+ export function renderContractCaptureMarkdown(capture, options = {}) {
177
+ return [
178
+ `# ${options.title ?? "Plugin Inspector Contract Capture"}`,
179
+ "",
180
+ `Generated: ${capture.generatedAt}`,
181
+ "",
182
+ "## Summary",
183
+ "",
184
+ markdownTable(
185
+ [
186
+ ["Fixtures", capture.summary.fixtureCount],
187
+ ["Registrations", capture.summary.registrationCount],
188
+ ["Hooks", capture.summary.hookCount],
189
+ ["SDK imports", capture.summary.sdkImportCount],
190
+ ["Package entrypoints", capture.summary.packageEntrypointCount],
191
+ ["Issue probes", capture.summary.issueProbeCount],
192
+ ["Inspector shim required", capture.summary.inspectorShimRequiredCount],
193
+ ["Compat aliases required", capture.summary.compatAliasRequiredCount],
194
+ ],
195
+ ["Metric", "Value"],
196
+ ),
197
+ "",
198
+ "## Registration Capture",
199
+ "",
200
+ markdownTable(
201
+ capture.fixtures.flatMap((fixture) =>
202
+ fixture.registrations.map((item) => [
203
+ fixture.id,
204
+ item.registrar,
205
+ item.support,
206
+ item.ref,
207
+ item.assertions.join("; "),
208
+ ]),
209
+ ),
210
+ ["Fixture", "Registrar", "Support", "Evidence", "Assertions"],
211
+ ),
212
+ "",
213
+ "## Hook Probes",
214
+ "",
215
+ markdownTable(
216
+ capture.fixtures.flatMap((fixture) =>
217
+ fixture.hooks.map((item) => [
218
+ fixture.id,
219
+ item.hook,
220
+ item.support,
221
+ item.ref,
222
+ item.assertions.join("; "),
223
+ ]),
224
+ ),
225
+ ["Fixture", "Hook", "Support", "Evidence", "Assertions"],
226
+ ),
227
+ "",
228
+ "## SDK Import Probes",
229
+ "",
230
+ markdownTable(
231
+ capture.fixtures.flatMap((fixture) =>
232
+ fixture.sdkImports.map((item) => [
233
+ fixture.id,
234
+ item.specifier,
235
+ item.support,
236
+ item.ref,
237
+ item.assertions.join("; "),
238
+ ]),
239
+ ),
240
+ ["Fixture", "Specifier", "Support", "Evidence", "Assertions"],
241
+ ),
242
+ "",
243
+ "## Issue Probe Backlog",
244
+ "",
245
+ markdownTable(
246
+ capture.issueProbes.map((probe) => [
247
+ probe.id,
248
+ probe.priority,
249
+ probe.fixture,
250
+ probe.target,
251
+ probe.assertions.join("; "),
252
+ probe.evidence.join(", "),
253
+ ]),
254
+ ["ID", "Priority", "Fixture", "Target", "Assertions", "Evidence"],
255
+ ),
256
+ ].join("\n");
257
+ }
258
+
259
+ function packageEntrypoints(fixture) {
260
+ return fixture.packages.flatMap((packageSummary) =>
261
+ (packageSummary.openclaw?.entrypoints ?? []).map((entrypoint) => ({
262
+ id: `package.${entrypoint.kind}:${fixture.id}:${slugForArtifact(entrypoint.relativePath)}`,
263
+ fixture: fixture.id,
264
+ kind: entrypoint.kind,
265
+ specifier: entrypoint.specifier,
266
+ ref: entrypoint.relativePath,
267
+ support: entrypoint.exists ? "source-present" : entrypoint.requiresBuild ? "build-required" : "missing",
268
+ assertions: ["entrypoint path resolves", "entrypoint can be cold-imported after required build step"],
269
+ })),
270
+ );
271
+ }
272
+
273
+ function assertionsForProbeTarget(target) {
274
+ const assertions = {
275
+ "channel-runtime": ["message envelope is stable", "sender/config metadata is preserved"],
276
+ "hook-runner": ["synthetic event payload is accepted", "return semantics are preserved"],
277
+ "inspector-capture-api": ["registration arguments are recorded", "registered handler metadata is retained"],
278
+ "manifest-loader": ["metadata key is accepted", "migration or compatibility mapping is visible"],
279
+ "package-loader": ["entrypoint metadata resolves", "cold import failure mode is classified"],
280
+ "sdk-alias": ["package export exists", "migration metadata is visible when alias is missing"],
281
+ "tool-runtime": ["tool schema is captured", "tool result metadata is retained"],
282
+ };
283
+ return assertions[target] ?? ["probe has fixture evidence and a target contract"];
284
+ }
285
+
286
+ function markdownTable(rows, headers) {
287
+ return renderPaddedMarkdownTable(rows, headers);
288
+ }
@@ -0,0 +1,167 @@
1
+ import { knownIssueCodes } from "./issues.js";
2
+
3
+ export const knownIssueClasses = new Set([
4
+ "compat-gap",
5
+ "deprecation-warning",
6
+ "fixture-regression",
7
+ "inspector-gap",
8
+ "live-issue",
9
+ "upstream-metadata",
10
+ ]);
11
+
12
+ export function validateContractCoverage(report, options = {}) {
13
+ const errors = [];
14
+ const issueCodes = options.knownIssueCodes ?? knownIssueCodes;
15
+ const issueClasses = options.knownIssueClasses ?? knownIssueClasses;
16
+
17
+ if (report.breakages.length > 0) {
18
+ for (const breakage of report.breakages) {
19
+ errors.push(`hard breakage: ${breakage.fixture} ${breakage.code}: ${breakage.message}`);
20
+ }
21
+ }
22
+
23
+ requireUniqueIssueIds(report, errors);
24
+ requireKnownIssueCodes(report, errors, issueCodes);
25
+ requireKnownIssueClasses(report, errors, issueClasses);
26
+ requireIssueEvidence(report, errors);
27
+ requireP1ProbeCoverage(report, errors);
28
+ requireFixtureEvidence(report, errors);
29
+ requireTargetHookRegistry(report, errors);
30
+ requireTargetApiBuilder(report, errors);
31
+ requireTargetCapturedRegistration(report, errors);
32
+ requireTargetSdkExports(report, errors);
33
+ requireTargetManifestTypes(report, errors);
34
+ requireCompatRecordReconciliation(report, errors);
35
+
36
+ return errors;
37
+ }
38
+
39
+ function requireTargetHookRegistry(report, errors) {
40
+ if (report.targetOpenClaw.status === "ok" && report.targetOpenClaw.hookNames.length === 0) {
41
+ errors.push("target OpenClaw hook registry was found but no hook names were parsed");
42
+ }
43
+ }
44
+
45
+ function requireTargetApiBuilder(report, errors) {
46
+ if (report.targetOpenClaw.status === "ok" && report.targetOpenClaw.apiRegistrars.length === 0) {
47
+ errors.push("target OpenClaw API builder was found but no api.register* names were parsed");
48
+ }
49
+ }
50
+
51
+ function requireTargetCapturedRegistration(report, errors) {
52
+ if (report.targetOpenClaw.status === "ok" && report.targetOpenClaw.capturedRegistrars.length === 0) {
53
+ errors.push("target OpenClaw captured-registration helper was found but no api.register* names were parsed");
54
+ }
55
+ }
56
+
57
+ function requireTargetSdkExports(report, errors) {
58
+ if (report.targetOpenClaw.status === "ok" && report.targetOpenClaw.sdkExports.length === 0) {
59
+ errors.push("target OpenClaw package metadata was found but no plugin SDK exports were parsed");
60
+ }
61
+ }
62
+
63
+ function requireTargetManifestTypes(report, errors) {
64
+ if (report.targetOpenClaw.status !== "ok") {
65
+ return;
66
+ }
67
+ if (report.targetOpenClaw.manifestFields.length === 0) {
68
+ errors.push("target OpenClaw manifest types were found but no PluginManifest fields were parsed");
69
+ }
70
+ if (report.targetOpenClaw.manifestContractFields.length === 0) {
71
+ errors.push("target OpenClaw manifest types were found but no PluginManifestContracts fields were parsed");
72
+ }
73
+ }
74
+
75
+ function requireUniqueIssueIds(report, errors) {
76
+ const seen = new Set();
77
+ for (const issue of report.issues) {
78
+ if (seen.has(issue.id)) {
79
+ errors.push(`duplicate issue id: ${issue.id}`);
80
+ }
81
+ seen.add(issue.id);
82
+ }
83
+ }
84
+
85
+ function requireKnownIssueCodes(report, errors, issueCodes) {
86
+ for (const issue of report.issues) {
87
+ if (!issueCodes.has(issue.code)) {
88
+ errors.push(`${issue.id}: unknown issue code ${issue.code}`);
89
+ }
90
+ }
91
+ }
92
+
93
+ function requireKnownIssueClasses(report, errors, issueClasses) {
94
+ for (const issue of report.issues) {
95
+ if (!issueClasses.has(issue.issueClass)) {
96
+ errors.push(`${issue.id}: unknown issue class ${issue.issueClass}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ function requireIssueEvidence(report, errors) {
102
+ for (const issue of report.issues) {
103
+ if (!Array.isArray(issue.evidence) || issue.evidence.length === 0) {
104
+ errors.push(`${issue.id}: missing evidence`);
105
+ }
106
+ }
107
+ }
108
+
109
+ function requireP1ProbeCoverage(report, errors) {
110
+ const probesByFixture = new Map();
111
+ for (const probe of report.contractProbes) {
112
+ const probes = probesByFixture.get(probe.fixture) ?? [];
113
+ probes.push(probe);
114
+ probesByFixture.set(probe.fixture, probes);
115
+ }
116
+
117
+ for (const issue of report.issues.filter((item) => item.severity === "P1")) {
118
+ const probes = probesByFixture.get(issue.fixture) ?? [];
119
+ if (probes.length === 0) {
120
+ errors.push(`${issue.id}: P1 issue has no contract probe for ${issue.fixture}`);
121
+ }
122
+ }
123
+ }
124
+
125
+ function requireFixtureEvidence(report, errors) {
126
+ for (const fixture of report.fixtures) {
127
+ for (const hook of fixture.hooks) {
128
+ if (!fixture.hookDetails.some((detail) => detail.name === hook)) {
129
+ errors.push(`${fixture.id}: hook ${hook} has no source evidence`);
130
+ }
131
+ }
132
+ for (const registration of fixture.registrations) {
133
+ if (!fixture.registrationDetails.some((detail) => detail.name === registration)) {
134
+ errors.push(`${fixture.id}: registration ${registration} has no source evidence`);
135
+ }
136
+ }
137
+ for (const contract of fixture.manifestContracts) {
138
+ if (contract !== "invalidManifest" && fixture.manifestFiles.length === 0) {
139
+ errors.push(`${fixture.id}: manifest contract ${contract} has no manifest evidence`);
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ function requireCompatRecordReconciliation(report, errors) {
146
+ if (report.targetOpenClaw.status !== "ok") {
147
+ return;
148
+ }
149
+
150
+ const presentRecords = new Set(
151
+ report.logs
152
+ .filter((finding) => finding.code === "compat-record-present")
153
+ .map((finding) => `${finding.fixture}:${finding.compatRecord}`),
154
+ );
155
+ const missingRecords = new Set(
156
+ report.suggestions
157
+ .filter((finding) => finding.code === "missing-compat-record")
158
+ .map((finding) => `${finding.fixture}:${finding.compatRecord}`),
159
+ );
160
+
161
+ for (const finding of [...report.warnings, ...report.suggestions].filter((item) => item.compatRecord)) {
162
+ const key = `${finding.fixture}:${finding.compatRecord}`;
163
+ if (!presentRecords.has(key) && !missingRecords.has(key)) {
164
+ errors.push(`${finding.fixture}: compat record ${finding.compatRecord} was not reconciled`);
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,156 @@
1
+ export const contractProbeRules = {
2
+ "before-tool-call-probe": {
3
+ id: "hook.before_tool_call.terminal-block-approval",
4
+ contract: "Hook returns preserve terminal, block, and approval semantics.",
5
+ target: "hook-runner",
6
+ },
7
+ "channel-contract-probe": {
8
+ id: "channel.runtime.envelope-config-metadata",
9
+ contract: "Channel setup, message envelope, sender metadata, and config schema remain stable.",
10
+ target: "channel-runtime",
11
+ },
12
+ "conversation-access-hook": {
13
+ id: "hook.llm-observer.privacy-payload",
14
+ contract: "LLM observer hooks receive documented prompt/output fields with expected redaction behavior.",
15
+ target: "hook-runner",
16
+ },
17
+ "legacy-root-sdk-import": {
18
+ id: "sdk.import.root-barrel-cold-import",
19
+ contract: "Root plugin SDK barrel remains importable or has a machine-readable migration path.",
20
+ target: "sdk-alias",
21
+ },
22
+ "legacy-before-agent-start": {
23
+ id: "hook.compat.before-agent-start-migration",
24
+ contract: "Legacy before_agent_start remains wired until plugins migrate to before_model_resolve and before_prompt_build.",
25
+ target: "hook-runner",
26
+ },
27
+ "sdk-export-missing": {
28
+ id: "sdk.import.package-export-cold-import",
29
+ contract: "Every observed OpenClaw plugin SDK import remains exported by the target OpenClaw package.",
30
+ target: "sdk-alias",
31
+ },
32
+ "provider-auth-env-vars": {
33
+ id: "manifest.compat.provider-auth-env-vars",
34
+ contract: "Legacy provider auth env metadata continues to map into config/help surfaces.",
35
+ target: "manifest-loader",
36
+ },
37
+ "channel-env-vars": {
38
+ id: "manifest.compat.channel-env-vars",
39
+ contract: "Legacy channel env metadata continues to map into channel setup/help surfaces.",
40
+ target: "manifest-loader",
41
+ },
42
+ "manifest-unknown-contracts": {
43
+ id: "manifest.schema.contract-keys",
44
+ contract: "Manifest contract keys are represented in target OpenClaw PluginManifestContracts.",
45
+ target: "manifest-loader",
46
+ },
47
+ "manifest-unknown-fields": {
48
+ id: "manifest.schema.top-level-fields",
49
+ contract: "Manifest top-level fields are represented in target OpenClaw PluginManifest.",
50
+ target: "manifest-loader",
51
+ },
52
+ "registration-capture-gap": {
53
+ id: "api.capture.runtime-registrars",
54
+ contract: "External inspector capture records service, route, gateway, command, and interactive registrations.",
55
+ target: "inspector-capture-api",
56
+ },
57
+ "package-build-artifact-entrypoint": {
58
+ id: "package.entrypoint.build-before-cold-import",
59
+ contract: "Inspector can build or resolve source aliases before cold importing package entrypoints.",
60
+ target: "package-loader",
61
+ },
62
+ "package-dependency-install-required": {
63
+ id: "package.entrypoint.isolated-dependency-install",
64
+ contract: "Inspector installs package dependencies in an isolated workspace before cold import.",
65
+ target: "package-loader",
66
+ },
67
+ "package-entrypoint-missing": {
68
+ id: "package.entrypoint.exists",
69
+ contract: "OpenClaw package entrypoints resolve to files in the published or built plugin package.",
70
+ target: "package-loader",
71
+ },
72
+ "package-openclaw-entry-missing": {
73
+ id: "package.entrypoint.openclaw-metadata",
74
+ contract: "OpenClaw package metadata declares entrypoints for cold import and registration capture.",
75
+ target: "package-loader",
76
+ },
77
+ "package-openclaw-metadata-missing": {
78
+ id: "package.metadata.openclaw",
79
+ contract: "Plugins that register OpenClaw APIs declare OpenClaw install and entrypoint metadata.",
80
+ target: "package-loader",
81
+ },
82
+ "package-manifest-version-drift": {
83
+ id: "package.metadata.version-alignment",
84
+ contract: "Package and OpenClaw manifest versions stay aligned for release compatibility reporting.",
85
+ target: "package-loader",
86
+ },
87
+ "package-plugin-api-compat-missing": {
88
+ id: "package.compat.plugin-api-range",
89
+ contract: "Package metadata declares the OpenClaw plugin API range used by the plugin.",
90
+ target: "package-loader",
91
+ },
92
+ "package-typescript-source-entrypoint": {
93
+ id: "package.entrypoint.typescript-loader",
94
+ contract: "Inspector can compile or load TypeScript source entrypoints before registration capture.",
95
+ target: "package-loader",
96
+ },
97
+ "runtime-tool-capture": {
98
+ id: "tool.registration.schema-capture",
99
+ contract: "Registered runtime tools expose stable names, input schemas, and result metadata.",
100
+ target: "tool-runtime",
101
+ },
102
+ };
103
+
104
+ export function buildContractProbes({ warnings = [], suggestions = [], fixtures = [] }) {
105
+ const fixtureById = new Map(fixtures.map((fixture) => [fixture.id, fixture]));
106
+ const probes = [];
107
+
108
+ for (const finding of [...warnings, ...suggestions]) {
109
+ const rule = contractProbeRules[finding.code];
110
+ if (!rule) {
111
+ continue;
112
+ }
113
+ probes.push({
114
+ id: `${rule.id}:${finding.fixture}`,
115
+ fixture: finding.fixture,
116
+ priority: probePriority(finding.code, fixtureById.get(finding.fixture)?.priority),
117
+ target: rule.target,
118
+ contract: rule.contract,
119
+ evidence: finding.evidence ?? [],
120
+ });
121
+ }
122
+
123
+ return dedupeBy(probes, (probe) => probe.id).sort(
124
+ (left, right) => priorityRank(left.priority) - priorityRank(right.priority) || left.id.localeCompare(right.id),
125
+ );
126
+ }
127
+
128
+ export function probePriority(code, fixturePriority) {
129
+ if (
130
+ [
131
+ "before-tool-call-probe",
132
+ "conversation-access-hook",
133
+ "missing-compat-record",
134
+ "registration-capture-gap",
135
+ "sdk-export-missing",
136
+ ].includes(code)
137
+ ) {
138
+ return "P1";
139
+ }
140
+ if (fixturePriority === "high") {
141
+ return "P2";
142
+ }
143
+ return "P3";
144
+ }
145
+
146
+ function dedupeBy(values, keyForValue) {
147
+ const output = new Map();
148
+ for (const value of values) {
149
+ output.set(keyForValue(value), value);
150
+ }
151
+ return [...output.values()];
152
+ }
153
+
154
+ function priorityRank(priority) {
155
+ return { P0: 0, P1: 1, P2: 2, P3: 3 }[priority] ?? 4;
156
+ }