codex-plugin-doctor 1.48.0 → 1.49.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/README.md +4 -2
- package/dist/core/corpus-quality-metrics.d.ts +68 -0
- package/dist/core/corpus-quality-metrics.js +289 -0
- package/dist/core/output-contract.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run-cli.d.ts +2 -1
- package/dist/run-cli.js +61 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -319,6 +319,8 @@ External corpus evaluation is static and offline. `doctor corpus --manifest <cor
|
|
|
319
319
|
|
|
320
320
|
`doctor corpus metrics --manifest <corpus.json>` measures reviewed true positives, false positives, and false negatives as precision, recall, and emitted-finding false-positive share. Optional thresholds make the measurement release-gating; incomplete reviews and digest drift return exit `2` instead of producing a favorable score. The dedicated metrics manifest stays offline, requires contained snapshot paths, and may record immutable HTTPS Git provenance without fetching it.
|
|
321
321
|
|
|
322
|
+
`doctor corpus metrics diff --before <metrics.json> --after <metrics.json>` compares two complete metrics reports from the same corpus identity. Add `--fail-on-regression` to fail CI when precision or recall decreases, or when the reviewed emitted-finding false-positive share increases. Reports with different `corpusDigest` values are rejected as non-comparable, and diff output never includes the input report paths.
|
|
323
|
+
|
|
322
324
|
`self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
|
|
323
325
|
|
|
324
326
|
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. The JSON report includes tarball metadata such as filename, path, integrity, shasum, sizes, file count, and extracted package root so automation can prove which archive was scanned. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor runtime-plan <path>` creates a non-executing runtime plan that lists MCP server commands, safe probe methods, risk reasons, and a stable approval digest before any local server is started. Add `--markdown --output runtime-plan.md` to preserve a review-ready approval artifact with the execution boundary, checklist, servers, probes, and risk reasons. `doctor runtime-policy <path>` evaluates the same runtime plan and security signals, then recommends `allow`, `review`, `sandbox_recommended`, or `deny` before local MCP execution starts. `doctor review-bundle <path> --output <dir> --sign-key-env NAME` writes a signed review directory with runtime plan, runtime policy, attestation, release evidence, manifest, Markdown summary files, and SHA-256 file integrity digests. `doctor review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME` verifies the bundle manifest, expected files, manifest integrity digests, runtime artifacts, signed attestation, and signed release evidence offline before a reviewer trusts the handoff. `doctor review-bundle diff --before <dir> --after <dir>` compares two review bundles and flags risk-increasing changes in status, runtime policy, release readiness, signatures, release evidence, and runtime plan digest. `check --runtime --require-runtime-approval --runtime-approval-digest <digest>` refuses to run runtime probes unless the current plan digest matches the approved digest. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, git release gates, and runtime approval status. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
|
|
@@ -425,9 +427,9 @@ jobs:
|
|
|
425
427
|
runs-on: ubuntu-latest
|
|
426
428
|
steps:
|
|
427
429
|
- uses: actions/checkout@v5
|
|
428
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
430
|
+
- uses: Esquetta/CodexPluginDoctor@v1.49.0
|
|
429
431
|
with:
|
|
430
|
-
version: "1.
|
|
432
|
+
version: "1.49.0"
|
|
431
433
|
path: .
|
|
432
434
|
runtime: "true"
|
|
433
435
|
policy: codex-publish
|
|
@@ -87,6 +87,7 @@ export interface CorpusQualityMetricsReport {
|
|
|
87
87
|
kind: "doctor.validation.corpus.metrics";
|
|
88
88
|
generatedAt: string;
|
|
89
89
|
version: string;
|
|
90
|
+
corpusDigest: string;
|
|
90
91
|
status: "pass" | "fail" | "incomplete";
|
|
91
92
|
exitCode: 0 | 1 | 2;
|
|
92
93
|
summary: CorpusMetricCounts & CorpusMetricValues & {
|
|
@@ -98,6 +99,64 @@ export interface CorpusQualityMetricsReport {
|
|
|
98
99
|
thresholdChecks: CorpusMetricThresholdCheck[];
|
|
99
100
|
targets: CorpusQualityMetricsTargetResult[];
|
|
100
101
|
}
|
|
102
|
+
export interface CorpusMetricCountDeltas {
|
|
103
|
+
truePositives: number;
|
|
104
|
+
falsePositives: number;
|
|
105
|
+
falseNegatives: number;
|
|
106
|
+
resolvedFalsePositives: number;
|
|
107
|
+
unreviewed: number;
|
|
108
|
+
unclear: number;
|
|
109
|
+
}
|
|
110
|
+
export interface CorpusMetricValueComparison {
|
|
111
|
+
before: number | null;
|
|
112
|
+
after: number | null;
|
|
113
|
+
delta: number | null;
|
|
114
|
+
}
|
|
115
|
+
export interface CorpusQualityMetricsDiffTarget {
|
|
116
|
+
id: string;
|
|
117
|
+
regressed: boolean;
|
|
118
|
+
counts: CorpusMetricCountDeltas;
|
|
119
|
+
precision: CorpusMetricValueComparison;
|
|
120
|
+
recall: CorpusMetricValueComparison;
|
|
121
|
+
falsePositiveRate: CorpusMetricValueComparison;
|
|
122
|
+
}
|
|
123
|
+
export interface CorpusQualityMetricsDiffReport {
|
|
124
|
+
schemaVersion: "1.0.0";
|
|
125
|
+
kind: "doctor.validation.corpus.metrics.diff";
|
|
126
|
+
generatedAt: string;
|
|
127
|
+
version: string;
|
|
128
|
+
corpusDigest: string;
|
|
129
|
+
status: "pass" | "fail";
|
|
130
|
+
exitCode: 0 | 1;
|
|
131
|
+
failOnRegression: boolean;
|
|
132
|
+
summary: {
|
|
133
|
+
comparable: true;
|
|
134
|
+
regression: boolean;
|
|
135
|
+
changedTargets: number;
|
|
136
|
+
precisionBefore: number | null;
|
|
137
|
+
precisionAfter: number | null;
|
|
138
|
+
precisionDelta: number | null;
|
|
139
|
+
recallBefore: number | null;
|
|
140
|
+
recallAfter: number | null;
|
|
141
|
+
recallDelta: number | null;
|
|
142
|
+
falsePositiveRateBefore: number | null;
|
|
143
|
+
falsePositiveRateAfter: number | null;
|
|
144
|
+
falsePositiveRateDelta: number | null;
|
|
145
|
+
counts: CorpusMetricCountDeltas;
|
|
146
|
+
};
|
|
147
|
+
before: {
|
|
148
|
+
version: string;
|
|
149
|
+
targetCount: number;
|
|
150
|
+
};
|
|
151
|
+
after: {
|
|
152
|
+
version: string;
|
|
153
|
+
targetCount: number;
|
|
154
|
+
};
|
|
155
|
+
targets: CorpusQualityMetricsDiffTarget[];
|
|
156
|
+
}
|
|
157
|
+
export interface BuildCorpusMetricsDiffOptions {
|
|
158
|
+
failOnRegression?: boolean;
|
|
159
|
+
}
|
|
101
160
|
export interface BuildCorpusMetricsOptions {
|
|
102
161
|
thresholds?: CorpusMetricThresholds;
|
|
103
162
|
environment?: {
|
|
@@ -114,6 +173,9 @@ export interface BuildCorpusMetricsOptions {
|
|
|
114
173
|
export declare class CorpusMetricsManifestError extends Error {
|
|
115
174
|
constructor(message: string);
|
|
116
175
|
}
|
|
176
|
+
export declare class CorpusMetricsDiffError extends Error {
|
|
177
|
+
constructor(message: string);
|
|
178
|
+
}
|
|
117
179
|
export declare function loadCorpusMetricsManifest(manifestPath: string): Promise<LoadedCorpusMetricsManifest>;
|
|
118
180
|
export declare function reconcileCorpusMetricFindings(reviews: CorpusMetricFindingReview[], actualFindings: CorpusMetricFinding[]): CorpusMetricReconciliation;
|
|
119
181
|
export declare function buildCorpusQualityMetricsReport(manifestPath: string, options?: BuildCorpusMetricsOptions): Promise<CorpusQualityMetricsReport>;
|
|
@@ -122,4 +184,10 @@ export declare function renderCorpusQualityMetricsText(report: CorpusQualityMetr
|
|
|
122
184
|
outputPath?: string | null;
|
|
123
185
|
}): string;
|
|
124
186
|
export declare function renderCorpusQualityMetricsMarkdown(report: CorpusQualityMetricsReport): string;
|
|
187
|
+
export declare function buildCorpusQualityMetricsDiffReport(beforePath: string, afterPath: string, options?: BuildCorpusMetricsDiffOptions): Promise<CorpusQualityMetricsDiffReport>;
|
|
188
|
+
export declare function renderCorpusQualityMetricsDiffJson(report: CorpusQualityMetricsDiffReport): string;
|
|
189
|
+
export declare function renderCorpusQualityMetricsDiffText(report: CorpusQualityMetricsDiffReport, options?: {
|
|
190
|
+
outputPath?: string | null;
|
|
191
|
+
}): string;
|
|
192
|
+
export declare function renderCorpusQualityMetricsDiffMarkdown(report: CorpusQualityMetricsDiffReport): string;
|
|
125
193
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { buildGenericMcpDoctor } from "../mcp/generic-mcp-doctor.js";
|
|
@@ -11,6 +12,12 @@ export class CorpusMetricsManifestError extends Error {
|
|
|
11
12
|
this.name = "CorpusMetricsManifestError";
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
export class CorpusMetricsDiffError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CorpusMetricsDiffError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
const manifestKeys = new Set(["schemaVersion", "thresholds", "targets"]);
|
|
15
22
|
const thresholdKeys = new Set(["minPrecision", "minRecall", "maxFalsePositiveRate"]);
|
|
16
23
|
const targetKeys = new Set([
|
|
@@ -206,6 +213,26 @@ function findingKey(findingId, fingerprint) {
|
|
|
206
213
|
function roundMetric(value) {
|
|
207
214
|
return value === null ? null : Math.round(value * 1_000_000) / 1_000_000;
|
|
208
215
|
}
|
|
216
|
+
function buildCorpusDigest(manifest) {
|
|
217
|
+
const identity = manifest.targets
|
|
218
|
+
.map((target) => ({
|
|
219
|
+
id: target.id,
|
|
220
|
+
profile: target.profile,
|
|
221
|
+
sourceType: target.sourceType,
|
|
222
|
+
mode: target.mode,
|
|
223
|
+
contentDigest: target.contentDigest,
|
|
224
|
+
reviews: target.reviews
|
|
225
|
+
.map((review) => ({
|
|
226
|
+
findingId: review.findingId,
|
|
227
|
+
fingerprint: review.fingerprint,
|
|
228
|
+
classification: review.classification
|
|
229
|
+
}))
|
|
230
|
+
.sort((left, right) => findingKey(left.findingId, left.fingerprint)
|
|
231
|
+
.localeCompare(findingKey(right.findingId, right.fingerprint)))
|
|
232
|
+
}))
|
|
233
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
234
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(identity)).digest("hex")}`;
|
|
235
|
+
}
|
|
209
236
|
function calculateRawMetrics(counts) {
|
|
210
237
|
const precisionDenominator = counts.truePositives + counts.falsePositives;
|
|
211
238
|
const recallDenominator = counts.truePositives + counts.falseNegatives;
|
|
@@ -339,6 +366,7 @@ export async function buildCorpusQualityMetricsReport(manifestPath, options = {}
|
|
|
339
366
|
kind: "doctor.validation.corpus.metrics",
|
|
340
367
|
generatedAt: new Date().toISOString(),
|
|
341
368
|
version: packageVersion,
|
|
369
|
+
corpusDigest: buildCorpusDigest(manifest),
|
|
342
370
|
status,
|
|
343
371
|
exitCode,
|
|
344
372
|
summary: {
|
|
@@ -431,3 +459,264 @@ export function renderCorpusQualityMetricsMarkdown(report) {
|
|
|
431
459
|
}
|
|
432
460
|
return lines.join("\n");
|
|
433
461
|
}
|
|
462
|
+
const reportSizeLimit = 5 * 1024 * 1024;
|
|
463
|
+
const countKeys = [
|
|
464
|
+
"truePositives",
|
|
465
|
+
"falsePositives",
|
|
466
|
+
"falseNegatives",
|
|
467
|
+
"resolvedFalsePositives",
|
|
468
|
+
"unreviewed",
|
|
469
|
+
"unclear"
|
|
470
|
+
];
|
|
471
|
+
function requireNonNegativeInteger(value, label) {
|
|
472
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
473
|
+
throw new CorpusMetricsDiffError(`${label} must be a non-negative integer.`);
|
|
474
|
+
}
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
function parseCounts(value, label) {
|
|
478
|
+
if (!isRecord(value))
|
|
479
|
+
throw new CorpusMetricsDiffError(`${label} is invalid.`);
|
|
480
|
+
return Object.fromEntries(countKeys.map((key) => [
|
|
481
|
+
key,
|
|
482
|
+
requireNonNegativeInteger(value[key], `${label}.${key}`)
|
|
483
|
+
]));
|
|
484
|
+
}
|
|
485
|
+
function metricsMatchCounts(value, counts) {
|
|
486
|
+
if (!isRecord(value))
|
|
487
|
+
return false;
|
|
488
|
+
const expected = calculateMetrics(counts);
|
|
489
|
+
return value.precision === expected.precision &&
|
|
490
|
+
value.recall === expected.recall &&
|
|
491
|
+
value.falsePositiveRate === expected.falsePositiveRate;
|
|
492
|
+
}
|
|
493
|
+
async function loadComparableMetricsReport(reportPath, label) {
|
|
494
|
+
let fileStat;
|
|
495
|
+
try {
|
|
496
|
+
fileStat = await stat(path.resolve(reportPath));
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report could not be read.`);
|
|
500
|
+
}
|
|
501
|
+
if (!fileStat.isFile() || fileStat.size > reportSizeLimit) {
|
|
502
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report must be a file no larger than 5 MiB.`);
|
|
503
|
+
}
|
|
504
|
+
let value;
|
|
505
|
+
try {
|
|
506
|
+
value = await readJsonFile(path.resolve(reportPath));
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report could not be read as JSON.`);
|
|
510
|
+
}
|
|
511
|
+
if (!isRecord(value) || value.schemaVersion !== "1.0.0" ||
|
|
512
|
+
value.kind !== "doctor.validation.corpus.metrics") {
|
|
513
|
+
throw new CorpusMetricsDiffError(`${label} file is not a supported corpus metrics report.`);
|
|
514
|
+
}
|
|
515
|
+
if (typeof value.version !== "string" || value.version.length === 0) {
|
|
516
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report version is invalid.`);
|
|
517
|
+
}
|
|
518
|
+
if (typeof value.corpusDigest !== "string" || !sha256Digest.test(value.corpusDigest)) {
|
|
519
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report is missing a valid corpusDigest.`);
|
|
520
|
+
}
|
|
521
|
+
if (!isRecord(value.summary) || !Array.isArray(value.targets)) {
|
|
522
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report structure is invalid.`);
|
|
523
|
+
}
|
|
524
|
+
if (value.status === "incomplete" || value.summary.incompleteTargets !== 0 ||
|
|
525
|
+
value.summary.unreviewed !== 0 || value.summary.unclear !== 0) {
|
|
526
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report must be complete.`);
|
|
527
|
+
}
|
|
528
|
+
if ((value.status !== "pass" && value.status !== "fail") ||
|
|
529
|
+
(value.status === "pass" ? value.exitCode !== 0 : value.exitCode !== 1)) {
|
|
530
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report status is invalid.`);
|
|
531
|
+
}
|
|
532
|
+
const targetCount = requireNonNegativeInteger(value.summary.targetCount, `${label} summary.targetCount`);
|
|
533
|
+
const summaryCounts = parseCounts(value.summary, `${label} summary`);
|
|
534
|
+
if (targetCount === 0 || targetCount !== value.targets.length ||
|
|
535
|
+
value.summary.completeTargets !== targetCount || !metricsMatchCounts(value.summary, summaryCounts)) {
|
|
536
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report is internally inconsistent.`);
|
|
537
|
+
}
|
|
538
|
+
const ids = new Set();
|
|
539
|
+
const targets = value.targets.map((target, index) => {
|
|
540
|
+
if (!isRecord(target) || typeof target.id !== "string" || !publicSafeId.test(target.id) ||
|
|
541
|
+
target.complete !== true || target.digestMatched !== true) {
|
|
542
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics target ${index} is invalid or incomplete.`);
|
|
543
|
+
}
|
|
544
|
+
if (ids.has(target.id)) {
|
|
545
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report contains duplicate target IDs.`);
|
|
546
|
+
}
|
|
547
|
+
ids.add(target.id);
|
|
548
|
+
const counts = parseCounts(target.counts, `${label} target ${target.id} counts`);
|
|
549
|
+
if (!metricsMatchCounts(target.metrics, counts)) {
|
|
550
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report is internally inconsistent.`);
|
|
551
|
+
}
|
|
552
|
+
return { id: target.id, counts };
|
|
553
|
+
});
|
|
554
|
+
const summedCounts = targets.reduce((total, target) => addCounts(total, target.counts), {
|
|
555
|
+
truePositives: 0,
|
|
556
|
+
falsePositives: 0,
|
|
557
|
+
falseNegatives: 0,
|
|
558
|
+
resolvedFalsePositives: 0,
|
|
559
|
+
unreviewed: 0,
|
|
560
|
+
unclear: 0
|
|
561
|
+
});
|
|
562
|
+
if (countKeys.some((key) => summedCounts[key] !== summaryCounts[key])) {
|
|
563
|
+
throw new CorpusMetricsDiffError(`${label} corpus metrics report is internally inconsistent.`);
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
version: value.version,
|
|
567
|
+
corpusDigest: value.corpusDigest,
|
|
568
|
+
summary: { ...summaryCounts, targetCount },
|
|
569
|
+
targets
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function subtractCounts(after, before) {
|
|
573
|
+
return Object.fromEntries(countKeys.map((key) => [key, after[key] - before[key]]));
|
|
574
|
+
}
|
|
575
|
+
function compareMetric(before, after) {
|
|
576
|
+
return {
|
|
577
|
+
before: roundMetric(before),
|
|
578
|
+
after: roundMetric(after),
|
|
579
|
+
delta: before === null || after === null ? null : roundMetric(after - before)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function metricRegressed(precision, recall, falsePositiveRate) {
|
|
583
|
+
return (precision.delta !== null && precision.delta < 0) ||
|
|
584
|
+
(recall.delta !== null && recall.delta < 0) ||
|
|
585
|
+
(falsePositiveRate.delta !== null && falsePositiveRate.delta > 0);
|
|
586
|
+
}
|
|
587
|
+
export async function buildCorpusQualityMetricsDiffReport(beforePath, afterPath, options = {}) {
|
|
588
|
+
const [before, after] = await Promise.all([
|
|
589
|
+
loadComparableMetricsReport(beforePath, "Before"),
|
|
590
|
+
loadComparableMetricsReport(afterPath, "After")
|
|
591
|
+
]);
|
|
592
|
+
const beforeIds = before.targets.map((target) => target.id).sort();
|
|
593
|
+
const afterIds = after.targets.map((target) => target.id).sort();
|
|
594
|
+
if (before.corpusDigest !== after.corpusDigest || JSON.stringify(beforeIds) !== JSON.stringify(afterIds)) {
|
|
595
|
+
throw new CorpusMetricsDiffError("Corpus metrics reports describe different corpus identities.");
|
|
596
|
+
}
|
|
597
|
+
const afterTargets = new Map(after.targets.map((target) => [target.id, target]));
|
|
598
|
+
const targets = before.targets.map((beforeTarget) => {
|
|
599
|
+
const afterTarget = afterTargets.get(beforeTarget.id);
|
|
600
|
+
const beforeMetrics = calculateRawMetrics(beforeTarget.counts);
|
|
601
|
+
const afterMetrics = calculateRawMetrics(afterTarget.counts);
|
|
602
|
+
const precision = compareMetric(beforeMetrics.precision, afterMetrics.precision);
|
|
603
|
+
const recall = compareMetric(beforeMetrics.recall, afterMetrics.recall);
|
|
604
|
+
const falsePositiveRate = compareMetric(beforeMetrics.falsePositiveRate, afterMetrics.falsePositiveRate);
|
|
605
|
+
return {
|
|
606
|
+
id: beforeTarget.id,
|
|
607
|
+
regressed: metricRegressed(precision, recall, falsePositiveRate),
|
|
608
|
+
counts: subtractCounts(afterTarget.counts, beforeTarget.counts),
|
|
609
|
+
precision,
|
|
610
|
+
recall,
|
|
611
|
+
falsePositiveRate
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
const changedTargets = targets.filter((target) => countKeys.some((key) => target.counts[key] !== 0)).length;
|
|
615
|
+
const beforeMetrics = calculateRawMetrics(before.summary);
|
|
616
|
+
const afterMetrics = calculateRawMetrics(after.summary);
|
|
617
|
+
const precision = compareMetric(beforeMetrics.precision, afterMetrics.precision);
|
|
618
|
+
const recall = compareMetric(beforeMetrics.recall, afterMetrics.recall);
|
|
619
|
+
const falsePositiveRate = compareMetric(beforeMetrics.falsePositiveRate, afterMetrics.falsePositiveRate);
|
|
620
|
+
const regression = metricRegressed(precision, recall, falsePositiveRate);
|
|
621
|
+
const failOnRegression = options.failOnRegression === true;
|
|
622
|
+
const failed = failOnRegression && regression;
|
|
623
|
+
return {
|
|
624
|
+
schemaVersion: "1.0.0",
|
|
625
|
+
kind: "doctor.validation.corpus.metrics.diff",
|
|
626
|
+
generatedAt: new Date().toISOString(),
|
|
627
|
+
version: packageVersion,
|
|
628
|
+
corpusDigest: before.corpusDigest,
|
|
629
|
+
status: failed ? "fail" : "pass",
|
|
630
|
+
exitCode: failed ? 1 : 0,
|
|
631
|
+
failOnRegression,
|
|
632
|
+
summary: {
|
|
633
|
+
comparable: true,
|
|
634
|
+
regression,
|
|
635
|
+
changedTargets,
|
|
636
|
+
precisionBefore: precision.before,
|
|
637
|
+
precisionAfter: precision.after,
|
|
638
|
+
precisionDelta: precision.delta,
|
|
639
|
+
recallBefore: recall.before,
|
|
640
|
+
recallAfter: recall.after,
|
|
641
|
+
recallDelta: recall.delta,
|
|
642
|
+
falsePositiveRateBefore: falsePositiveRate.before,
|
|
643
|
+
falsePositiveRateAfter: falsePositiveRate.after,
|
|
644
|
+
falsePositiveRateDelta: falsePositiveRate.delta,
|
|
645
|
+
counts: subtractCounts(after.summary, before.summary)
|
|
646
|
+
},
|
|
647
|
+
before: { version: before.version, targetCount: before.summary.targetCount },
|
|
648
|
+
after: { version: after.version, targetCount: after.summary.targetCount },
|
|
649
|
+
targets
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function formatDelta(value) {
|
|
653
|
+
if (value === null)
|
|
654
|
+
return "N/A";
|
|
655
|
+
const sign = value > 0 ? "+" : "";
|
|
656
|
+
return `${sign}${(value * 100).toFixed(2)} pp`;
|
|
657
|
+
}
|
|
658
|
+
export function renderCorpusQualityMetricsDiffJson(report) {
|
|
659
|
+
return JSON.stringify(report, null, 2);
|
|
660
|
+
}
|
|
661
|
+
export function renderCorpusQualityMetricsDiffText(report, options = {}) {
|
|
662
|
+
const lines = [
|
|
663
|
+
"Doctor Corpus Metrics Diff",
|
|
664
|
+
"==========================",
|
|
665
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
666
|
+
`Before version: ${report.before.version}`,
|
|
667
|
+
`After version: ${report.after.version}`,
|
|
668
|
+
`Regression: ${report.summary.regression ? "yes" : "no"}`,
|
|
669
|
+
`Precision delta: ${formatDelta(report.summary.precisionDelta)}`,
|
|
670
|
+
`Recall delta: ${formatDelta(report.summary.recallDelta)}`,
|
|
671
|
+
`False-positive rate delta: ${formatDelta(report.summary.falsePositiveRateDelta)}`,
|
|
672
|
+
`Changed targets: ${report.summary.changedTargets}`
|
|
673
|
+
];
|
|
674
|
+
if (options.outputPath)
|
|
675
|
+
lines.push(`Output: ${options.outputPath}`);
|
|
676
|
+
lines.push("", "Target Changes", "--------------");
|
|
677
|
+
const changed = report.targets.filter((target) => countKeys.some((key) => target.counts[key] !== 0));
|
|
678
|
+
if (changed.length === 0) {
|
|
679
|
+
lines.push("None.");
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
for (const target of changed) {
|
|
683
|
+
lines.push(`${target.id}: ${target.regressed ? "REGRESSED" : "CHANGED"} ` +
|
|
684
|
+
`(precision ${formatDelta(target.precision.delta)}, recall ${formatDelta(target.recall.delta)}, ` +
|
|
685
|
+
`false-positive rate ${formatDelta(target.falsePositiveRate.delta)})`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return lines.join("\n");
|
|
689
|
+
}
|
|
690
|
+
export function renderCorpusQualityMetricsDiffMarkdown(report) {
|
|
691
|
+
const lines = [
|
|
692
|
+
"# Doctor Corpus Metrics Diff",
|
|
693
|
+
"",
|
|
694
|
+
`- Status: ${report.status.toUpperCase()}`,
|
|
695
|
+
`- Before version: ${report.before.version}`,
|
|
696
|
+
`- After version: ${report.after.version}`,
|
|
697
|
+
`- Regression: ${report.summary.regression ? "yes" : "no"}`,
|
|
698
|
+
"",
|
|
699
|
+
"| Metric | Before | After | Delta |",
|
|
700
|
+
"| --- | ---: | ---: | ---: |",
|
|
701
|
+
`| Precision | ${formatMetric(report.summary.precisionBefore)} | ${formatMetric(report.summary.precisionAfter)} | ${formatDelta(report.summary.precisionDelta)} |`,
|
|
702
|
+
`| Recall | ${formatMetric(report.summary.recallBefore)} | ${formatMetric(report.summary.recallAfter)} | ${formatDelta(report.summary.recallDelta)} |`,
|
|
703
|
+
`| False-positive rate | ${formatMetric(report.summary.falsePositiveRateBefore)} | ${formatMetric(report.summary.falsePositiveRateAfter)} | ${formatDelta(report.summary.falsePositiveRateDelta)} |`,
|
|
704
|
+
"",
|
|
705
|
+
"## Target Changes",
|
|
706
|
+
"",
|
|
707
|
+
"| Target | Result | Precision | Recall | False-positive rate |",
|
|
708
|
+
"| --- | --- | ---: | ---: | ---: |"
|
|
709
|
+
];
|
|
710
|
+
const changed = report.targets.filter((target) => countKeys.some((key) => target.counts[key] !== 0));
|
|
711
|
+
if (changed.length === 0) {
|
|
712
|
+
lines.push("| None | unchanged | 0.00 pp | 0.00 pp | 0.00 pp |");
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
for (const target of changed) {
|
|
716
|
+
lines.push(`| ${target.id} | ${target.regressed ? "regressed" : "changed"} | ` +
|
|
717
|
+
`${formatDelta(target.precision.delta)} | ${formatDelta(target.recall.delta)} | ` +
|
|
718
|
+
`${formatDelta(target.falsePositiveRate.delta)} |`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return lines.join("\n");
|
|
722
|
+
}
|
|
@@ -110,6 +110,7 @@ const publicSchemaDefinitions = [
|
|
|
110
110
|
"kind",
|
|
111
111
|
"generatedAt",
|
|
112
112
|
"version",
|
|
113
|
+
"corpusDigest",
|
|
113
114
|
"status",
|
|
114
115
|
"exitCode",
|
|
115
116
|
"summary",
|
|
@@ -118,6 +119,25 @@ const publicSchemaDefinitions = [
|
|
|
118
119
|
"targets"
|
|
119
120
|
]
|
|
120
121
|
},
|
|
122
|
+
{
|
|
123
|
+
id: "doctor.validation.corpus.metrics.diff.json",
|
|
124
|
+
command: "codex-plugin-doctor doctor corpus metrics diff --before <metrics.json> --after <metrics.json> --json",
|
|
125
|
+
outputKind: "doctor.validation.corpus.metrics.diff",
|
|
126
|
+
required: [
|
|
127
|
+
"schemaVersion",
|
|
128
|
+
"kind",
|
|
129
|
+
"generatedAt",
|
|
130
|
+
"version",
|
|
131
|
+
"corpusDigest",
|
|
132
|
+
"status",
|
|
133
|
+
"exitCode",
|
|
134
|
+
"failOnRegression",
|
|
135
|
+
"summary",
|
|
136
|
+
"before",
|
|
137
|
+
"after",
|
|
138
|
+
"targets"
|
|
139
|
+
]
|
|
140
|
+
},
|
|
121
141
|
{
|
|
122
142
|
id: "doctor.recommendations.json",
|
|
123
143
|
command: "codex-plugin-doctor doctor recommend <path> --json",
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRe
|
|
|
11
11
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson, type DoctorExportBundle } from "./core/doctor-export-bundle.js";
|
|
12
12
|
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation, type DoctorAttestation, type DoctorAttestationVerificationCheck, type DoctorAttestationVerificationReport, type Digest, type PackageFingerprint } from "./core/attestation.js";
|
|
13
13
|
export { buildExternalValidationCorpusReport, ExternalCorpusManifestError, loadExternalCorpusManifest, type BuildExternalValidationCorpusOptions, type ExternalCorpusCaseResult, type ExternalCorpusClassificationCounts, type ExternalCorpusFindingReview, type ExternalCorpusManifest, type ExternalCorpusMode, type ExternalCorpusProfile, type ExternalCorpusSourceType, type ExternalValidationCorpusReport, type FindingReviewClassification } from "./core/external-validation-corpus.js";
|
|
14
|
-
export { buildCorpusQualityMetricsReport, CorpusMetricsManifestError, loadCorpusMetricsManifest, reconcileCorpusMetricFindings, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText, type BuildCorpusMetricsOptions, type CorpusMetricCounts, type CorpusMetricFinding, type CorpusMetricFindingReview, type CorpusMetricReconciliation, type CorpusMetricsManifest, type CorpusMetricSource, type CorpusMetricThresholdCheck, type CorpusMetricThresholds, type CorpusMetricValues, type CorpusQualityMetricsReport, type CorpusQualityMetricsTargetResult } from "./core/corpus-quality-metrics.js";
|
|
14
|
+
export { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport, CorpusMetricsDiffError, CorpusMetricsManifestError, loadCorpusMetricsManifest, reconcileCorpusMetricFindings, renderCorpusQualityMetricsDiffJson, renderCorpusQualityMetricsDiffMarkdown, renderCorpusQualityMetricsDiffText, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText, type BuildCorpusMetricsDiffOptions, type BuildCorpusMetricsOptions, type CorpusMetricCounts, type CorpusMetricCountDeltas, type CorpusMetricFinding, type CorpusMetricFindingReview, type CorpusMetricReconciliation, type CorpusMetricsManifest, type CorpusMetricSource, type CorpusMetricThresholdCheck, type CorpusMetricThresholds, type CorpusMetricValues, type CorpusMetricValueComparison, type CorpusQualityMetricsDiffReport, type CorpusQualityMetricsDiffTarget, type CorpusQualityMetricsReport, type CorpusQualityMetricsTargetResult } from "./core/corpus-quality-metrics.js";
|
|
15
15
|
export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson, type DoctorOutputContract, type OutputContractRule, type OutputContractSchema } from "./core/output-contract.js";
|
|
16
16
|
export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport, type BuildDoctorValidationCorpusOptions, type DoctorValidationCorpusReport, type ValidationCorpusCaseDefinition, type ValidationCorpusCaseResult } from "./core/validation-corpus.js";
|
|
17
17
|
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRe
|
|
|
11
11
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
12
12
|
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation } from "./core/attestation.js";
|
|
13
13
|
export { buildExternalValidationCorpusReport, ExternalCorpusManifestError, loadExternalCorpusManifest } from "./core/external-validation-corpus.js";
|
|
14
|
-
export { buildCorpusQualityMetricsReport, CorpusMetricsManifestError, loadCorpusMetricsManifest, reconcileCorpusMetricFindings, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText } from "./core/corpus-quality-metrics.js";
|
|
14
|
+
export { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport, CorpusMetricsDiffError, CorpusMetricsManifestError, loadCorpusMetricsManifest, reconcileCorpusMetricFindings, renderCorpusQualityMetricsDiffJson, renderCorpusQualityMetricsDiffMarkdown, renderCorpusQualityMetricsDiffText, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText } from "./core/corpus-quality-metrics.js";
|
|
15
15
|
export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
|
|
16
16
|
export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
|
|
17
17
|
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
|
package/dist/run-cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCorpusQualityMetricsReport } from "./core/corpus-quality-metrics.js";
|
|
1
|
+
import { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport } from "./core/corpus-quality-metrics.js";
|
|
2
2
|
import { runCheck } from "./index.js";
|
|
3
3
|
import { writeRawDoctorConfig } from "./index.js";
|
|
4
4
|
export interface CliIo {
|
|
@@ -19,6 +19,7 @@ export interface RunCliOptions {
|
|
|
19
19
|
now?: () => Date;
|
|
20
20
|
releaseAssetUploadImpl?: (args: string[]) => Promise<void>;
|
|
21
21
|
resolveLatestVersion?: () => Promise<string>;
|
|
22
|
+
buildCorpusMetricsDiffReportImpl?: typeof buildCorpusQualityMetricsDiffReport;
|
|
22
23
|
buildCorpusMetricsReportImpl?: typeof buildCorpusQualityMetricsReport;
|
|
23
24
|
}
|
|
24
25
|
export declare function runCli(args: string[], io?: CliIo, options?: RunCliOptions): Promise<number>;
|
package/dist/run-cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
|
|
|
21
21
|
import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
|
|
22
22
|
import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
|
|
23
23
|
import { buildExternalValidationCorpusReport, ExternalCorpusManifestError } from "./core/external-validation-corpus.js";
|
|
24
|
-
import { buildCorpusQualityMetricsReport, CorpusMetricsManifestError, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText } from "./core/corpus-quality-metrics.js";
|
|
24
|
+
import { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport, CorpusMetricsDiffError, CorpusMetricsManifestError, renderCorpusQualityMetricsDiffJson, renderCorpusQualityMetricsDiffMarkdown, renderCorpusQualityMetricsDiffText, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText } from "./core/corpus-quality-metrics.js";
|
|
25
25
|
import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
26
26
|
import { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
|
|
27
27
|
import { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson } from "./core/runtime-policy.js";
|
|
@@ -101,6 +101,7 @@ function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
|
|
|
101
101
|
}
|
|
102
102
|
function printUsage(io) {
|
|
103
103
|
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--runtime --sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--runtime --sandbox docker] [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--runtime --sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime --sandbox docker]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
104
|
+
io.writeStderr("Corpus quality regression: codex-plugin-doctor doctor corpus metrics diff --before <metrics.json> --after <metrics.json> [--fail-on-regression] [--json|--markdown] [--output <path>]");
|
|
104
105
|
io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
|
|
105
106
|
io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
|
|
106
107
|
}
|
|
@@ -1419,6 +1420,65 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1419
1420
|
if (maybePath === "corpus") {
|
|
1420
1421
|
if (remainingArgs[0] === "metrics") {
|
|
1421
1422
|
const metricsArgs = remainingArgs.slice(1);
|
|
1423
|
+
if (metricsArgs[0] === "diff") {
|
|
1424
|
+
const diffArgs = metricsArgs.slice(1);
|
|
1425
|
+
const jsonOutput = diffArgs.includes("--json");
|
|
1426
|
+
const markdownOutput = diffArgs.includes("--markdown");
|
|
1427
|
+
const failOnRegression = diffArgs.includes("--fail-on-regression");
|
|
1428
|
+
const valueOptions = new Map();
|
|
1429
|
+
const valueFlags = new Set(["--before", "--after", "--output"]);
|
|
1430
|
+
const booleanFlags = new Set(["--json", "--markdown", "--fail-on-regression"]);
|
|
1431
|
+
for (let index = 0; index < diffArgs.length; index += 1) {
|
|
1432
|
+
const argument = diffArgs[index];
|
|
1433
|
+
if (booleanFlags.has(argument))
|
|
1434
|
+
continue;
|
|
1435
|
+
if (!valueFlags.has(argument)) {
|
|
1436
|
+
io.writeStderr(`Unknown corpus metrics diff argument: ${argument}.`);
|
|
1437
|
+
return 2;
|
|
1438
|
+
}
|
|
1439
|
+
const value = diffArgs[index + 1];
|
|
1440
|
+
if (!value || value.startsWith("--")) {
|
|
1441
|
+
io.writeStderr(`Missing path after ${argument}.`);
|
|
1442
|
+
return 2;
|
|
1443
|
+
}
|
|
1444
|
+
valueOptions.set(argument, value);
|
|
1445
|
+
index += 1;
|
|
1446
|
+
}
|
|
1447
|
+
const beforePath = valueOptions.get("--before");
|
|
1448
|
+
const afterPath = valueOptions.get("--after");
|
|
1449
|
+
const outputPath = valueOptions.get("--output") ?? null;
|
|
1450
|
+
if (!beforePath || !afterPath) {
|
|
1451
|
+
io.writeStderr("Corpus metrics diff requires --before <metrics.json> and --after <metrics.json>.");
|
|
1452
|
+
return 2;
|
|
1453
|
+
}
|
|
1454
|
+
if (jsonOutput && markdownOutput) {
|
|
1455
|
+
io.writeStderr("Use either --json or --markdown, not both.");
|
|
1456
|
+
return 2;
|
|
1457
|
+
}
|
|
1458
|
+
let report;
|
|
1459
|
+
try {
|
|
1460
|
+
const builder = options.buildCorpusMetricsDiffReportImpl ?? buildCorpusQualityMetricsDiffReport;
|
|
1461
|
+
report = await builder(beforePath, afterPath, { failOnRegression });
|
|
1462
|
+
}
|
|
1463
|
+
catch (error) {
|
|
1464
|
+
if (error instanceof CorpusMetricsDiffError) {
|
|
1465
|
+
io.writeStderr(error.message);
|
|
1466
|
+
return 2;
|
|
1467
|
+
}
|
|
1468
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1469
|
+
io.writeStderr(`Corpus metrics diff failed: ${message}`);
|
|
1470
|
+
return 2;
|
|
1471
|
+
}
|
|
1472
|
+
const rendered = jsonOutput
|
|
1473
|
+
? renderCorpusQualityMetricsDiffJson(report)
|
|
1474
|
+
: markdownOutput
|
|
1475
|
+
? renderCorpusQualityMetricsDiffMarkdown(report)
|
|
1476
|
+
: renderCorpusQualityMetricsDiffText(report, { outputPath });
|
|
1477
|
+
if (outputPath)
|
|
1478
|
+
await writeFile(outputPath, rendered, "utf8");
|
|
1479
|
+
io.writeStdout(rendered);
|
|
1480
|
+
return report.exitCode;
|
|
1481
|
+
}
|
|
1422
1482
|
const jsonOutput = metricsArgs.includes("--json");
|
|
1423
1483
|
const markdownOutput = metricsArgs.includes("--markdown");
|
|
1424
1484
|
const valueOptions = new Map();
|
package/package.json
CHANGED