@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 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);