@rainy-updates/cli 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/bin/cli.js +9 -467
- package/dist/bin/dispatch.d.ts +16 -0
- package/dist/bin/dispatch.js +150 -0
- package/dist/bin/help.d.ts +1 -0
- package/dist/bin/help.js +284 -0
- package/dist/commands/doctor/parser.js +6 -0
- package/dist/commands/doctor/runner.js +5 -2
- package/dist/core/analysis/options.d.ts +6 -0
- package/dist/core/analysis/options.js +69 -0
- package/dist/core/analysis/review-items.d.ts +4 -0
- package/dist/core/analysis/review-items.js +128 -0
- package/dist/core/analysis/run-silenced.d.ts +1 -0
- package/dist/core/analysis/run-silenced.js +14 -0
- package/dist/core/analysis-bundle.js +3 -211
- package/dist/core/doctor/findings.d.ts +2 -0
- package/dist/core/doctor/findings.js +166 -0
- package/dist/core/doctor/render.d.ts +3 -0
- package/dist/core/doctor/render.js +44 -0
- package/dist/core/doctor/result.d.ts +2 -0
- package/dist/core/doctor/result.js +55 -0
- package/dist/core/doctor/score.d.ts +5 -0
- package/dist/core/doctor/score.js +28 -0
- package/dist/core/review-model.d.ts +3 -3
- package/dist/core/review-model.js +4 -68
- package/dist/core/review-verdict.d.ts +2 -0
- package/dist/core/review-verdict.js +14 -0
- package/dist/core/summary.js +6 -0
- package/dist/output/format.js +7 -0
- package/dist/output/github.js +4 -0
- package/dist/output/sarif.js +4 -0
- package/dist/types/index.d.ts +28 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
export async function runSilenced(fn) {
|
|
3
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
4
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
5
|
+
process.stdout.write = (() => true);
|
|
6
|
+
process.stderr.write = (() => true);
|
|
7
|
+
try {
|
|
8
|
+
return await fn();
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
process.stdout.write = stdoutWrite;
|
|
12
|
+
process.stderr.write = stderrWrite;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { fetchChangelog } from "../commands/changelog/fetcher.js";
|
|
2
|
-
import { applyImpactScores } from "./impact.js";
|
|
3
|
-
import { applyRiskAssessments } from "../risk/index.js";
|
|
4
1
|
import { check } from "./check.js";
|
|
2
|
+
import { toAuditOptions, toHealthOptions, toLicenseOptions, toResolveOptions, toUnusedOptions, } from "./analysis/options.js";
|
|
3
|
+
import { buildReviewItems } from "./analysis/review-items.js";
|
|
4
|
+
import { runSilenced } from "./analysis/run-silenced.js";
|
|
5
5
|
export async function buildAnalysisBundle(options, config = {}) {
|
|
6
6
|
const baseCheckOptions = {
|
|
7
7
|
...options,
|
|
@@ -31,211 +31,3 @@ export async function buildAnalysisBundle(options, config = {}) {
|
|
|
31
31
|
.map((source) => source.source),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
async function buildReviewItems(updates, auditResult, resolveResult, healthResult, licenseResult, unusedResult, config) {
|
|
35
|
-
const advisoryPackages = new Set(auditResult.packages.map((pkg) => pkg.packageName));
|
|
36
|
-
const impactedUpdates = applyImpactScores(updates, {
|
|
37
|
-
advisoryPackages,
|
|
38
|
-
workspaceDependentCount: (name) => updates.filter((item) => item.name === name).length,
|
|
39
|
-
});
|
|
40
|
-
const healthByName = new Map(healthResult.metrics.map((metric) => [metric.name, metric]));
|
|
41
|
-
const advisoriesByName = new Map();
|
|
42
|
-
const conflictsByName = new Map();
|
|
43
|
-
const licenseByName = new Map(licenseResult.packages.map((pkg) => [pkg.name, pkg]));
|
|
44
|
-
const licenseViolationNames = new Set(licenseResult.violations.map((pkg) => pkg.name));
|
|
45
|
-
const unusedByName = new Map();
|
|
46
|
-
for (const advisory of auditResult.advisories) {
|
|
47
|
-
const list = advisoriesByName.get(advisory.packageName) ?? [];
|
|
48
|
-
list.push(advisory);
|
|
49
|
-
advisoriesByName.set(advisory.packageName, list);
|
|
50
|
-
}
|
|
51
|
-
for (const conflict of resolveResult.conflicts) {
|
|
52
|
-
const list = conflictsByName.get(conflict.requester) ?? [];
|
|
53
|
-
list.push(conflict);
|
|
54
|
-
conflictsByName.set(conflict.requester, list);
|
|
55
|
-
const peerList = conflictsByName.get(conflict.peer) ?? [];
|
|
56
|
-
peerList.push(conflict);
|
|
57
|
-
conflictsByName.set(conflict.peer, peerList);
|
|
58
|
-
}
|
|
59
|
-
for (const issue of [...unusedResult.unused, ...unusedResult.missing]) {
|
|
60
|
-
const list = unusedByName.get(issue.name) ?? [];
|
|
61
|
-
list.push(issue);
|
|
62
|
-
unusedByName.set(issue.name, list);
|
|
63
|
-
}
|
|
64
|
-
const enrichedUpdates = await maybeAttachReleaseNotes(impactedUpdates, Boolean(config.includeChangelog));
|
|
65
|
-
return applyRiskAssessments(enrichedUpdates.map((update) => enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName)), {
|
|
66
|
-
knownPackageNames: new Set(updates.map((item) => item.name)),
|
|
67
|
-
}).map((item) => ({
|
|
68
|
-
...item,
|
|
69
|
-
update: {
|
|
70
|
-
...item.update,
|
|
71
|
-
policyAction: derivePolicyAction(item),
|
|
72
|
-
decisionState: deriveDecisionState(item),
|
|
73
|
-
selectedByDefault: deriveDecisionState(item) !== "blocked",
|
|
74
|
-
blockedReason: deriveDecisionState(item) === "blocked"
|
|
75
|
-
? item.update.recommendedAction
|
|
76
|
-
: undefined,
|
|
77
|
-
monitorReason: item.update.healthStatus === "stale" ? "Package health should be monitored." : undefined,
|
|
78
|
-
},
|
|
79
|
-
}));
|
|
80
|
-
}
|
|
81
|
-
function enrichUpdate(update, advisoriesByName, conflictsByName, healthByName, licenseByName, licenseViolationNames, unusedByName) {
|
|
82
|
-
const advisories = advisoriesByName.get(update.name) ?? [];
|
|
83
|
-
const peerConflicts = conflictsByName.get(update.name) ?? [];
|
|
84
|
-
const health = healthByName.get(update.name);
|
|
85
|
-
const license = licenseByName.get(update.name);
|
|
86
|
-
const unusedIssues = unusedByName.get(update.name) ?? [];
|
|
87
|
-
return {
|
|
88
|
-
update: {
|
|
89
|
-
...update,
|
|
90
|
-
advisoryCount: advisories.length,
|
|
91
|
-
peerConflictSeverity: peerConflicts.some((item) => item.severity === "error")
|
|
92
|
-
? "error"
|
|
93
|
-
: peerConflicts.length > 0
|
|
94
|
-
? "warning"
|
|
95
|
-
: "none",
|
|
96
|
-
licenseStatus: licenseViolationNames.has(update.name)
|
|
97
|
-
? "denied"
|
|
98
|
-
: license
|
|
99
|
-
? "allowed"
|
|
100
|
-
: "review",
|
|
101
|
-
healthStatus: health?.flags[0] ?? "healthy",
|
|
102
|
-
},
|
|
103
|
-
advisories,
|
|
104
|
-
health,
|
|
105
|
-
peerConflicts,
|
|
106
|
-
license,
|
|
107
|
-
unusedIssues,
|
|
108
|
-
selected: true,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function derivePolicyAction(item) {
|
|
112
|
-
if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
|
|
113
|
-
return "block";
|
|
114
|
-
}
|
|
115
|
-
if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
|
|
116
|
-
return "review";
|
|
117
|
-
}
|
|
118
|
-
if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
|
|
119
|
-
return "monitor";
|
|
120
|
-
}
|
|
121
|
-
return "allow";
|
|
122
|
-
}
|
|
123
|
-
function deriveDecisionState(item) {
|
|
124
|
-
if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
|
|
125
|
-
return "blocked";
|
|
126
|
-
}
|
|
127
|
-
if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
|
|
128
|
-
return "actionable";
|
|
129
|
-
}
|
|
130
|
-
if (item.update.riskLevel === "high" || item.update.diffType === "major") {
|
|
131
|
-
return "review";
|
|
132
|
-
}
|
|
133
|
-
return "safe";
|
|
134
|
-
}
|
|
135
|
-
async function maybeAttachReleaseNotes(updates, includeChangelog) {
|
|
136
|
-
if (!includeChangelog || updates.length === 0) {
|
|
137
|
-
return updates;
|
|
138
|
-
}
|
|
139
|
-
const enriched = await Promise.all(updates.map(async (update) => ({
|
|
140
|
-
...update,
|
|
141
|
-
releaseNotesSummary: summarizeChangelog(await fetchChangelog(update.name, update.repository)),
|
|
142
|
-
})));
|
|
143
|
-
return enriched;
|
|
144
|
-
}
|
|
145
|
-
function summarizeChangelog(content) {
|
|
146
|
-
if (!content)
|
|
147
|
-
return undefined;
|
|
148
|
-
const lines = content
|
|
149
|
-
.split(/\r?\n/)
|
|
150
|
-
.map((line) => line.trim())
|
|
151
|
-
.filter(Boolean);
|
|
152
|
-
const title = lines.find((line) => line.startsWith("#"))?.replace(/^#+\s*/, "") ?? "Release notes";
|
|
153
|
-
const excerpt = lines.find((line) => !line.startsWith("#")) ?? "No summary available.";
|
|
154
|
-
return {
|
|
155
|
-
source: content.includes("# Release") ? "github-release" : "changelog-file",
|
|
156
|
-
title,
|
|
157
|
-
excerpt: excerpt.slice(0, 240),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
async function runSilenced(fn) {
|
|
161
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
162
|
-
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
163
|
-
process.stdout.write = (() => true);
|
|
164
|
-
process.stderr.write = (() => true);
|
|
165
|
-
try {
|
|
166
|
-
return await fn();
|
|
167
|
-
}
|
|
168
|
-
finally {
|
|
169
|
-
process.stdout.write = stdoutWrite;
|
|
170
|
-
process.stderr.write = stderrWrite;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
function toAuditOptions(options) {
|
|
174
|
-
return {
|
|
175
|
-
cwd: options.cwd,
|
|
176
|
-
workspace: options.workspace,
|
|
177
|
-
severity: undefined,
|
|
178
|
-
fix: false,
|
|
179
|
-
dryRun: true,
|
|
180
|
-
commit: false,
|
|
181
|
-
packageManager: "auto",
|
|
182
|
-
reportFormat: "json",
|
|
183
|
-
sourceMode: "auto",
|
|
184
|
-
jsonFile: undefined,
|
|
185
|
-
concurrency: options.concurrency,
|
|
186
|
-
registryTimeoutMs: options.registryTimeoutMs,
|
|
187
|
-
silent: true,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
function toResolveOptions(options) {
|
|
191
|
-
return {
|
|
192
|
-
cwd: options.cwd,
|
|
193
|
-
workspace: options.workspace,
|
|
194
|
-
afterUpdate: true,
|
|
195
|
-
safe: false,
|
|
196
|
-
jsonFile: undefined,
|
|
197
|
-
concurrency: options.concurrency,
|
|
198
|
-
registryTimeoutMs: options.registryTimeoutMs,
|
|
199
|
-
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
200
|
-
silent: true,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function toHealthOptions(options) {
|
|
204
|
-
return {
|
|
205
|
-
cwd: options.cwd,
|
|
206
|
-
workspace: options.workspace,
|
|
207
|
-
staleDays: 365,
|
|
208
|
-
includeDeprecated: true,
|
|
209
|
-
includeAlternatives: false,
|
|
210
|
-
reportFormat: "json",
|
|
211
|
-
jsonFile: undefined,
|
|
212
|
-
concurrency: options.concurrency,
|
|
213
|
-
registryTimeoutMs: options.registryTimeoutMs,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
function toLicenseOptions(options) {
|
|
217
|
-
return {
|
|
218
|
-
cwd: options.cwd,
|
|
219
|
-
workspace: options.workspace,
|
|
220
|
-
allow: undefined,
|
|
221
|
-
deny: undefined,
|
|
222
|
-
sbomFile: undefined,
|
|
223
|
-
jsonFile: undefined,
|
|
224
|
-
diffMode: false,
|
|
225
|
-
concurrency: options.concurrency,
|
|
226
|
-
registryTimeoutMs: options.registryTimeoutMs,
|
|
227
|
-
cacheTtlSeconds: options.cacheTtlSeconds,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
function toUnusedOptions(options) {
|
|
231
|
-
return {
|
|
232
|
-
cwd: options.cwd,
|
|
233
|
-
workspace: options.workspace,
|
|
234
|
-
srcDirs: ["src", "."],
|
|
235
|
-
includeDevDependencies: true,
|
|
236
|
-
fix: false,
|
|
237
|
-
dryRun: true,
|
|
238
|
-
jsonFile: undefined,
|
|
239
|
-
concurrency: options.concurrency,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function buildDoctorFindings(review) {
|
|
3
|
+
const findings = [];
|
|
4
|
+
for (const error of review.errors) {
|
|
5
|
+
findings.push({
|
|
6
|
+
id: `execution-error:${error}`,
|
|
7
|
+
code: "execution-error",
|
|
8
|
+
category: "Registry / Execution",
|
|
9
|
+
severity: "error",
|
|
10
|
+
scope: "project",
|
|
11
|
+
summary: error,
|
|
12
|
+
details: "Execution errors make the scan incomplete or require operator review.",
|
|
13
|
+
help: "Resolve execution and registry issues before treating the run as clean.",
|
|
14
|
+
recommendedAction: "Run `rup review --interactive` after fixing execution failures.",
|
|
15
|
+
evidence: [error],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
for (const degradedSource of review.summary.degradedSources ?? []) {
|
|
19
|
+
findings.push({
|
|
20
|
+
id: `degraded-source:${degradedSource}`,
|
|
21
|
+
code: "degraded-advisory-source",
|
|
22
|
+
category: "Registry / Execution",
|
|
23
|
+
severity: "warning",
|
|
24
|
+
scope: "project",
|
|
25
|
+
summary: `${degradedSource} returned degraded advisory coverage.`,
|
|
26
|
+
details: "Security findings may be partial while an advisory source is degraded.",
|
|
27
|
+
help: "Retry the scan or pin the advisory source when full coverage matters.",
|
|
28
|
+
recommendedAction: "Re-run `rup doctor` or `rup audit --source` once the degraded source recovers.",
|
|
29
|
+
evidence: [degradedSource],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const item of review.items) {
|
|
33
|
+
const workspace = path.basename(item.update.packagePath);
|
|
34
|
+
if (item.update.peerConflictSeverity === "error" || item.update.peerConflictSeverity === "warning") {
|
|
35
|
+
findings.push({
|
|
36
|
+
id: `peer-conflict:${item.update.name}:${workspace}`,
|
|
37
|
+
code: "peer-conflict",
|
|
38
|
+
category: "Compatibility",
|
|
39
|
+
severity: item.update.peerConflictSeverity === "error" ? "error" : "warning",
|
|
40
|
+
scope: "package",
|
|
41
|
+
packageName: item.update.name,
|
|
42
|
+
workspace,
|
|
43
|
+
summary: `${item.update.name} has ${item.update.peerConflictSeverity} peer conflicts after the proposed upgrade.`,
|
|
44
|
+
details: item.peerConflicts[0]?.suggestion,
|
|
45
|
+
help: "Inspect peer dependency requirements before applying the update.",
|
|
46
|
+
recommendedAction: item.update.recommendedAction ?? "Review peer requirements in `rup review --interactive`.",
|
|
47
|
+
evidence: item.peerConflicts.map((conflict) => `${conflict.requester} -> ${conflict.peer}@${conflict.requiredRange}`),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (item.update.licenseStatus === "denied") {
|
|
51
|
+
findings.push({
|
|
52
|
+
id: `license-denied:${item.update.name}:${workspace}`,
|
|
53
|
+
code: "license-policy-denied",
|
|
54
|
+
category: "Licensing",
|
|
55
|
+
severity: "error",
|
|
56
|
+
scope: "package",
|
|
57
|
+
packageName: item.update.name,
|
|
58
|
+
workspace,
|
|
59
|
+
summary: `${item.update.name} violates the current license policy.`,
|
|
60
|
+
details: item.license?.license,
|
|
61
|
+
help: "Keep denied licenses out of the approved update set.",
|
|
62
|
+
recommendedAction: item.update.recommendedAction ?? "Block this package in `rup review --interactive`.",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if ((item.update.advisoryCount ?? 0) > 0) {
|
|
66
|
+
findings.push({
|
|
67
|
+
id: `security-advisory:${item.update.name}:${workspace}`,
|
|
68
|
+
code: "security-advisory",
|
|
69
|
+
category: "Security",
|
|
70
|
+
severity: item.update.riskLevel === "critical" ? "error" : "warning",
|
|
71
|
+
scope: "package",
|
|
72
|
+
packageName: item.update.name,
|
|
73
|
+
workspace,
|
|
74
|
+
summary: `${item.update.name} has ${item.update.advisoryCount} known security advisories.`,
|
|
75
|
+
details: item.advisories[0]?.title,
|
|
76
|
+
help: "Prioritize secure minimum upgrades before applying other dependency changes.",
|
|
77
|
+
recommendedAction: item.update.recommendedAction ??
|
|
78
|
+
"Run `rup review --security-only` and consider `rup audit --fix` for minimum safe patches.",
|
|
79
|
+
evidence: item.advisories.map((advisory) => advisory.cveId),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (item.update.riskLevel === "critical" || item.update.riskLevel === "high") {
|
|
83
|
+
findings.push({
|
|
84
|
+
id: `release-risk:${item.update.name}:${workspace}`,
|
|
85
|
+
code: item.update.riskLevel === "critical" ? "release-risk-critical" : "release-risk-high",
|
|
86
|
+
category: "Release Risk",
|
|
87
|
+
severity: item.update.riskLevel === "critical" ? "error" : "warning",
|
|
88
|
+
scope: "package",
|
|
89
|
+
packageName: item.update.name,
|
|
90
|
+
workspace,
|
|
91
|
+
summary: `${item.update.name} is classified as ${item.update.riskLevel} release risk.`,
|
|
92
|
+
details: item.update.riskReasons?.[0],
|
|
93
|
+
help: "Use review mode to inspect why the update was classified as risky before applying it.",
|
|
94
|
+
recommendedAction: item.update.recommendedAction ?? "Keep this package in review until risk reasons are cleared.",
|
|
95
|
+
evidence: item.update.riskReasons,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (item.update.diffType === "major") {
|
|
99
|
+
findings.push({
|
|
100
|
+
id: `major-upgrade:${item.update.name}:${workspace}`,
|
|
101
|
+
code: "major-upgrade",
|
|
102
|
+
category: "Release Risk",
|
|
103
|
+
severity: "warning",
|
|
104
|
+
scope: "package",
|
|
105
|
+
packageName: item.update.name,
|
|
106
|
+
workspace,
|
|
107
|
+
summary: `${item.update.name} is a major version upgrade.`,
|
|
108
|
+
help: "Major upgrades should be reviewed explicitly before being applied.",
|
|
109
|
+
recommendedAction: item.update.recommendedAction ?? "Review major changes in `rup review --interactive`.",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
|
|
113
|
+
findings.push({
|
|
114
|
+
id: `health:${item.update.name}:${workspace}`,
|
|
115
|
+
code: item.update.healthStatus === "archived" ? "package-archived" : "package-stale",
|
|
116
|
+
category: "Operational Health",
|
|
117
|
+
severity: "warning",
|
|
118
|
+
scope: "package",
|
|
119
|
+
packageName: item.update.name,
|
|
120
|
+
workspace,
|
|
121
|
+
summary: `${item.update.name} is flagged as ${item.update.healthStatus}.`,
|
|
122
|
+
help: "Monitor package health and plan alternatives if maintenance does not improve.",
|
|
123
|
+
recommendedAction: item.update.monitorReason ?? "Keep this package under monitoring in `rup review`.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (item.unusedIssues.length > 0) {
|
|
127
|
+
findings.push({
|
|
128
|
+
id: `unused:${item.update.name}:${workspace}`,
|
|
129
|
+
code: "unused-dependency-signal",
|
|
130
|
+
category: "Unused / Cleanup",
|
|
131
|
+
severity: "warning",
|
|
132
|
+
scope: "package",
|
|
133
|
+
packageName: item.update.name,
|
|
134
|
+
workspace,
|
|
135
|
+
summary: `${item.update.name} has unused or missing dependency signals.`,
|
|
136
|
+
details: item.unusedIssues[0]?.kind,
|
|
137
|
+
help: "Clean up unused or missing dependencies before widening the upgrade scope.",
|
|
138
|
+
recommendedAction: "Run `rup unused` to inspect cleanup opportunities.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return findings.sort(compareDoctorFindings);
|
|
143
|
+
}
|
|
144
|
+
function compareDoctorFindings(left, right) {
|
|
145
|
+
const severityOrder = { error: 0, warning: 1 };
|
|
146
|
+
const categoryOrder = {
|
|
147
|
+
Security: 0,
|
|
148
|
+
Compatibility: 1,
|
|
149
|
+
Policy: 2,
|
|
150
|
+
Licensing: 3,
|
|
151
|
+
"Release Risk": 4,
|
|
152
|
+
"Operational Health": 5,
|
|
153
|
+
"Unused / Cleanup": 6,
|
|
154
|
+
"Workspace Integrity": 7,
|
|
155
|
+
"Registry / Execution": 8,
|
|
156
|
+
};
|
|
157
|
+
const bySeverity = severityOrder[left.severity] - severityOrder[right.severity];
|
|
158
|
+
if (bySeverity !== 0)
|
|
159
|
+
return bySeverity;
|
|
160
|
+
const byCategory = categoryOrder[left.category] - categoryOrder[right.category];
|
|
161
|
+
if (byCategory !== 0)
|
|
162
|
+
return byCategory;
|
|
163
|
+
const leftTarget = `${left.packageName ?? ""}:${left.workspace ?? ""}:${left.code}`;
|
|
164
|
+
const rightTarget = `${right.packageName ?? ""}:${right.workspace ?? ""}:${right.code}`;
|
|
165
|
+
return leftTarget.localeCompare(rightTarget);
|
|
166
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function renderDoctorResult(result, verdictOnly = false) {
|
|
2
|
+
const lines = [
|
|
3
|
+
`State: ${result.verdict}`,
|
|
4
|
+
`Score: ${result.score}/100 (${result.scoreLabel})`,
|
|
5
|
+
`PrimaryRisk: ${result.primaryFindings[0] ?? "No blocking findings."}`,
|
|
6
|
+
`NextAction: ${result.recommendedCommand}`,
|
|
7
|
+
];
|
|
8
|
+
if (!verdictOnly) {
|
|
9
|
+
lines.push(`Counts: updates=${result.summary.updatesFound}, security=${result.summary.securityPackages ?? 0}, risk=${result.summary.riskPackages ?? 0}, peer=${result.summary.peerConflictPackages ?? 0}, license=${result.summary.licenseViolationPackages ?? 0}`);
|
|
10
|
+
lines.push(`NextActionReason: ${result.nextActionReason}`);
|
|
11
|
+
}
|
|
12
|
+
return lines.join("\n");
|
|
13
|
+
}
|
|
14
|
+
export function renderDoctorAgentReport(result) {
|
|
15
|
+
const lines = [
|
|
16
|
+
`Rainy Updates doctor report for ${result.review.projectPath}`,
|
|
17
|
+
`State: ${result.verdict}`,
|
|
18
|
+
`Score: ${result.score}/100 (${result.scoreLabel})`,
|
|
19
|
+
`PrimaryRisk: ${result.primaryFindings[0] ?? "No blocking findings."}`,
|
|
20
|
+
`NextAction: ${result.recommendedCommand}`,
|
|
21
|
+
`Why: ${result.nextActionReason}`,
|
|
22
|
+
"",
|
|
23
|
+
"Priority findings:",
|
|
24
|
+
];
|
|
25
|
+
const findings = result.findings.slice(0, 8);
|
|
26
|
+
if (findings.length === 0) {
|
|
27
|
+
lines.push("- No blocking findings detected.");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
for (const finding of findings) {
|
|
31
|
+
const target = finding.packageName ? ` package=${finding.packageName}` : "";
|
|
32
|
+
lines.push(`- [${finding.severity}] ${finding.category}${target}: ${finding.summary}`);
|
|
33
|
+
if (finding.recommendedAction) {
|
|
34
|
+
lines.push(` fix: ${finding.recommendedAction}`);
|
|
35
|
+
}
|
|
36
|
+
if (finding.help) {
|
|
37
|
+
lines.push(` help: ${finding.help}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(`Verification: ${result.recommendedCommand}`);
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { deriveReviewVerdict } from "../review-verdict.js";
|
|
2
|
+
import { buildDoctorFindings } from "./findings.js";
|
|
3
|
+
import { calculateDoctorScore, countFindingsByCategory, countFindingsBySeverity, labelDoctorScore, } from "./score.js";
|
|
4
|
+
export function createDoctorResult(review) {
|
|
5
|
+
const verdict = review.summary.verdict ?? deriveReviewVerdict(review.items, review.errors);
|
|
6
|
+
const findings = buildDoctorFindings(review);
|
|
7
|
+
const score = calculateDoctorScore(findings);
|
|
8
|
+
const scoreLabel = labelDoctorScore(score);
|
|
9
|
+
const primaryFindings = findings.length > 0
|
|
10
|
+
? findings.map((finding) => finding.summary)
|
|
11
|
+
: ["No blocking findings; remaining updates are low-risk."];
|
|
12
|
+
const recommendedCommand = recommendDoctorCommand(review, verdict);
|
|
13
|
+
const nextActionReason = describeNextActionReason(review, verdict);
|
|
14
|
+
review.summary.dependencyHealthScore = score;
|
|
15
|
+
review.summary.findingCountsByCategory = countFindingsByCategory(findings);
|
|
16
|
+
review.summary.findingCountsBySeverity = countFindingsBySeverity(findings);
|
|
17
|
+
review.summary.primaryFindingCode = findings[0]?.code;
|
|
18
|
+
review.summary.primaryFindingCategory = findings[0]?.category;
|
|
19
|
+
review.summary.nextActionReason = nextActionReason;
|
|
20
|
+
return {
|
|
21
|
+
verdict,
|
|
22
|
+
score,
|
|
23
|
+
scoreLabel,
|
|
24
|
+
summary: review.summary,
|
|
25
|
+
review,
|
|
26
|
+
findings,
|
|
27
|
+
primaryFindings,
|
|
28
|
+
recommendedCommand,
|
|
29
|
+
nextActionReason,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function recommendDoctorCommand(review, verdict) {
|
|
33
|
+
if (verdict === "blocked")
|
|
34
|
+
return "rup review --interactive";
|
|
35
|
+
if ((review.summary.securityPackages ?? 0) > 0)
|
|
36
|
+
return "rup review --security-only";
|
|
37
|
+
if (review.errors.length > 0 || review.items.length > 0)
|
|
38
|
+
return "rup review --interactive";
|
|
39
|
+
return "rup check";
|
|
40
|
+
}
|
|
41
|
+
function describeNextActionReason(review, verdict) {
|
|
42
|
+
if (verdict === "blocked") {
|
|
43
|
+
return "Blocked findings exist, so the update set needs an explicit package-by-package review before any mutation.";
|
|
44
|
+
}
|
|
45
|
+
if ((review.summary.securityPackages ?? 0) > 0) {
|
|
46
|
+
return "Security advisories are present, so the next step should focus on the secure subset first.";
|
|
47
|
+
}
|
|
48
|
+
if (review.errors.length > 0) {
|
|
49
|
+
return "Execution issues reduce trust in the current scan, so the result should be reviewed before treating it as clean.";
|
|
50
|
+
}
|
|
51
|
+
if (review.items.length > 0) {
|
|
52
|
+
return "Reviewable updates were found, so the next step is to inspect and approve the package set.";
|
|
53
|
+
}
|
|
54
|
+
return "No reviewable changes remain, so a normal check is enough to verify the repository stays clean.";
|
|
55
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DoctorFinding, DoctorFindingCategory, DoctorFindingSeverity, DoctorScoreLabel } from "../../types/index.js";
|
|
2
|
+
export declare function calculateDoctorScore(findings: DoctorFinding[]): number;
|
|
3
|
+
export declare function labelDoctorScore(score: number): DoctorScoreLabel;
|
|
4
|
+
export declare function countFindingsByCategory(findings: DoctorFinding[]): Partial<Record<DoctorFindingCategory, number>>;
|
|
5
|
+
export declare function countFindingsBySeverity(findings: DoctorFinding[]): Partial<Record<DoctorFindingSeverity, number>>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function calculateDoctorScore(findings) {
|
|
2
|
+
const uniqueErrorCodes = new Set(findings.filter((finding) => finding.severity === "error").map((finding) => finding.code));
|
|
3
|
+
const uniqueWarningCodes = new Set(findings.filter((finding) => finding.severity === "warning").map((finding) => finding.code));
|
|
4
|
+
return Math.max(0, 100 - uniqueErrorCodes.size * 12 - uniqueWarningCodes.size * 5);
|
|
5
|
+
}
|
|
6
|
+
export function labelDoctorScore(score) {
|
|
7
|
+
if (score >= 85)
|
|
8
|
+
return "Strong";
|
|
9
|
+
if (score >= 65)
|
|
10
|
+
return "Needs Review";
|
|
11
|
+
if (score >= 40)
|
|
12
|
+
return "Action Needed";
|
|
13
|
+
return "Blocked / Critical";
|
|
14
|
+
}
|
|
15
|
+
export function countFindingsByCategory(findings) {
|
|
16
|
+
const counts = {};
|
|
17
|
+
for (const finding of findings) {
|
|
18
|
+
counts[finding.category] = (counts[finding.category] ?? 0) + 1;
|
|
19
|
+
}
|
|
20
|
+
return counts;
|
|
21
|
+
}
|
|
22
|
+
export function countFindingsBySeverity(findings) {
|
|
23
|
+
const counts = {};
|
|
24
|
+
for (const finding of findings) {
|
|
25
|
+
counts[finding.severity] = (counts[finding.severity] ?? 0) + 1;
|
|
26
|
+
}
|
|
27
|
+
return counts;
|
|
28
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CheckOptions, DoctorOptions,
|
|
1
|
+
import type { CheckOptions, DoctorOptions, ReviewOptions, ReviewResult } from "../types/index.js";
|
|
2
|
+
export { createDoctorResult } from "./doctor/result.js";
|
|
3
|
+
export { renderDoctorAgentReport, renderDoctorResult, } from "./doctor/render.js";
|
|
2
4
|
export declare function buildReviewResult(options: ReviewOptions | DoctorOptions | CheckOptions): Promise<ReviewResult>;
|
|
3
|
-
export declare function createDoctorResult(review: ReviewResult): DoctorResult;
|
|
4
5
|
export declare function renderReviewResult(review: ReviewResult): string;
|
|
5
|
-
export declare function renderDoctorResult(result: DoctorResult, verdictOnly?: boolean): string;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createSummary, finalizeSummary } from "./summary.js";
|
|
3
3
|
import { buildAnalysisBundle } from "./analysis-bundle.js";
|
|
4
|
+
export { createDoctorResult } from "./doctor/result.js";
|
|
5
|
+
export { renderDoctorAgentReport, renderDoctorResult, } from "./doctor/render.js";
|
|
6
|
+
import { deriveReviewVerdict } from "./review-verdict.js";
|
|
4
7
|
export async function buildReviewResult(options) {
|
|
5
8
|
const includeChangelog = ("showChangelog" in options && options.showChangelog === true) ||
|
|
6
9
|
("includeChangelog" in options && options.includeChangelog === true) ||
|
|
@@ -48,17 +51,6 @@ export async function buildReviewResult(options) {
|
|
|
48
51
|
],
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
|
-
export function createDoctorResult(review) {
|
|
52
|
-
const verdict = review.summary.verdict ?? deriveVerdict(review.items, review.errors);
|
|
53
|
-
const primaryFindings = buildPrimaryFindings(review);
|
|
54
|
-
return {
|
|
55
|
-
verdict,
|
|
56
|
-
summary: review.summary,
|
|
57
|
-
review,
|
|
58
|
-
primaryFindings,
|
|
59
|
-
recommendedCommand: recommendCommand(review, verdict),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
54
|
export function renderReviewResult(review) {
|
|
63
55
|
const lines = [];
|
|
64
56
|
lines.push(`Project: ${review.projectPath}`);
|
|
@@ -119,17 +111,6 @@ export function renderReviewResult(review) {
|
|
|
119
111
|
lines.push(`Summary: ${review.summary.updatesFound} updates, riskPackages=${review.summary.riskPackages ?? 0}, securityPackages=${review.summary.securityPackages ?? 0}, peerConflictPackages=${review.summary.peerConflictPackages ?? 0}`);
|
|
120
112
|
return lines.join("\n");
|
|
121
113
|
}
|
|
122
|
-
export function renderDoctorResult(result, verdictOnly = false) {
|
|
123
|
-
const lines = [
|
|
124
|
-
`State: ${result.verdict}`,
|
|
125
|
-
`PrimaryRisk: ${result.primaryFindings[0] ?? "No blocking findings."}`,
|
|
126
|
-
`NextAction: ${result.recommendedCommand}`,
|
|
127
|
-
];
|
|
128
|
-
if (!verdictOnly) {
|
|
129
|
-
lines.push(`Counts: updates=${result.summary.updatesFound}, security=${result.summary.securityPackages ?? 0}, risk=${result.summary.riskPackages ?? 0}, peer=${result.summary.peerConflictPackages ?? 0}, license=${result.summary.licenseViolationPackages ?? 0}`);
|
|
130
|
-
}
|
|
131
|
-
return lines.join("\n");
|
|
132
|
-
}
|
|
133
114
|
function matchesReviewFilters(item, options) {
|
|
134
115
|
if ("securityOnly" in options && options.securityOnly && item.advisories.length === 0) {
|
|
135
116
|
return false;
|
|
@@ -196,51 +177,6 @@ function createReviewSummary(base, items, errors, warnings, interactiveSession,
|
|
|
196
177
|
summary.monitorPackages = items.filter((item) => item.update.policyAction === "monitor").length;
|
|
197
178
|
summary.decisionPackages = items.length;
|
|
198
179
|
summary.degradedSources = degradedSources;
|
|
199
|
-
summary.verdict =
|
|
180
|
+
summary.verdict = deriveReviewVerdict(items, errors);
|
|
200
181
|
return summary;
|
|
201
182
|
}
|
|
202
|
-
function deriveVerdict(items, errors) {
|
|
203
|
-
if (items.some((item) => item.update.peerConflictSeverity === "error" ||
|
|
204
|
-
item.update.licenseStatus === "denied")) {
|
|
205
|
-
return "blocked";
|
|
206
|
-
}
|
|
207
|
-
if (items.some((item) => item.advisories.length > 0 || item.update.riskLevel === "critical")) {
|
|
208
|
-
return "actionable";
|
|
209
|
-
}
|
|
210
|
-
if (errors.length > 0 ||
|
|
211
|
-
items.some((item) => item.update.riskLevel === "high" || item.update.diffType === "major")) {
|
|
212
|
-
return "review";
|
|
213
|
-
}
|
|
214
|
-
return "safe";
|
|
215
|
-
}
|
|
216
|
-
function buildPrimaryFindings(review) {
|
|
217
|
-
const findings = [];
|
|
218
|
-
if ((review.summary.peerConflictPackages ?? 0) > 0) {
|
|
219
|
-
findings.push(`${review.summary.peerConflictPackages} package(s) have peer conflicts.`);
|
|
220
|
-
}
|
|
221
|
-
if ((review.summary.licenseViolationPackages ?? 0) > 0) {
|
|
222
|
-
findings.push(`${review.summary.licenseViolationPackages} package(s) violate license policy.`);
|
|
223
|
-
}
|
|
224
|
-
if ((review.summary.securityPackages ?? 0) > 0) {
|
|
225
|
-
findings.push(`${review.summary.securityPackages} package(s) have security advisories.`);
|
|
226
|
-
}
|
|
227
|
-
if ((review.summary.riskPackages ?? 0) > 0) {
|
|
228
|
-
findings.push(`${review.summary.riskPackages} package(s) are high risk.`);
|
|
229
|
-
}
|
|
230
|
-
if (review.errors.length > 0) {
|
|
231
|
-
findings.push(`${review.errors.length} execution error(s) need review before treating the result as clean.`);
|
|
232
|
-
}
|
|
233
|
-
if (findings.length === 0) {
|
|
234
|
-
findings.push("No blocking findings; remaining updates are low-risk.");
|
|
235
|
-
}
|
|
236
|
-
return findings;
|
|
237
|
-
}
|
|
238
|
-
function recommendCommand(review, verdict) {
|
|
239
|
-
if (verdict === "blocked")
|
|
240
|
-
return "rup review --interactive";
|
|
241
|
-
if ((review.summary.securityPackages ?? 0) > 0)
|
|
242
|
-
return "rup review --security-only";
|
|
243
|
-
if (review.errors.length > 0 || review.items.length > 0)
|
|
244
|
-
return "rup review --interactive";
|
|
245
|
-
return "rup check";
|
|
246
|
-
}
|