@getcodesentinel/codesentinel 1.7.0 → 1.8.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 +32 -22
- package/dist/index.js +501 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ Then run:
|
|
|
70
70
|
```bash
|
|
71
71
|
codesentinel analyze [path]
|
|
72
72
|
codesentinel explain [path]
|
|
73
|
+
codesentinel report [path]
|
|
73
74
|
codesentinel dependency-risk <dependency[@version]>
|
|
74
75
|
```
|
|
75
76
|
|
|
@@ -83,6 +84,10 @@ codesentinel explain
|
|
|
83
84
|
codesentinel explain . --top 5 --format text
|
|
84
85
|
codesentinel explain . --file src/app/page.tsx
|
|
85
86
|
codesentinel explain . --module src/components
|
|
87
|
+
codesentinel report
|
|
88
|
+
codesentinel report --format md --output report.md
|
|
89
|
+
codesentinel report --snapshot snapshot.json
|
|
90
|
+
codesentinel report --compare baseline.json --format text
|
|
86
91
|
codesentinel dependency-risk react
|
|
87
92
|
codesentinel dependency-risk react@19.0.0
|
|
88
93
|
```
|
|
@@ -121,6 +126,13 @@ codesentinel explain . --module src/components
|
|
|
121
126
|
# Explain in markdown or json
|
|
122
127
|
codesentinel explain . --format md
|
|
123
128
|
codesentinel explain . --format json
|
|
129
|
+
|
|
130
|
+
# Report generation (human + machine readable)
|
|
131
|
+
codesentinel report .
|
|
132
|
+
codesentinel report . --format md --output report.md
|
|
133
|
+
codesentinel report . --format json
|
|
134
|
+
codesentinel report . --snapshot snapshot.json
|
|
135
|
+
codesentinel report . --compare baseline.json --format text
|
|
124
136
|
```
|
|
125
137
|
|
|
126
138
|
Notes:
|
|
@@ -145,8 +157,28 @@ pnpm dev -- analyze . --author-identity strict_email
|
|
|
145
157
|
pnpm dev -- explain
|
|
146
158
|
pnpm dev -- explain . --top 5 --format text
|
|
147
159
|
pnpm dev -- explain . --file src/app/page.tsx
|
|
160
|
+
pnpm dev -- report
|
|
161
|
+
pnpm dev -- report . --format md --output report.md
|
|
162
|
+
pnpm dev -- report . --compare baseline.json --format text
|
|
148
163
|
```
|
|
149
164
|
|
|
165
|
+
## Report Output
|
|
166
|
+
|
|
167
|
+
`codesentinel report` produces deterministic engineering artifacts from existing analysis outputs.
|
|
168
|
+
|
|
169
|
+
- formats: `text`, `md`, `json`
|
|
170
|
+
- optional file output: `--output <path>`
|
|
171
|
+
- optional snapshot export: `--snapshot <path>`
|
|
172
|
+
- optional diff mode: `--compare <baseline.json>`
|
|
173
|
+
|
|
174
|
+
Diff mode compares snapshots and reports:
|
|
175
|
+
|
|
176
|
+
- repository score deltas
|
|
177
|
+
- file/module risk deltas
|
|
178
|
+
- new/resolved hotspots
|
|
179
|
+
- new/resolved cycles
|
|
180
|
+
- dependency exposure list changes
|
|
181
|
+
|
|
150
182
|
## Explain Output
|
|
151
183
|
|
|
152
184
|
`codesentinel explain` uses the same risk-engine scoring model as `analyze` and adds structured explanation traces.
|
|
@@ -278,28 +310,6 @@ Propagation policy is explicit and deterministic:
|
|
|
278
310
|
|
|
279
311
|
This keeps package-level facts local while still surfacing meaningful transitive exposure.
|
|
280
312
|
|
|
281
|
-
## Release Automation
|
|
282
|
-
|
|
283
|
-
- Pull requests to `main` run build and tests via `.github/workflows/ci.yml`.
|
|
284
|
-
- Pull requests and release runs also validate the packed CLI artifact (`npm pack`) with install/execute smoke checks before publish.
|
|
285
|
-
- Merges to `main` run semantic-release via `.github/workflows/release.yml`.
|
|
286
|
-
- semantic-release bumps `packages/cli/package.json`, creates a GitHub release, publishes to npm, and pushes the version-bump commit back to `main`.
|
|
287
|
-
- Dependabot is configured monthly in `.github/dependabot.yml` for npm and GitHub Actions updates.
|
|
288
|
-
|
|
289
|
-
Trusted Publisher setup (no `NPM_TOKEN` secret):
|
|
290
|
-
|
|
291
|
-
- In npm package settings for `@getcodesentinel/codesentinel`, add a Trusted Publisher.
|
|
292
|
-
- Provider: `GitHub Actions`.
|
|
293
|
-
- Repository: `getcodesentinel/codesentinel`.
|
|
294
|
-
- Workflow filename: `release.yml`.
|
|
295
|
-
- Environment name: leave empty unless you explicitly use a GitHub Actions environment in this workflow.
|
|
296
|
-
|
|
297
|
-
Commit messages on `main` should follow Conventional Commits (example: `feat:`, `fix:`, `chore:`) so semantic-release can calculate versions automatically.
|
|
298
|
-
|
|
299
|
-
## Contributing
|
|
300
|
-
|
|
301
|
-
This project aims to be production-grade and minimal. If you add new dependencies or abstractions, justify them clearly and keep the architecture clean.
|
|
302
|
-
|
|
303
313
|
## ESM Import Policy
|
|
304
314
|
|
|
305
315
|
- The workspace uses `TypeScript` with `moduleResolution: "NodeNext"` and ESM output.
|
package/dist/index.js
CHANGED
|
@@ -4030,6 +4030,474 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, logger = createSil
|
|
|
4030
4030
|
};
|
|
4031
4031
|
};
|
|
4032
4032
|
|
|
4033
|
+
// src/application/run-report-command.ts
|
|
4034
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4035
|
+
|
|
4036
|
+
// ../reporter/dist/index.js
|
|
4037
|
+
var SNAPSHOT_SCHEMA_VERSION = "codesentinel.snapshot.v1";
|
|
4038
|
+
var REPORT_SCHEMA_VERSION = "codesentinel.report.v1";
|
|
4039
|
+
var RISK_MODEL_VERSION = "deterministic-v1";
|
|
4040
|
+
var round45 = (value) => Number(value.toFixed(4));
|
|
4041
|
+
var toRiskTier = (score) => {
|
|
4042
|
+
if (score < 20) {
|
|
4043
|
+
return "low";
|
|
4044
|
+
}
|
|
4045
|
+
if (score < 40) {
|
|
4046
|
+
return "moderate";
|
|
4047
|
+
}
|
|
4048
|
+
if (score < 60) {
|
|
4049
|
+
return "elevated";
|
|
4050
|
+
}
|
|
4051
|
+
if (score < 80) {
|
|
4052
|
+
return "high";
|
|
4053
|
+
}
|
|
4054
|
+
return "very_high";
|
|
4055
|
+
};
|
|
4056
|
+
var factorLabelById2 = {
|
|
4057
|
+
"repository.structural": "Structural complexity",
|
|
4058
|
+
"repository.evolution": "Change volatility",
|
|
4059
|
+
"repository.external": "External dependency pressure",
|
|
4060
|
+
"repository.composite.interactions": "Intersection amplification",
|
|
4061
|
+
"file.structural": "File structural complexity",
|
|
4062
|
+
"file.evolution": "File change volatility",
|
|
4063
|
+
"file.external": "File external pressure",
|
|
4064
|
+
"file.composite.interactions": "File interaction amplification",
|
|
4065
|
+
"module.average_file_risk": "Average file risk",
|
|
4066
|
+
"module.peak_file_risk": "Peak file risk",
|
|
4067
|
+
"dependency.signals": "Dependency risk signals",
|
|
4068
|
+
"dependency.staleness": "Dependency staleness",
|
|
4069
|
+
"dependency.maintainer_concentration": "Maintainer concentration",
|
|
4070
|
+
"dependency.topology": "Dependency topology pressure",
|
|
4071
|
+
"dependency.bus_factor": "Dependency bus factor",
|
|
4072
|
+
"dependency.popularity_dampening": "Popularity dampening"
|
|
4073
|
+
};
|
|
4074
|
+
var factorLabel = (factorId) => factorLabelById2[factorId] ?? factorId;
|
|
4075
|
+
var summarizeEvidence = (factor) => {
|
|
4076
|
+
const entries = Object.entries(factor.rawMetrics).filter(([, value]) => value !== null).sort((a, b) => a[0].localeCompare(b[0])).slice(0, 3).map(([key, value]) => `${key}=${value}`);
|
|
4077
|
+
if (entries.length > 0) {
|
|
4078
|
+
return entries.join(", ");
|
|
4079
|
+
}
|
|
4080
|
+
const evidence = [...factor.evidence].map((entry) => {
|
|
4081
|
+
if (entry.kind === "file_metric") {
|
|
4082
|
+
return `${entry.target}:${entry.metric}`;
|
|
4083
|
+
}
|
|
4084
|
+
if (entry.kind === "dependency_metric") {
|
|
4085
|
+
return `${entry.target}:${entry.metric}`;
|
|
4086
|
+
}
|
|
4087
|
+
if (entry.kind === "repository_metric") {
|
|
4088
|
+
return entry.metric;
|
|
4089
|
+
}
|
|
4090
|
+
if (entry.kind === "graph_cycle") {
|
|
4091
|
+
return `cycle:${entry.cycleId}`;
|
|
4092
|
+
}
|
|
4093
|
+
return `${entry.fileA}<->${entry.fileB}`;
|
|
4094
|
+
}).sort((a, b) => a.localeCompare(b));
|
|
4095
|
+
return evidence.join(", ");
|
|
4096
|
+
};
|
|
4097
|
+
var diffSets = (current, baseline) => {
|
|
4098
|
+
const currentSet = new Set(current);
|
|
4099
|
+
const baselineSet = new Set(baseline);
|
|
4100
|
+
const added = [...currentSet].filter((item) => !baselineSet.has(item)).sort((a, b) => a.localeCompare(b));
|
|
4101
|
+
const removed = [...baselineSet].filter((item) => !currentSet.has(item)).sort((a, b) => a.localeCompare(b));
|
|
4102
|
+
return { added, removed };
|
|
4103
|
+
};
|
|
4104
|
+
var diffScoreMap = (current, baseline) => {
|
|
4105
|
+
const keys = [.../* @__PURE__ */ new Set([...current.keys(), ...baseline.keys()])].sort((a, b) => a.localeCompare(b));
|
|
4106
|
+
return keys.map((key) => {
|
|
4107
|
+
const before = baseline.get(key) ?? 0;
|
|
4108
|
+
const after = current.get(key) ?? 0;
|
|
4109
|
+
const delta = round45(after - before);
|
|
4110
|
+
return {
|
|
4111
|
+
target: key,
|
|
4112
|
+
before: round45(before),
|
|
4113
|
+
after: round45(after),
|
|
4114
|
+
delta
|
|
4115
|
+
};
|
|
4116
|
+
}).filter((entry) => entry.delta !== 0).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta) || a.target.localeCompare(b.target));
|
|
4117
|
+
};
|
|
4118
|
+
var cycleKey = (nodes) => [...nodes].sort((a, b) => a.localeCompare(b)).join(" -> ");
|
|
4119
|
+
var compareSnapshots = (current, baseline) => {
|
|
4120
|
+
const currentFileScores = new Map(
|
|
4121
|
+
current.analysis.risk.fileScores.map((item) => [item.file, item.score])
|
|
4122
|
+
);
|
|
4123
|
+
const baselineFileScores = new Map(
|
|
4124
|
+
baseline.analysis.risk.fileScores.map((item) => [item.file, item.score])
|
|
4125
|
+
);
|
|
4126
|
+
const currentModuleScores = new Map(
|
|
4127
|
+
current.analysis.risk.moduleScores.map((item) => [item.module, item.score])
|
|
4128
|
+
);
|
|
4129
|
+
const baselineModuleScores = new Map(
|
|
4130
|
+
baseline.analysis.risk.moduleScores.map((item) => [item.module, item.score])
|
|
4131
|
+
);
|
|
4132
|
+
const currentHotspots = current.analysis.risk.hotspots.slice(0, 10).map((item) => item.file);
|
|
4133
|
+
const baselineHotspots = baseline.analysis.risk.hotspots.slice(0, 10).map((item) => item.file);
|
|
4134
|
+
const currentCycles = current.analysis.structural.cycles.map((cycle) => cycleKey(cycle.nodes));
|
|
4135
|
+
const baselineCycles = baseline.analysis.structural.cycles.map((cycle) => cycleKey(cycle.nodes));
|
|
4136
|
+
const currentExternal = current.analysis.external.available ? current.analysis.external : {
|
|
4137
|
+
highRiskDependencies: [],
|
|
4138
|
+
singleMaintainerDependencies: [],
|
|
4139
|
+
abandonedDependencies: []
|
|
4140
|
+
};
|
|
4141
|
+
const baselineExternal = baseline.analysis.external.available ? baseline.analysis.external : {
|
|
4142
|
+
highRiskDependencies: [],
|
|
4143
|
+
singleMaintainerDependencies: [],
|
|
4144
|
+
abandonedDependencies: []
|
|
4145
|
+
};
|
|
4146
|
+
const highRisk = diffSets(currentExternal.highRiskDependencies, baselineExternal.highRiskDependencies);
|
|
4147
|
+
const singleMaintainer = diffSets(
|
|
4148
|
+
currentExternal.singleMaintainerDependencies,
|
|
4149
|
+
baselineExternal.singleMaintainerDependencies
|
|
4150
|
+
);
|
|
4151
|
+
const abandoned = diffSets(currentExternal.abandonedDependencies, baselineExternal.abandonedDependencies);
|
|
4152
|
+
const hotspots = diffSets(currentHotspots, baselineHotspots);
|
|
4153
|
+
const cycles = diffSets(currentCycles, baselineCycles);
|
|
4154
|
+
return {
|
|
4155
|
+
repositoryScoreDelta: round45(current.analysis.risk.repositoryScore - baseline.analysis.risk.repositoryScore),
|
|
4156
|
+
normalizedScoreDelta: round45(current.analysis.risk.normalizedScore - baseline.analysis.risk.normalizedScore),
|
|
4157
|
+
fileRiskChanges: diffScoreMap(currentFileScores, baselineFileScores),
|
|
4158
|
+
moduleRiskChanges: diffScoreMap(currentModuleScores, baselineModuleScores),
|
|
4159
|
+
newHotspots: hotspots.added,
|
|
4160
|
+
resolvedHotspots: hotspots.removed,
|
|
4161
|
+
newCycles: cycles.added,
|
|
4162
|
+
resolvedCycles: cycles.removed,
|
|
4163
|
+
externalChanges: {
|
|
4164
|
+
highRiskAdded: highRisk.added,
|
|
4165
|
+
highRiskRemoved: highRisk.removed,
|
|
4166
|
+
singleMaintainerAdded: singleMaintainer.added,
|
|
4167
|
+
singleMaintainerRemoved: singleMaintainer.removed,
|
|
4168
|
+
abandonedAdded: abandoned.added,
|
|
4169
|
+
abandonedRemoved: abandoned.removed
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
};
|
|
4173
|
+
var findTraceTarget = (snapshot, targetType, targetId) => snapshot.trace?.targets.find(
|
|
4174
|
+
(target) => target.targetType === targetType && target.targetId === targetId
|
|
4175
|
+
);
|
|
4176
|
+
var toRenderedFactors = (target) => {
|
|
4177
|
+
if (target === void 0) {
|
|
4178
|
+
return [];
|
|
4179
|
+
}
|
|
4180
|
+
return [...target.factors].sort((a, b) => b.contribution - a.contribution || a.factorId.localeCompare(b.factorId)).slice(0, 4).map((factor) => ({
|
|
4181
|
+
id: factor.factorId,
|
|
4182
|
+
label: factorLabel(factor.factorId),
|
|
4183
|
+
contribution: round45(factor.contribution),
|
|
4184
|
+
confidence: round45(factor.confidence),
|
|
4185
|
+
evidence: summarizeEvidence(factor)
|
|
4186
|
+
}));
|
|
4187
|
+
};
|
|
4188
|
+
var suggestedActions = (target) => {
|
|
4189
|
+
if (target === void 0) {
|
|
4190
|
+
return [];
|
|
4191
|
+
}
|
|
4192
|
+
const actions = [];
|
|
4193
|
+
for (const lever of target.reductionLevers) {
|
|
4194
|
+
switch (lever.factorId) {
|
|
4195
|
+
case "file.evolution":
|
|
4196
|
+
case "repository.evolution":
|
|
4197
|
+
actions.push("Reduce recent churn and volatile edit frequency in this area.");
|
|
4198
|
+
break;
|
|
4199
|
+
case "file.structural":
|
|
4200
|
+
case "repository.structural":
|
|
4201
|
+
actions.push("Reduce fan-in/fan-out concentration and simplify deep dependency paths.");
|
|
4202
|
+
break;
|
|
4203
|
+
case "file.composite.interactions":
|
|
4204
|
+
case "repository.composite.interactions":
|
|
4205
|
+
actions.push("Stabilize central files before concurrent structural changes.");
|
|
4206
|
+
break;
|
|
4207
|
+
case "file.external":
|
|
4208
|
+
case "repository.external":
|
|
4209
|
+
actions.push("Review external dependency pressure for this hotspot.");
|
|
4210
|
+
break;
|
|
4211
|
+
default:
|
|
4212
|
+
actions.push(`Reduce ${factorLabel(lever.factorId).toLowerCase()} influence.`);
|
|
4213
|
+
break;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
return [...new Set(actions)].slice(0, 3);
|
|
4217
|
+
};
|
|
4218
|
+
var hotspotItems = (snapshot) => snapshot.analysis.risk.hotspots.slice(0, 10).map((hotspot) => {
|
|
4219
|
+
const fileScore = snapshot.analysis.risk.fileScores.find((item) => item.file === hotspot.file);
|
|
4220
|
+
const traceTarget = findTraceTarget(snapshot, "file", hotspot.file);
|
|
4221
|
+
const factors = toRenderedFactors(traceTarget);
|
|
4222
|
+
return {
|
|
4223
|
+
target: hotspot.file,
|
|
4224
|
+
score: hotspot.score,
|
|
4225
|
+
normalizedScore: fileScore?.normalizedScore ?? round45(hotspot.score / 100),
|
|
4226
|
+
topFactors: factors,
|
|
4227
|
+
suggestedActions: suggestedActions(traceTarget),
|
|
4228
|
+
biggestLevers: (traceTarget?.reductionLevers ?? []).slice(0, 3).map((lever) => `${factorLabel(lever.factorId)} (${lever.estimatedImpact})`)
|
|
4229
|
+
};
|
|
4230
|
+
});
|
|
4231
|
+
var repositoryConfidence = (snapshot) => {
|
|
4232
|
+
const target = findTraceTarget(snapshot, "repository", snapshot.analysis.structural.targetPath);
|
|
4233
|
+
if (target === void 0 || target.factors.length === 0) {
|
|
4234
|
+
return null;
|
|
4235
|
+
}
|
|
4236
|
+
const weight = target.factors.reduce((sum, factor) => sum + factor.contribution, 0);
|
|
4237
|
+
if (weight <= 0) {
|
|
4238
|
+
return null;
|
|
4239
|
+
}
|
|
4240
|
+
const weighted = target.factors.reduce(
|
|
4241
|
+
(sum, factor) => sum + factor.confidence * factor.contribution,
|
|
4242
|
+
0
|
|
4243
|
+
);
|
|
4244
|
+
return round45(weighted / weight);
|
|
4245
|
+
};
|
|
4246
|
+
var createReport = (snapshot, diff) => {
|
|
4247
|
+
const external = snapshot.analysis.external;
|
|
4248
|
+
return {
|
|
4249
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
4250
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4251
|
+
repository: {
|
|
4252
|
+
targetPath: snapshot.analysis.structural.targetPath,
|
|
4253
|
+
repositoryScore: snapshot.analysis.risk.repositoryScore,
|
|
4254
|
+
normalizedScore: snapshot.analysis.risk.normalizedScore,
|
|
4255
|
+
riskTier: toRiskTier(snapshot.analysis.risk.repositoryScore),
|
|
4256
|
+
confidence: repositoryConfidence(snapshot)
|
|
4257
|
+
},
|
|
4258
|
+
hotspots: hotspotItems(snapshot),
|
|
4259
|
+
structural: {
|
|
4260
|
+
cycleCount: snapshot.analysis.structural.metrics.cycleCount,
|
|
4261
|
+
cycles: snapshot.analysis.structural.cycles.map(
|
|
4262
|
+
(cycle) => [...cycle.nodes].sort((a, b) => a.localeCompare(b)).join(" -> ")
|
|
4263
|
+
),
|
|
4264
|
+
fragileClusters: snapshot.analysis.risk.fragileClusters.map((cluster) => ({
|
|
4265
|
+
id: cluster.id,
|
|
4266
|
+
kind: cluster.kind,
|
|
4267
|
+
score: cluster.score,
|
|
4268
|
+
files: [...cluster.files].sort((a, b) => a.localeCompare(b))
|
|
4269
|
+
}))
|
|
4270
|
+
},
|
|
4271
|
+
external: !external.available ? {
|
|
4272
|
+
available: false,
|
|
4273
|
+
reason: external.reason
|
|
4274
|
+
} : {
|
|
4275
|
+
available: true,
|
|
4276
|
+
highRiskDependencies: [...external.highRiskDependencies].sort((a, b) => a.localeCompare(b)),
|
|
4277
|
+
highRiskDevelopmentDependencies: [...external.highRiskDevelopmentDependencies].sort((a, b) => a.localeCompare(b)),
|
|
4278
|
+
singleMaintainerDependencies: [...external.singleMaintainerDependencies].sort((a, b) => a.localeCompare(b)),
|
|
4279
|
+
abandonedDependencies: [...external.abandonedDependencies].sort((a, b) => a.localeCompare(b))
|
|
4280
|
+
},
|
|
4281
|
+
appendix: {
|
|
4282
|
+
snapshotSchemaVersion: snapshot.schemaVersion,
|
|
4283
|
+
riskModelVersion: snapshot.riskModelVersion,
|
|
4284
|
+
timestamp: snapshot.generatedAt,
|
|
4285
|
+
normalization: "Scores are deterministic 0-100 outputs from risk-engine normalized factors and interaction terms.",
|
|
4286
|
+
...snapshot.analysisConfig === void 0 ? {} : { analysisConfig: snapshot.analysisConfig }
|
|
4287
|
+
},
|
|
4288
|
+
...diff === void 0 ? {} : { diff }
|
|
4289
|
+
};
|
|
4290
|
+
};
|
|
4291
|
+
var renderTextDiff = (report) => {
|
|
4292
|
+
if (report.diff === void 0) {
|
|
4293
|
+
return [];
|
|
4294
|
+
}
|
|
4295
|
+
return [
|
|
4296
|
+
"",
|
|
4297
|
+
"Diff",
|
|
4298
|
+
` repositoryScoreDelta: ${report.diff.repositoryScoreDelta}`,
|
|
4299
|
+
` normalizedScoreDelta: ${report.diff.normalizedScoreDelta}`,
|
|
4300
|
+
` newHotspots: ${report.diff.newHotspots.join(", ") || "none"}`,
|
|
4301
|
+
` resolvedHotspots: ${report.diff.resolvedHotspots.join(", ") || "none"}`,
|
|
4302
|
+
` newCycles: ${report.diff.newCycles.join(", ") || "none"}`,
|
|
4303
|
+
` resolvedCycles: ${report.diff.resolvedCycles.join(", ") || "none"}`
|
|
4304
|
+
];
|
|
4305
|
+
};
|
|
4306
|
+
var renderTextReport = (report) => {
|
|
4307
|
+
const lines = [];
|
|
4308
|
+
lines.push("Repository Summary");
|
|
4309
|
+
lines.push(` target: ${report.repository.targetPath}`);
|
|
4310
|
+
lines.push(` repositoryScore: ${report.repository.repositoryScore}`);
|
|
4311
|
+
lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
|
|
4312
|
+
lines.push(` riskTier: ${report.repository.riskTier}`);
|
|
4313
|
+
lines.push(` confidence: ${report.repository.confidence ?? "n/a"}`);
|
|
4314
|
+
lines.push("");
|
|
4315
|
+
lines.push("Top Hotspots");
|
|
4316
|
+
for (const hotspot of report.hotspots) {
|
|
4317
|
+
lines.push(` - ${hotspot.target} | score=${hotspot.score}`);
|
|
4318
|
+
for (const factor of hotspot.topFactors) {
|
|
4319
|
+
lines.push(
|
|
4320
|
+
` factor: ${factor.label} contribution=${factor.contribution} confidence=${factor.confidence}`
|
|
4321
|
+
);
|
|
4322
|
+
lines.push(` evidence: ${factor.evidence}`);
|
|
4323
|
+
}
|
|
4324
|
+
lines.push(` actions: ${hotspot.suggestedActions.join(" | ") || "none"}`);
|
|
4325
|
+
lines.push(` levers: ${hotspot.biggestLevers.join(" | ") || "none"}`);
|
|
4326
|
+
}
|
|
4327
|
+
lines.push("");
|
|
4328
|
+
lines.push("Structural Observations");
|
|
4329
|
+
lines.push(` cycleCount: ${report.structural.cycleCount}`);
|
|
4330
|
+
lines.push(` cycles: ${report.structural.cycles.join(" ; ") || "none"}`);
|
|
4331
|
+
lines.push(` fragileClusters: ${report.structural.fragileClusters.length}`);
|
|
4332
|
+
lines.push("");
|
|
4333
|
+
lines.push("External Exposure");
|
|
4334
|
+
if (!report.external.available) {
|
|
4335
|
+
lines.push(` unavailable: ${report.external.reason}`);
|
|
4336
|
+
} else {
|
|
4337
|
+
lines.push(` highRiskDependencies: ${report.external.highRiskDependencies.join(", ") || "none"}`);
|
|
4338
|
+
lines.push(
|
|
4339
|
+
` highRiskDevelopmentDependencies: ${report.external.highRiskDevelopmentDependencies.join(", ") || "none"}`
|
|
4340
|
+
);
|
|
4341
|
+
lines.push(
|
|
4342
|
+
` singleMaintainerDependencies: ${report.external.singleMaintainerDependencies.join(", ") || "none"}`
|
|
4343
|
+
);
|
|
4344
|
+
lines.push(` abandonedDependencies: ${report.external.abandonedDependencies.join(", ") || "none"}`);
|
|
4345
|
+
}
|
|
4346
|
+
lines.push("");
|
|
4347
|
+
lines.push("Appendix");
|
|
4348
|
+
lines.push(` snapshotSchemaVersion: ${report.appendix.snapshotSchemaVersion}`);
|
|
4349
|
+
lines.push(` riskModelVersion: ${report.appendix.riskModelVersion}`);
|
|
4350
|
+
lines.push(` timestamp: ${report.appendix.timestamp}`);
|
|
4351
|
+
lines.push(` normalization: ${report.appendix.normalization}`);
|
|
4352
|
+
lines.push(...renderTextDiff(report));
|
|
4353
|
+
return lines.join("\n");
|
|
4354
|
+
};
|
|
4355
|
+
var renderMarkdownDiff = (report) => {
|
|
4356
|
+
if (report.diff === void 0) {
|
|
4357
|
+
return [];
|
|
4358
|
+
}
|
|
4359
|
+
return [
|
|
4360
|
+
"",
|
|
4361
|
+
"## Diff",
|
|
4362
|
+
`- repositoryScoreDelta: \`${report.diff.repositoryScoreDelta}\``,
|
|
4363
|
+
`- normalizedScoreDelta: \`${report.diff.normalizedScoreDelta}\``,
|
|
4364
|
+
`- newHotspots: ${report.diff.newHotspots.map((item) => `\`${item}\``).join(", ") || "none"}`,
|
|
4365
|
+
`- resolvedHotspots: ${report.diff.resolvedHotspots.map((item) => `\`${item}\``).join(", ") || "none"}`,
|
|
4366
|
+
`- newCycles: ${report.diff.newCycles.map((item) => `\`${item}\``).join(", ") || "none"}`,
|
|
4367
|
+
`- resolvedCycles: ${report.diff.resolvedCycles.map((item) => `\`${item}\``).join(", ") || "none"}`
|
|
4368
|
+
];
|
|
4369
|
+
};
|
|
4370
|
+
var renderMarkdownReport = (report) => {
|
|
4371
|
+
const lines = [];
|
|
4372
|
+
lines.push("# CodeSentinel Report");
|
|
4373
|
+
lines.push("");
|
|
4374
|
+
lines.push("## Repository Summary");
|
|
4375
|
+
lines.push(`- target: \`${report.repository.targetPath}\``);
|
|
4376
|
+
lines.push(`- repositoryScore: \`${report.repository.repositoryScore}\``);
|
|
4377
|
+
lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
|
|
4378
|
+
lines.push(`- riskTier: \`${report.repository.riskTier}\``);
|
|
4379
|
+
lines.push(`- confidence: \`${report.repository.confidence ?? "n/a"}\``);
|
|
4380
|
+
lines.push("");
|
|
4381
|
+
lines.push("## Top Hotspots");
|
|
4382
|
+
for (const hotspot of report.hotspots) {
|
|
4383
|
+
lines.push(`- **${hotspot.target}** (score: \`${hotspot.score}\`)`);
|
|
4384
|
+
lines.push(` - Top factors:`);
|
|
4385
|
+
for (const factor of hotspot.topFactors) {
|
|
4386
|
+
lines.push(
|
|
4387
|
+
` - ${factor.label}: contribution=\`${factor.contribution}\`, confidence=\`${factor.confidence}\``
|
|
4388
|
+
);
|
|
4389
|
+
lines.push(` - evidence: \`${factor.evidence}\``);
|
|
4390
|
+
}
|
|
4391
|
+
lines.push(` - Suggested actions: ${hotspot.suggestedActions.join(" | ") || "none"}`);
|
|
4392
|
+
lines.push(` - Biggest levers: ${hotspot.biggestLevers.join(" | ") || "none"}`);
|
|
4393
|
+
}
|
|
4394
|
+
lines.push("");
|
|
4395
|
+
lines.push("## Structural Observations");
|
|
4396
|
+
lines.push(`- cycles detected: \`${report.structural.cycleCount}\``);
|
|
4397
|
+
lines.push(`- cycles: ${report.structural.cycles.map((cycle) => `\`${cycle}\``).join(", ") || "none"}`);
|
|
4398
|
+
lines.push(`- fragile clusters: \`${report.structural.fragileClusters.length}\``);
|
|
4399
|
+
lines.push("");
|
|
4400
|
+
lines.push("## External Exposure Summary");
|
|
4401
|
+
if (!report.external.available) {
|
|
4402
|
+
lines.push(`- unavailable: \`${report.external.reason}\``);
|
|
4403
|
+
} else {
|
|
4404
|
+
lines.push(`- high-risk dependencies: ${report.external.highRiskDependencies.map((item) => `\`${item}\``).join(", ") || "none"}`);
|
|
4405
|
+
lines.push(
|
|
4406
|
+
`- high-risk development dependencies: ${report.external.highRiskDevelopmentDependencies.map((item) => `\`${item}\``).join(", ") || "none"}`
|
|
4407
|
+
);
|
|
4408
|
+
lines.push(
|
|
4409
|
+
`- single maintainer dependencies: ${report.external.singleMaintainerDependencies.map((item) => `\`${item}\``).join(", ") || "none"}`
|
|
4410
|
+
);
|
|
4411
|
+
lines.push(`- abandoned dependencies: ${report.external.abandonedDependencies.map((item) => `\`${item}\``).join(", ") || "none"}`);
|
|
4412
|
+
}
|
|
4413
|
+
lines.push("");
|
|
4414
|
+
lines.push("## Appendix");
|
|
4415
|
+
lines.push(`- snapshot schema: \`${report.appendix.snapshotSchemaVersion}\``);
|
|
4416
|
+
lines.push(`- risk model version: \`${report.appendix.riskModelVersion}\``);
|
|
4417
|
+
lines.push(`- timestamp: \`${report.appendix.timestamp}\``);
|
|
4418
|
+
lines.push(`- normalization: ${report.appendix.normalization}`);
|
|
4419
|
+
lines.push(...renderMarkdownDiff(report));
|
|
4420
|
+
return lines.join("\n");
|
|
4421
|
+
};
|
|
4422
|
+
var createSnapshot = (input) => ({
|
|
4423
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
4424
|
+
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4425
|
+
riskModelVersion: RISK_MODEL_VERSION,
|
|
4426
|
+
source: {
|
|
4427
|
+
targetPath: input.analysis.structural.targetPath
|
|
4428
|
+
},
|
|
4429
|
+
analysis: input.analysis,
|
|
4430
|
+
...input.trace === void 0 ? {} : { trace: input.trace },
|
|
4431
|
+
...input.analysisConfig === void 0 ? {} : { analysisConfig: input.analysisConfig }
|
|
4432
|
+
});
|
|
4433
|
+
var parseSnapshot = (raw) => {
|
|
4434
|
+
const parsed = JSON.parse(raw);
|
|
4435
|
+
if (parsed.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
|
|
4436
|
+
throw new Error("unsupported_snapshot_schema");
|
|
4437
|
+
}
|
|
4438
|
+
if (typeof parsed.generatedAt !== "string") {
|
|
4439
|
+
throw new Error("invalid_snapshot_generated_at");
|
|
4440
|
+
}
|
|
4441
|
+
if (parsed.analysis === void 0 || parsed.analysis === null) {
|
|
4442
|
+
throw new Error("invalid_snapshot_analysis");
|
|
4443
|
+
}
|
|
4444
|
+
if (parsed.source === void 0 || typeof parsed.source.targetPath !== "string") {
|
|
4445
|
+
throw new Error("invalid_snapshot_source");
|
|
4446
|
+
}
|
|
4447
|
+
return parsed;
|
|
4448
|
+
};
|
|
4449
|
+
var formatReport = (report, format) => {
|
|
4450
|
+
if (format === "json") {
|
|
4451
|
+
return JSON.stringify(report, null, 2);
|
|
4452
|
+
}
|
|
4453
|
+
if (format === "md") {
|
|
4454
|
+
return renderMarkdownReport(report);
|
|
4455
|
+
}
|
|
4456
|
+
return renderTextReport(report);
|
|
4457
|
+
};
|
|
4458
|
+
|
|
4459
|
+
// src/application/run-report-command.ts
|
|
4460
|
+
var buildSnapshot = async (inputPath, authorIdentityMode, includeTrace, logger) => {
|
|
4461
|
+
const analysisInputs = await collectAnalysisInputs(inputPath, authorIdentityMode, logger);
|
|
4462
|
+
const evaluation = evaluateRepositoryRisk(analysisInputs, { explain: includeTrace });
|
|
4463
|
+
const summary = {
|
|
4464
|
+
...analysisInputs,
|
|
4465
|
+
risk: evaluation.summary
|
|
4466
|
+
};
|
|
4467
|
+
return createSnapshot({
|
|
4468
|
+
analysis: summary,
|
|
4469
|
+
...evaluation.trace === void 0 ? {} : { trace: evaluation.trace },
|
|
4470
|
+
analysisConfig: {
|
|
4471
|
+
authorIdentityMode,
|
|
4472
|
+
includeTrace
|
|
4473
|
+
}
|
|
4474
|
+
});
|
|
4475
|
+
};
|
|
4476
|
+
var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
|
|
4477
|
+
logger.info("building analysis snapshot");
|
|
4478
|
+
const current = await buildSnapshot(inputPath, authorIdentityMode, options.includeTrace, logger);
|
|
4479
|
+
if (options.snapshotPath !== void 0) {
|
|
4480
|
+
await writeFile(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
|
|
4481
|
+
logger.info(`snapshot written: ${options.snapshotPath}`);
|
|
4482
|
+
}
|
|
4483
|
+
let report;
|
|
4484
|
+
if (options.comparePath === void 0) {
|
|
4485
|
+
report = createReport(current);
|
|
4486
|
+
} else {
|
|
4487
|
+
logger.info(`loading baseline snapshot: ${options.comparePath}`);
|
|
4488
|
+
const baselineRaw = await readFile(options.comparePath, "utf8");
|
|
4489
|
+
const baseline = parseSnapshot(baselineRaw);
|
|
4490
|
+
const diff = compareSnapshots(current, baseline);
|
|
4491
|
+
report = createReport(current, diff);
|
|
4492
|
+
}
|
|
4493
|
+
const rendered = formatReport(report, options.format);
|
|
4494
|
+
if (options.outputPath !== void 0) {
|
|
4495
|
+
await writeFile(options.outputPath, rendered, "utf8");
|
|
4496
|
+
logger.info(`report written: ${options.outputPath}`);
|
|
4497
|
+
}
|
|
4498
|
+
return { report, rendered };
|
|
4499
|
+
};
|
|
4500
|
+
|
|
4033
4501
|
// src/application/run-explain-command.ts
|
|
4034
4502
|
var selectTargets = (trace, summary, options) => {
|
|
4035
4503
|
if (options.file !== void 0) {
|
|
@@ -4163,6 +4631,39 @@ program.command("dependency-risk").argument("<dependency>", "dependency spec to
|
|
|
4163
4631
|
`);
|
|
4164
4632
|
}
|
|
4165
4633
|
);
|
|
4634
|
+
program.command("report").argument("[path]", "path to the project to analyze").addOption(
|
|
4635
|
+
new Option(
|
|
4636
|
+
"--author-identity <mode>",
|
|
4637
|
+
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
4638
|
+
).choices(["likely_merge", "strict_email"]).default("likely_merge")
|
|
4639
|
+
).addOption(
|
|
4640
|
+
new Option(
|
|
4641
|
+
"--log-level <level>",
|
|
4642
|
+
"log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
|
|
4643
|
+
).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
|
|
4644
|
+
).addOption(
|
|
4645
|
+
new Option("--format <mode>", "output format: text, json, md").choices(["text", "json", "md"]).default("text")
|
|
4646
|
+
).option("--output <path>", "write rendered report to a file path").option("--compare <baseline>", "compare against a baseline snapshot JSON file").option("--snapshot <path>", "write current snapshot JSON artifact").option("--no-trace", "disable trace embedding in generated snapshot").action(
|
|
4647
|
+
async (path, options) => {
|
|
4648
|
+
const logger = createStderrLogger(options.logLevel);
|
|
4649
|
+
const result = await runReportCommand(
|
|
4650
|
+
path,
|
|
4651
|
+
options.authorIdentity,
|
|
4652
|
+
{
|
|
4653
|
+
format: options.format,
|
|
4654
|
+
...options.output === void 0 ? {} : { outputPath: options.output },
|
|
4655
|
+
...options.compare === void 0 ? {} : { comparePath: options.compare },
|
|
4656
|
+
...options.snapshot === void 0 ? {} : { snapshotPath: options.snapshot },
|
|
4657
|
+
includeTrace: options.trace
|
|
4658
|
+
},
|
|
4659
|
+
logger
|
|
4660
|
+
);
|
|
4661
|
+
if (options.output === void 0) {
|
|
4662
|
+
process.stdout.write(`${result.rendered}
|
|
4663
|
+
`);
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
);
|
|
4166
4667
|
if (process.argv.length <= 2) {
|
|
4167
4668
|
program.outputHelp();
|
|
4168
4669
|
process.exit(0);
|