@openclaw/plugin-inspector 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.js ADDED
@@ -0,0 +1,182 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export const npmPackagePayloadDir = ".crabpot-package";
6
+ export const defaultPluginRootConfigFiles = ["plugin-inspector.config.json", ".plugin-inspector.json"];
7
+
8
+ export async function loadInspectorConfig(configPath, options = {}) {
9
+ if (!configPath) {
10
+ throw new Error("--config is required");
11
+ }
12
+ const resolvedPath = path.resolve(options.cwd ?? process.cwd(), configPath);
13
+ const config = JSON.parse(await readFile(resolvedPath, "utf8"));
14
+ const rootDir = path.resolve(options.cwd ?? process.cwd(), options.rootDir ?? path.dirname(resolvedPath));
15
+ const normalizedConfig = await normalizeInspectorConfig(config, { rootDir });
16
+ validateInspectorConfig(normalizedConfig);
17
+ return {
18
+ ...normalizedConfig,
19
+ rootDir,
20
+ configPath: resolvedPath,
21
+ };
22
+ }
23
+
24
+ export async function loadPluginRootConfig(configPath = null, options = {}) {
25
+ const rootDir = path.resolve(options.cwd ?? process.cwd());
26
+ const resolvedPath = configPath ? path.resolve(rootDir, configPath) : findPluginRootConfigPath(rootDir);
27
+ if (!resolvedPath && !existsSync(path.join(rootDir, "package.json")) && !existsSync(path.join(rootDir, "openclaw.plugin.json"))) {
28
+ throw new Error("run from a plugin root with package.json/openclaw.plugin.json, or pass --config");
29
+ }
30
+ const config = resolvedPath ? JSON.parse(await readFile(resolvedPath, "utf8")) : { version: 1 };
31
+ const normalizedConfig = await normalizePluginRootConfig(config, { rootDir });
32
+ validateInspectorConfig(normalizedConfig);
33
+ return {
34
+ ...normalizedConfig,
35
+ rootDir,
36
+ configPath: resolvedPath,
37
+ };
38
+ }
39
+
40
+ export function validateInspectorConfig(config) {
41
+ const errors = [];
42
+
43
+ if (config.version !== 1) {
44
+ errors.push("config.version must be 1");
45
+ }
46
+
47
+ if (!config.submoduleRoot || typeof config.submoduleRoot !== "string") {
48
+ errors.push("config.submoduleRoot must be set");
49
+ }
50
+
51
+ if (!Array.isArray(config.fixtures) || config.fixtures.length === 0) {
52
+ errors.push("config.fixtures must be a non-empty array");
53
+ }
54
+
55
+ const ids = new Set();
56
+ const paths = new Set();
57
+ for (const fixture of config.fixtures ?? []) {
58
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(fixture.id ?? "")) {
59
+ errors.push(`invalid fixture id: ${fixture.id}`);
60
+ }
61
+ if (ids.has(fixture.id)) {
62
+ errors.push(`duplicate fixture id: ${fixture.id}`);
63
+ }
64
+ ids.add(fixture.id);
65
+
66
+ if (typeof fixture.path !== "string" || fixture.path.length === 0) {
67
+ errors.push(`${fixture.id}: path must be set`);
68
+ }
69
+ if (paths.has(fixture.path)) {
70
+ errors.push(`duplicate fixture path: ${fixture.path}`);
71
+ }
72
+ paths.add(fixture.path);
73
+
74
+ const hasRepo = typeof fixture.repo === "string";
75
+ const hasPackage = fixture.package && typeof fixture.package === "object";
76
+ if (hasRepo === hasPackage) {
77
+ errors.push(`${fixture.id}: fixture must declare exactly one of repo or package`);
78
+ }
79
+ if (!["high", "medium", "low"].includes(fixture.priority)) {
80
+ errors.push(`${fixture.id}: priority must be high, medium, or low`);
81
+ }
82
+ if (!Array.isArray(fixture.seams) || fixture.seams.length === 0) {
83
+ errors.push(`${fixture.id}: seams must be non-empty`);
84
+ }
85
+ for (const key of ["hooks", "registrations", "manifestContracts"]) {
86
+ const values = fixture.expect?.[key];
87
+ if (values !== undefined && (!Array.isArray(values) || values.length === 0)) {
88
+ errors.push(`${fixture.id}: expect.${key} must be a non-empty array when present`);
89
+ }
90
+ }
91
+ }
92
+
93
+ if (errors.length > 0) {
94
+ throw new Error(errors.join("\n"));
95
+ }
96
+ }
97
+
98
+ export function fixtureCheckoutPath(config, fixture) {
99
+ return path.resolve(config.rootDir ?? process.cwd(), fixture.path);
100
+ }
101
+
102
+ export function fixtureSourceRoot(config, fixture) {
103
+ const checkoutPath = fixtureCheckoutPath(config, fixture);
104
+ if (fixture.subdir) {
105
+ return path.join(checkoutPath, fixture.subdir);
106
+ }
107
+ if (fixture.package) {
108
+ return path.join(checkoutPath, npmPackagePayloadDir);
109
+ }
110
+ return checkoutPath;
111
+ }
112
+
113
+ export async function normalizePluginRootConfig(config, options = {}) {
114
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
115
+ const plugin = config.plugin ?? {};
116
+ const packageJson = await readJsonIfExists(path.join(rootDir, "package.json"));
117
+ const pluginManifest = await readJsonIfExists(path.join(rootDir, "openclaw.plugin.json"));
118
+ const sourceRoot = plugin.sourceRoot ?? config.sourceRoot ?? ".";
119
+ const fixture = {
120
+ id: plugin.id ?? pluginManifest?.id ?? packageId(packageJson?.name) ?? "plugin",
121
+ name: plugin.name ?? pluginManifest?.name ?? packageJson?.name ?? "Plugin",
122
+ path: ".",
123
+ repo: "local",
124
+ priority: plugin.priority ?? config.priority ?? "high",
125
+ seams: plugin.seams ?? config.seams ?? inferPluginSeams(pluginManifest, packageJson),
126
+ why: plugin.why ?? config.why ?? "local OpenClaw plugin root",
127
+ expect: plugin.expect ?? config.expect,
128
+ };
129
+
130
+ if (sourceRoot !== ".") {
131
+ fixture.subdir = sourceRoot;
132
+ }
133
+
134
+ return {
135
+ version: 1,
136
+ submoduleRoot: ".",
137
+ openclaw: config.openclaw,
138
+ fixtures: [fixture],
139
+ };
140
+ }
141
+
142
+ export async function normalizeInspectorConfig(config, options = {}) {
143
+ if (Array.isArray(config.fixtures)) {
144
+ return config;
145
+ }
146
+ return normalizePluginRootConfig(config, options);
147
+ }
148
+
149
+ function findPluginRootConfigPath(rootDir) {
150
+ return defaultPluginRootConfigFiles.map((file) => path.join(rootDir, file)).find(existsSync) ?? null;
151
+ }
152
+
153
+ async function readJsonIfExists(filePath) {
154
+ if (!existsSync(filePath)) {
155
+ return null;
156
+ }
157
+ return JSON.parse(await readFile(filePath, "utf8"));
158
+ }
159
+
160
+ function packageId(packageName) {
161
+ if (!packageName) {
162
+ return null;
163
+ }
164
+ return packageName
165
+ .split("/")
166
+ .pop()
167
+ .replace(/^openclaw-/, "")
168
+ .replace(/[^a-zA-Z0-9]+/g, "-")
169
+ .replace(/^-+|-+$/g, "")
170
+ .toLowerCase();
171
+ }
172
+
173
+ function inferPluginSeams(pluginManifest, packageJson) {
174
+ const contracts = Object.keys(pluginManifest?.contracts ?? {});
175
+ if (contracts.includes("tools")) {
176
+ return ["dynamic-tool"];
177
+ }
178
+ if (packageJson?.openclaw?.extensions || packageJson?.openclaw?.runtimeExtensions) {
179
+ return ["plugin-runtime"];
180
+ }
181
+ return ["plugin-metadata"];
182
+ }
@@ -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
+ }