codex-plugin-doctor 0.13.0 → 0.15.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.
@@ -0,0 +1,173 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { gunzip } from "node:zlib";
7
+ import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const gunzipAsync = promisify(gunzip);
10
+ function npmCommand() {
11
+ return "npm";
12
+ }
13
+ function isPathWithinRoot(rootPath, candidatePath) {
14
+ const relativePath = path.relative(rootPath, candidatePath);
15
+ return (relativePath === "" ||
16
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
17
+ }
18
+ function readTarString(buffer, start, length) {
19
+ const slice = buffer.subarray(start, start + length);
20
+ const end = slice.indexOf(0);
21
+ const finalSlice = end === -1 ? slice : slice.subarray(0, end);
22
+ return finalSlice.toString("utf8").trim();
23
+ }
24
+ function readTarSize(buffer) {
25
+ const rawSize = readTarString(buffer, 124, 12).replace(/\0/g, "").trim();
26
+ const parsedSize = Number.parseInt(rawSize, 8);
27
+ return Number.isFinite(parsedSize) ? parsedSize : 0;
28
+ }
29
+ function isEmptyHeader(header) {
30
+ return header.every((byte) => byte === 0);
31
+ }
32
+ async function extractTarGz(tarballPath, destinationPath) {
33
+ const tarBuffer = await gunzipAsync(await readFile(tarballPath));
34
+ let offset = 0;
35
+ await mkdir(destinationPath, { recursive: true });
36
+ while (offset + 512 <= tarBuffer.length) {
37
+ const header = tarBuffer.subarray(offset, offset + 512);
38
+ if (isEmptyHeader(header)) {
39
+ break;
40
+ }
41
+ const name = readTarString(header, 0, 100);
42
+ const prefix = readTarString(header, 345, 155);
43
+ const entryName = prefix ? `${prefix}/${name}` : name;
44
+ const size = readTarSize(header);
45
+ const typeFlag = readTarString(header, 156, 1);
46
+ const fileStart = offset + 512;
47
+ const fileEnd = fileStart + size;
48
+ const targetPath = path.resolve(destinationPath, entryName);
49
+ if (!isPathWithinRoot(destinationPath, targetPath)) {
50
+ throw new Error(`Refusing to extract tar entry outside destination: ${entryName}`);
51
+ }
52
+ if (typeFlag === "5") {
53
+ await mkdir(targetPath, { recursive: true });
54
+ }
55
+ else if (typeFlag === "" || typeFlag === "0") {
56
+ await mkdir(path.dirname(targetPath), { recursive: true });
57
+ await writeFile(targetPath, tarBuffer.subarray(fileStart, fileEnd));
58
+ }
59
+ offset = fileStart + Math.ceil(size / 512) * 512;
60
+ }
61
+ }
62
+ async function directoryExists(targetPath) {
63
+ try {
64
+ const details = await stat(targetPath);
65
+ return details.isDirectory();
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ async function resolvePackageSpecForPack(packageSpec) {
72
+ const candidatePath = path.resolve(packageSpec);
73
+ try {
74
+ await stat(candidatePath);
75
+ return candidatePath;
76
+ }
77
+ catch {
78
+ return packageSpec;
79
+ }
80
+ }
81
+ async function packNpmPackage(packageSpec, destinationPath) {
82
+ const resolvedPackageSpec = await resolvePackageSpecForPack(packageSpec);
83
+ const { stdout } = await execFileAsync(npmCommand(), [
84
+ "pack",
85
+ resolvedPackageSpec,
86
+ "--json",
87
+ "--ignore-scripts",
88
+ "--pack-destination",
89
+ destinationPath
90
+ ], {
91
+ cwd: destinationPath,
92
+ maxBuffer: 10 * 1024 * 1024,
93
+ shell: process.platform === "win32"
94
+ });
95
+ const packEntries = JSON.parse(stdout);
96
+ const metadata = packEntries[0];
97
+ if (!metadata?.filename) {
98
+ throw new Error(`npm pack did not return a tarball for ${packageSpec}`);
99
+ }
100
+ return {
101
+ tarballPath: path.isAbsolute(metadata.filename)
102
+ ? metadata.filename
103
+ : path.join(destinationPath, metadata.filename),
104
+ metadata
105
+ };
106
+ }
107
+ export async function buildDoctorNpmPackageReport(packageSpec, options = {}) {
108
+ const workspacePath = await mkdtemp(path.join(os.tmpdir(), "codex-plugin-doctor-npm-"));
109
+ try {
110
+ const { tarballPath, metadata } = await packNpmPackage(packageSpec, workspacePath);
111
+ const extractPath = path.join(workspacePath, "extract");
112
+ await extractTarGz(tarballPath, extractPath);
113
+ const packageRoot = await directoryExists(path.join(extractPath, "package"))
114
+ ? path.join(extractPath, "package")
115
+ : extractPath;
116
+ const analysis = await buildPackageAnalysis(packageRoot, {
117
+ environment: options.environment
118
+ });
119
+ const recommendations = buildDoctorRecommendationsFromAnalysis(analysis);
120
+ return {
121
+ schemaVersion: "1.0.0",
122
+ generatedAt: analysis.generatedAt,
123
+ kind: "doctor.npm",
124
+ packageSpec,
125
+ package: {
126
+ name: typeof metadata.name === "string" ? metadata.name : null,
127
+ version: typeof metadata.version === "string" ? metadata.version : null,
128
+ fileCount: Array.isArray(metadata.files) ? metadata.files.length : null
129
+ },
130
+ summary: {
131
+ status: recommendations.status,
132
+ exitCode: recommendations.exitCode,
133
+ safeToInstall: recommendations.status === "pass"
134
+ },
135
+ validation: analysis.validationJson,
136
+ security: analysis.security,
137
+ trust: analysis.trust,
138
+ recommendations
139
+ };
140
+ }
141
+ finally {
142
+ await rm(workspacePath, { recursive: true, force: true });
143
+ }
144
+ }
145
+ export function renderDoctorNpmPackageReportJson(report) {
146
+ return JSON.stringify(report, null, 2);
147
+ }
148
+ export function renderDoctorNpmPackageReport(report, options = {}) {
149
+ const packageLabel = report.package.name
150
+ ? `${report.package.name}${report.package.version ? `@${report.package.version}` : ""}`
151
+ : report.packageSpec;
152
+ const lines = [
153
+ "Doctor npm Preinstall Scan",
154
+ "==========================",
155
+ `Package: ${packageLabel}`,
156
+ `Spec: ${report.packageSpec}`,
157
+ `Status: ${report.summary.status.toUpperCase()}`,
158
+ `Safe to install: ${report.summary.safeToInstall ? "yes" : "no"}`,
159
+ `Security: ${report.security.status.toUpperCase()} (${report.security.score}/100)`,
160
+ `Trust: ${report.trust.status.toUpperCase()} (${report.trust.score}/100)`,
161
+ `Actions: ${report.recommendations.actions.length}`
162
+ ];
163
+ if (options.outputPath) {
164
+ lines.push(`Output: ${options.outputPath}`);
165
+ }
166
+ if (report.recommendations.actions.length > 0) {
167
+ lines.push("", "Top Actions", "-----------");
168
+ for (const action of report.recommendations.actions.slice(0, 5)) {
169
+ lines.push(`[${action.priority.toUpperCase()}] ${action.title}`);
170
+ }
171
+ }
172
+ return lines.join("\n");
173
+ }
@@ -0,0 +1,28 @@
1
+ import { type CompatibilityEnvironment, type CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
2
+ import type { CheckResult, JsonReport } from "../domain/types.js";
3
+ import type { DoctorRecommendationsReport } from "./doctor-recommendations.js";
4
+ import type { DoctorExportBundle } from "./doctor-export-bundle.js";
5
+ import { type SecurityAudit } from "../security/security-audit.js";
6
+ import { type TrustScoreReport } from "../security/trust-score.js";
7
+ export interface PackageAnalysis {
8
+ generatedAt: string;
9
+ targetPath: string;
10
+ validation: CheckResult;
11
+ validationJson: JsonReport;
12
+ security: SecurityAudit;
13
+ compatibility: CompatibilityMatrix;
14
+ trust: TrustScoreReport;
15
+ }
16
+ export type PackageAnalysisStage = "validation" | "doctorConfig" | "security" | "compatibility" | "trust";
17
+ export interface PackageAnalysisTiming {
18
+ stage: PackageAnalysisStage;
19
+ durationMs: number;
20
+ }
21
+ export interface PackageAnalysisOptions {
22
+ environment?: CompatibilityEnvironment;
23
+ recordTiming?: (timing: PackageAnalysisTiming) => void;
24
+ runCheck?: (targetPath: string) => Promise<CheckResult>;
25
+ }
26
+ export declare function buildPackageAnalysis(targetPath: string, options?: PackageAnalysisOptions): Promise<PackageAnalysis>;
27
+ export declare function buildDoctorRecommendationsFromAnalysis(analysis: PackageAnalysis): DoctorRecommendationsReport;
28
+ export declare function buildDoctorExportBundleFromAnalysis(analysis: PackageAnalysis, recommendations?: DoctorRecommendationsReport): DoctorExportBundle;
@@ -0,0 +1,180 @@
1
+ import path from "node:path";
2
+ import { performance } from "node:perf_hooks";
3
+ import { buildCompatibilityMatrix, matrixExitCode } from "../compatibility/compatibility-matrix.js";
4
+ import { applyDoctorConfig, loadDoctorConfig } from "./doctor-config.js";
5
+ import { buildJsonReport } from "../reporting/render-json-report.js";
6
+ import { buildSecurityAudit } from "../security/security-audit.js";
7
+ import { buildTrustScore } from "../security/trust-score.js";
8
+ import { validatePlugin } from "./validate-plugin.js";
9
+ import { packageVersion } from "../version.js";
10
+ const priorityRank = {
11
+ blocker: 0,
12
+ high: 1,
13
+ medium: 2,
14
+ info: 3
15
+ };
16
+ function countActions(actions) {
17
+ return {
18
+ blocker: actions.filter((action) => action.priority === "blocker").length,
19
+ high: actions.filter((action) => action.priority === "high").length,
20
+ medium: actions.filter((action) => action.priority === "medium").length,
21
+ info: actions.filter((action) => action.priority === "info").length
22
+ };
23
+ }
24
+ function priorityForFinding(finding) {
25
+ if (finding.severity === "fail") {
26
+ return "blocker";
27
+ }
28
+ return finding.id.startsWith("plugin.security.") ? "high" : "medium";
29
+ }
30
+ function categoryForFinding(finding) {
31
+ return finding.id.startsWith("plugin.security.") ? "security" : "validation";
32
+ }
33
+ function commandForCategory(category, targetPath) {
34
+ if (category === "security") {
35
+ return `codex-plugin-doctor security ${targetPath} --scorecard`;
36
+ }
37
+ if (category === "compatibility") {
38
+ return `codex-plugin-doctor compat ${targetPath} --all --scorecard`;
39
+ }
40
+ return `codex-plugin-doctor check ${targetPath} --explain`;
41
+ }
42
+ function actionFromFinding(finding, targetPath) {
43
+ const category = categoryForFinding(finding);
44
+ return {
45
+ priority: priorityForFinding(finding),
46
+ category,
47
+ findingId: finding.id,
48
+ title: finding.message,
49
+ reason: finding.impact,
50
+ nextCommand: commandForCategory(category, targetPath)
51
+ };
52
+ }
53
+ function dedupeActions(actions) {
54
+ const seen = new Set();
55
+ return actions.filter((action) => {
56
+ const key = `${action.category}\n${action.findingId ?? action.title}\n${action.reason}`;
57
+ if (seen.has(key)) {
58
+ return false;
59
+ }
60
+ seen.add(key);
61
+ return true;
62
+ });
63
+ }
64
+ function actionsFromCompatibility(matrix, targetPath) {
65
+ return matrix.results
66
+ .filter((result) => result.status === "fail")
67
+ .map((result) => ({
68
+ priority: "high",
69
+ category: "compatibility",
70
+ title: `${result.client} compatibility failed.`,
71
+ reason: result.summary,
72
+ nextCommand: commandForCategory("compatibility", targetPath)
73
+ }));
74
+ }
75
+ function sortActions(actions) {
76
+ return [...actions].sort((left, right) => {
77
+ const priorityDelta = priorityRank[left.priority] - priorityRank[right.priority];
78
+ if (priorityDelta !== 0) {
79
+ return priorityDelta;
80
+ }
81
+ return left.category.localeCompare(right.category);
82
+ });
83
+ }
84
+ async function measureStage(stage, operation, recordTiming) {
85
+ const startedAt = performance.now();
86
+ try {
87
+ return await operation();
88
+ }
89
+ finally {
90
+ recordTiming?.({
91
+ stage,
92
+ durationMs: performance.now() - startedAt
93
+ });
94
+ }
95
+ }
96
+ export async function buildPackageAnalysis(targetPath, options = {}) {
97
+ const rootPath = path.resolve(targetPath);
98
+ const runCheck = options.runCheck ?? validatePlugin;
99
+ const [rawValidation, doctorConfig, security, compatibility] = await Promise.all([
100
+ measureStage("validation", () => runCheck(rootPath), options.recordTiming),
101
+ measureStage("doctorConfig", () => loadDoctorConfig(rootPath), options.recordTiming),
102
+ measureStage("security", () => buildSecurityAudit(rootPath), options.recordTiming),
103
+ measureStage("compatibility", () => buildCompatibilityMatrix(rootPath, options.environment ?? {}), options.recordTiming)
104
+ ]);
105
+ const validation = applyDoctorConfig(rawValidation, doctorConfig);
106
+ const trust = await measureStage("trust", () => buildTrustScore(rootPath, { securityAudit: security }), options.recordTiming);
107
+ return {
108
+ generatedAt: new Date().toISOString(),
109
+ targetPath: rootPath,
110
+ validation,
111
+ validationJson: buildJsonReport(validation, { runtimeProbeEnabled: false }),
112
+ security,
113
+ compatibility,
114
+ trust
115
+ };
116
+ }
117
+ export function buildDoctorRecommendationsFromAnalysis(analysis) {
118
+ const actions = sortActions(dedupeActions([
119
+ ...analysis.validation.findings.map((finding) => actionFromFinding(finding, analysis.targetPath)),
120
+ ...analysis.security.findings.map((finding) => actionFromFinding(finding, analysis.targetPath)),
121
+ ...actionsFromCompatibility(analysis.compatibility, analysis.targetPath)
122
+ ]));
123
+ const finalActions = actions.length > 0
124
+ ? actions
125
+ : [
126
+ {
127
+ priority: "info",
128
+ category: "release",
129
+ title: "No blocker actions.",
130
+ reason: "The package has no validation, security, or compatibility blockers in this recommendation pass.",
131
+ nextCommand: `codex-plugin-doctor check ${analysis.targetPath} --profile publish`
132
+ }
133
+ ];
134
+ const status = finalActions.some((action) => action.priority === "blocker")
135
+ ? "fail"
136
+ : finalActions.some((action) => action.priority === "high" || action.priority === "medium")
137
+ ? "warn"
138
+ : "pass";
139
+ return {
140
+ schemaVersion: "1.0.0",
141
+ generatedAt: analysis.generatedAt,
142
+ targetPath: analysis.targetPath,
143
+ status,
144
+ exitCode: status === "fail" ? 1 : 0,
145
+ summary: {
146
+ actionCounts: countActions(finalActions)
147
+ },
148
+ validation: {
149
+ status: analysis.validation.status,
150
+ findingCount: analysis.validation.findings.length
151
+ },
152
+ security: {
153
+ status: analysis.security.status,
154
+ score: analysis.security.score,
155
+ findingCount: analysis.security.findings.length
156
+ },
157
+ compatibility: {
158
+ failedClients: matrixExitCode(analysis.compatibility) === 1
159
+ ? analysis.compatibility.results
160
+ .filter((result) => result.status === "fail")
161
+ .map((result) => result.client)
162
+ : []
163
+ },
164
+ actions: finalActions
165
+ };
166
+ }
167
+ export function buildDoctorExportBundleFromAnalysis(analysis, recommendations = buildDoctorRecommendationsFromAnalysis(analysis)) {
168
+ return {
169
+ schemaVersion: "1.0.0",
170
+ generatedAt: analysis.generatedAt,
171
+ kind: "doctor.export.bundle",
172
+ version: packageVersion,
173
+ targetPath: analysis.targetPath,
174
+ validation: analysis.validationJson,
175
+ security: analysis.security,
176
+ compatibility: analysis.compatibility,
177
+ recommendations,
178
+ trust: analysis.trust
179
+ };
180
+ }
@@ -0,0 +1,37 @@
1
+ import { type CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
2
+ import type { CheckResult } from "../domain/types.js";
3
+ import { type PackageAnalysisStage } from "./package-analysis.js";
4
+ export type DoctorPerformanceStageName = PackageAnalysisStage | "recommendations" | "total";
5
+ export interface DoctorPerformanceStage {
6
+ name: DoctorPerformanceStageName;
7
+ durationMs: number;
8
+ status?: "pass" | "warn" | "fail";
9
+ itemCount?: number;
10
+ }
11
+ export interface DoctorPerformanceReport {
12
+ schemaVersion: "1.0.0";
13
+ generatedAt: string;
14
+ kind: "doctor.perf";
15
+ targetPath: string;
16
+ status: "pass";
17
+ exitCode: 0;
18
+ summary: {
19
+ stageCount: number;
20
+ slowestStage: DoctorPerformanceStageName;
21
+ totalDurationMs: number;
22
+ validationStatus: "pass" | "warn" | "fail";
23
+ securityStatus: "pass" | "warn" | "fail";
24
+ trustScore: number;
25
+ compatibilityFailures: number;
26
+ };
27
+ stages: DoctorPerformanceStage[];
28
+ }
29
+ export interface BuildDoctorPerformanceReportOptions {
30
+ environment?: CompatibilityEnvironment;
31
+ runCheck?: (targetPath: string) => Promise<CheckResult>;
32
+ }
33
+ export declare function buildDoctorPerformanceReport(targetPath: string, options?: BuildDoctorPerformanceReportOptions): Promise<DoctorPerformanceReport>;
34
+ export declare function renderDoctorPerformanceReportJson(report: DoctorPerformanceReport): string;
35
+ export declare function renderDoctorPerformanceReport(report: DoctorPerformanceReport, options?: {
36
+ outputPath?: string | null;
37
+ }): string;
@@ -0,0 +1,141 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { matrixExitCode } from "../compatibility/compatibility-matrix.js";
3
+ import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
4
+ const stageOrder = [
5
+ "validation",
6
+ "doctorConfig",
7
+ "security",
8
+ "compatibility",
9
+ "trust",
10
+ "recommendations",
11
+ "total"
12
+ ];
13
+ function roundDuration(durationMs) {
14
+ return Number(durationMs.toFixed(3));
15
+ }
16
+ function compatibilityStatus(exitCode, warningCount) {
17
+ if (exitCode === 1) {
18
+ return "fail";
19
+ }
20
+ return warningCount > 0 ? "warn" : "pass";
21
+ }
22
+ function slowestStage(stages) {
23
+ return stages
24
+ .filter((stage) => stage.name !== "total")
25
+ .reduce((slowest, stage) => (stage.durationMs > slowest.durationMs ? stage : slowest), stages[0]).name;
26
+ }
27
+ export async function buildDoctorPerformanceReport(targetPath, options = {}) {
28
+ const timings = [];
29
+ const startedAt = performance.now();
30
+ const analysis = await buildPackageAnalysis(targetPath, {
31
+ environment: options.environment,
32
+ recordTiming: (timing) => timings.push(timing),
33
+ runCheck: options.runCheck
34
+ });
35
+ const recommendationsStartedAt = performance.now();
36
+ const recommendations = buildDoctorRecommendationsFromAnalysis(analysis);
37
+ const recommendationTiming = performance.now() - recommendationsStartedAt;
38
+ const totalDurationMs = performance.now() - startedAt;
39
+ const compatibilityFailures = analysis.compatibility.results
40
+ .filter((result) => result.status === "fail").length;
41
+ const compatibilityWarnings = analysis.compatibility.results
42
+ .filter((result) => result.status === "warn").length;
43
+ const timingByStage = new Map(timings.map((timing) => [timing.stage, timing.durationMs]));
44
+ const stages = stageOrder.map((stageName) => {
45
+ if (stageName === "validation") {
46
+ return {
47
+ name: stageName,
48
+ durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
49
+ status: analysis.validation.status,
50
+ itemCount: analysis.validation.findings.length
51
+ };
52
+ }
53
+ if (stageName === "doctorConfig") {
54
+ return {
55
+ name: stageName,
56
+ durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
57
+ status: "pass"
58
+ };
59
+ }
60
+ if (stageName === "security") {
61
+ return {
62
+ name: stageName,
63
+ durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
64
+ status: analysis.security.status,
65
+ itemCount: analysis.security.findings.length
66
+ };
67
+ }
68
+ if (stageName === "compatibility") {
69
+ return {
70
+ name: stageName,
71
+ durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
72
+ status: compatibilityStatus(matrixExitCode(analysis.compatibility), compatibilityWarnings),
73
+ itemCount: analysis.compatibility.results.length
74
+ };
75
+ }
76
+ if (stageName === "trust") {
77
+ return {
78
+ name: stageName,
79
+ durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
80
+ status: analysis.trust.status,
81
+ itemCount: analysis.trust.findings.length
82
+ };
83
+ }
84
+ if (stageName === "recommendations") {
85
+ return {
86
+ name: stageName,
87
+ durationMs: roundDuration(recommendationTiming),
88
+ status: recommendations.status,
89
+ itemCount: recommendations.actions.length
90
+ };
91
+ }
92
+ return {
93
+ name: "total",
94
+ durationMs: roundDuration(totalDurationMs)
95
+ };
96
+ });
97
+ return {
98
+ schemaVersion: "1.0.0",
99
+ generatedAt: analysis.generatedAt,
100
+ kind: "doctor.perf",
101
+ targetPath: analysis.targetPath,
102
+ status: "pass",
103
+ exitCode: 0,
104
+ summary: {
105
+ stageCount: stages.length,
106
+ slowestStage: slowestStage(stages),
107
+ totalDurationMs: roundDuration(totalDurationMs),
108
+ validationStatus: analysis.validation.status,
109
+ securityStatus: analysis.security.status,
110
+ trustScore: analysis.trust.score,
111
+ compatibilityFailures
112
+ },
113
+ stages
114
+ };
115
+ }
116
+ export function renderDoctorPerformanceReportJson(report) {
117
+ return JSON.stringify(report, null, 2);
118
+ }
119
+ export function renderDoctorPerformanceReport(report, options = {}) {
120
+ const lines = [
121
+ "Doctor Performance",
122
+ "==================",
123
+ `Target: ${report.targetPath}`,
124
+ `Status: ${report.status.toUpperCase()}`,
125
+ `Total: ${report.summary.totalDurationMs}ms`,
126
+ `Slowest: ${report.summary.slowestStage}`,
127
+ `Validation: ${report.summary.validationStatus.toUpperCase()}`,
128
+ `Security: ${report.summary.securityStatus.toUpperCase()}`,
129
+ `Trust: ${report.summary.trustScore}/100`
130
+ ];
131
+ if (options.outputPath) {
132
+ lines.push(`Output: ${options.outputPath}`);
133
+ }
134
+ lines.push("", "Stages", "------");
135
+ for (const stage of report.stages) {
136
+ const status = stage.status ? ` (${stage.status.toUpperCase()})` : "";
137
+ const count = stage.itemCount === undefined ? "" : `, items: ${stage.itemCount}`;
138
+ lines.push(`${stage.name}: ${stage.durationMs}ms${status}${count}`);
139
+ }
140
+ return lines.join("\n");
141
+ }
@@ -0,0 +1,33 @@
1
+ import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
2
+ import type { Finding } from "../domain/types.js";
3
+ export type RiskFindingCategory = "validation" | "security";
4
+ export interface RiskDiffFinding extends Finding {
5
+ category: RiskFindingCategory;
6
+ }
7
+ export interface DoctorRiskDiffReport {
8
+ schemaVersion: "1.0.0";
9
+ generatedAt: string;
10
+ kind: "doctor.risk.diff";
11
+ beforePath: string;
12
+ afterPath: string;
13
+ status: "pass" | "warn" | "fail";
14
+ exitCode: 0 | 1;
15
+ summary: {
16
+ riskIncreased: boolean;
17
+ newFindings: number;
18
+ resolvedFindings: number;
19
+ trustScoreBefore: number;
20
+ trustScoreAfter: number;
21
+ trustScoreDelta: number;
22
+ };
23
+ newFindings: RiskDiffFinding[];
24
+ resolvedFindings: RiskDiffFinding[];
25
+ }
26
+ export interface BuildDoctorRiskDiffReportOptions {
27
+ environment?: CompatibilityEnvironment;
28
+ }
29
+ export declare function buildDoctorRiskDiffReport(beforePath: string, afterPath: string, options?: BuildDoctorRiskDiffReportOptions): Promise<DoctorRiskDiffReport>;
30
+ export declare function renderDoctorRiskDiffReportJson(report: DoctorRiskDiffReport): string;
31
+ export declare function renderDoctorRiskDiffReport(report: DoctorRiskDiffReport, options?: {
32
+ outputPath?: string | null;
33
+ }): string;