@getcodesentinel/codesentinel 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1656,6 +1656,7 @@ var createReport = (snapshot, diff) => {
1656
1656
  confidence: repositoryConfidence(snapshot),
1657
1657
  dimensionScores: repositoryDimensionScores(snapshot)
1658
1658
  },
1659
+ quality: snapshot.analysis.quality,
1659
1660
  hotspots: hotspotItems(snapshot),
1660
1661
  structural: {
1661
1662
  cycleCount: snapshot.analysis.structural.metrics.cycleCount,
@@ -1727,6 +1728,22 @@ var renderTextReport = (report) => {
1727
1728
  lines.push(` external: ${report.repository.dimensionScores.external ?? "n/a"}`);
1728
1729
  lines.push(` interactions: ${report.repository.dimensionScores.interactions ?? "n/a"}`);
1729
1730
  lines.push("");
1731
+ lines.push("Quality Summary");
1732
+ lines.push(` qualityScore: ${report.quality.qualityScore}`);
1733
+ lines.push(` normalizedScore: ${report.quality.normalizedScore}`);
1734
+ lines.push(` modularity: ${report.quality.dimensions.modularity}`);
1735
+ lines.push(` changeHygiene: ${report.quality.dimensions.changeHygiene}`);
1736
+ lines.push(` testHealth: ${report.quality.dimensions.testHealth}`);
1737
+ lines.push(" topIssues:");
1738
+ for (const issue of report.quality.topIssues.slice(0, 5)) {
1739
+ lines.push(
1740
+ ` - [${issue.severity}] (${issue.dimension}) ${issue.id} @ ${issue.target}: ${issue.message}`
1741
+ );
1742
+ }
1743
+ if (report.quality.topIssues.length === 0) {
1744
+ lines.push(" - none");
1745
+ }
1746
+ lines.push("");
1730
1747
  lines.push("Top Hotspots");
1731
1748
  for (const hotspot of report.hotspots) {
1732
1749
  lines.push(` - ${hotspot.target} | score=${hotspot.score}`);
@@ -1802,6 +1819,23 @@ var renderMarkdownReport = (report) => {
1802
1819
  lines.push(`- external: \`${report.repository.dimensionScores.external ?? "n/a"}\``);
1803
1820
  lines.push(`- interactions: \`${report.repository.dimensionScores.interactions ?? "n/a"}\``);
1804
1821
  lines.push("");
1822
+ lines.push("## Quality Summary");
1823
+ lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
1824
+ lines.push(`- normalizedScore: \`${report.quality.normalizedScore}\``);
1825
+ lines.push(`- modularity: \`${report.quality.dimensions.modularity}\``);
1826
+ lines.push(`- changeHygiene: \`${report.quality.dimensions.changeHygiene}\``);
1827
+ lines.push(`- testHealth: \`${report.quality.dimensions.testHealth}\``);
1828
+ if (report.quality.topIssues.length === 0) {
1829
+ lines.push("- top issues: none");
1830
+ } else {
1831
+ lines.push("- top issues:");
1832
+ for (const issue of report.quality.topIssues.slice(0, 5)) {
1833
+ lines.push(
1834
+ ` - [${issue.severity}] \`${issue.id}\` (\`${issue.dimension}\`) @ \`${issue.target}\`: ${issue.message}`
1835
+ );
1836
+ }
1837
+ }
1838
+ lines.push("");
1805
1839
  lines.push("## Top Hotspots");
1806
1840
  for (const hotspot of report.hotspots) {
1807
1841
  lines.push(`- **${hotspot.target}** (score: \`${hotspot.score}\`)`);
@@ -2459,7 +2493,7 @@ var resolveAutoBaselineRef = async (input) => {
2459
2493
 
2460
2494
  // src/index.ts
2461
2495
  import { readFileSync as readFileSync2 } from "fs";
2462
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
2496
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
2463
2497
  import { dirname as dirname2, resolve as resolve5 } from "path";
2464
2498
  import { fileURLToPath } from "url";
2465
2499
 
@@ -2500,6 +2534,12 @@ var createSummaryShape = (summary) => ({
2500
2534
  })),
2501
2535
  fragileClusterCount: summary.risk.fragileClusters.length,
2502
2536
  dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
2537
+ },
2538
+ quality: {
2539
+ qualityScore: summary.quality.qualityScore,
2540
+ normalizedScore: summary.quality.normalizedScore,
2541
+ dimensions: summary.quality.dimensions,
2542
+ topIssues: summary.quality.topIssues.slice(0, 5)
2503
2543
  }
2504
2544
  });
2505
2545
  var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
@@ -3176,7 +3216,8 @@ var checkForCliUpdates = async (input) => {
3176
3216
  };
3177
3217
 
3178
3218
  // src/application/run-analyze-command.ts
3179
- import { resolve as resolve3 } from "path";
3219
+ import { readFile as readFile2 } from "fs/promises";
3220
+ import { join as join4, resolve as resolve3 } from "path";
3180
3221
 
3181
3222
  // ../code-graph/dist/index.js
3182
3223
  import { extname, isAbsolute, relative, resolve as resolve2 } from "path";
@@ -4224,6 +4265,208 @@ var analyzeRepositoryEvolutionFromGit = (input, onProgress) => {
4224
4265
  return analyzeRepositoryEvolution(input, historyProvider, onProgress);
4225
4266
  };
4226
4267
 
4268
+ // ../quality-engine/dist/index.js
4269
+ var clamp01 = (value) => {
4270
+ if (!Number.isFinite(value)) {
4271
+ return 0;
4272
+ }
4273
+ if (value <= 0) {
4274
+ return 0;
4275
+ }
4276
+ if (value >= 1) {
4277
+ return 1;
4278
+ }
4279
+ return value;
4280
+ };
4281
+ var round45 = (value) => Number(value.toFixed(4));
4282
+ var average = (values) => {
4283
+ if (values.length === 0) {
4284
+ return 0;
4285
+ }
4286
+ const total = values.reduce((sum, value) => sum + value, 0);
4287
+ return total / values.length;
4288
+ };
4289
+ var concentration = (rawValues) => {
4290
+ const values = rawValues.filter((value) => value > 0);
4291
+ const count = values.length;
4292
+ if (count <= 1) {
4293
+ return 0;
4294
+ }
4295
+ const total = values.reduce((sum, value) => sum + value, 0);
4296
+ if (total <= 0) {
4297
+ return 0;
4298
+ }
4299
+ const hhi = values.reduce((sum, value) => {
4300
+ const share = value / total;
4301
+ return sum + share * share;
4302
+ }, 0);
4303
+ const minHhi = 1 / count;
4304
+ const normalized = (hhi - minHhi) / (1 - minHhi);
4305
+ return clamp01(normalized);
4306
+ };
4307
+ var DIMENSION_WEIGHTS = {
4308
+ modularity: 0.45,
4309
+ changeHygiene: 0.35,
4310
+ testHealth: 0.2
4311
+ };
4312
+ var TODO_FIXME_MAX_IMPACT = 0.08;
4313
+ var toPercentage = (normalizedQuality) => round45(clamp01(normalizedQuality) * 100);
4314
+ var filePaths = (structural) => structural.files.map((file) => file.relativePath);
4315
+ var isTestPath = (path) => {
4316
+ const normalized = path.toLowerCase();
4317
+ return normalized.includes("/__tests__/") || normalized.includes("\\__tests__\\") || normalized.includes(".test.") || normalized.includes(".spec.");
4318
+ };
4319
+ var isSourcePath = (path) => {
4320
+ if (path.endsWith(".d.ts")) {
4321
+ return false;
4322
+ }
4323
+ return !isTestPath(path);
4324
+ };
4325
+ var pushIssue = (issues, issue) => {
4326
+ issues.push({
4327
+ ...issue,
4328
+ severity: issue.severity ?? "warn"
4329
+ });
4330
+ };
4331
+ var computeRepositoryQualitySummary = (input) => {
4332
+ const issues = [];
4333
+ const sourceFileSet = new Set(input.structural.files.map((file) => file.relativePath));
4334
+ const cycleCount = input.structural.metrics.cycleCount;
4335
+ const cycleSizeAverage = input.structural.cycles.length === 0 ? 0 : average(input.structural.cycles.map((cycle) => cycle.nodes.length));
4336
+ const cyclePenalty = clamp01(cycleCount / 6) * 0.7 + clamp01((cycleSizeAverage - 2) / 8) * 0.3;
4337
+ if (cycleCount > 0) {
4338
+ pushIssue(issues, {
4339
+ id: "quality.modularity.structural_cycles",
4340
+ dimension: "modularity",
4341
+ target: input.structural.cycles[0]?.nodes.slice().sort((a, b) => a.localeCompare(b)).join(" -> ") ?? input.structural.targetPath,
4342
+ message: `${cycleCount} structural cycle(s) increase coupling and refactor cost.`,
4343
+ severity: cycleCount >= 3 ? "error" : "warn",
4344
+ impact: round45(cyclePenalty * 0.6)
4345
+ });
4346
+ }
4347
+ const fanInConcentration = concentration(input.structural.files.map((file) => file.fanIn));
4348
+ const fanOutConcentration = concentration(input.structural.files.map((file) => file.fanOut));
4349
+ const centralityConcentration = average([fanInConcentration, fanOutConcentration]);
4350
+ if (centralityConcentration >= 0.5) {
4351
+ const hottest = [...input.structural.files].map((file) => ({
4352
+ path: file.relativePath,
4353
+ pressure: file.fanIn + file.fanOut
4354
+ })).sort((a, b) => b.pressure - a.pressure || a.path.localeCompare(b.path))[0];
4355
+ pushIssue(issues, {
4356
+ id: "quality.modularity.centrality_concentration",
4357
+ dimension: "modularity",
4358
+ target: hottest?.path ?? input.structural.targetPath,
4359
+ message: "Fan-in/fan-out pressure is concentrated in a small set of files.",
4360
+ impact: round45(centralityConcentration * 0.5)
4361
+ });
4362
+ }
4363
+ let churnConcentration = 0;
4364
+ let volatilityConcentration = 0;
4365
+ let couplingDensity = 0;
4366
+ let couplingIntensity = 0;
4367
+ if (input.evolution.available) {
4368
+ const evolutionSourceFiles = input.evolution.files.filter(
4369
+ (file) => sourceFileSet.has(file.filePath)
4370
+ );
4371
+ churnConcentration = concentration(evolutionSourceFiles.map((file) => file.churnTotal));
4372
+ volatilityConcentration = concentration(
4373
+ evolutionSourceFiles.map((file) => file.recentVolatility)
4374
+ );
4375
+ const fileCount = Math.max(1, evolutionSourceFiles.length);
4376
+ const maxPairs = fileCount * (fileCount - 1) / 2;
4377
+ const sourcePairs = input.evolution.coupling.pairs.filter(
4378
+ (pair) => sourceFileSet.has(pair.fileA) && sourceFileSet.has(pair.fileB)
4379
+ );
4380
+ couplingDensity = maxPairs <= 0 ? 0 : clamp01(sourcePairs.length / maxPairs);
4381
+ couplingIntensity = average(sourcePairs.map((pair) => pair.couplingScore));
4382
+ if (churnConcentration >= 0.45) {
4383
+ const mostChurn = [...evolutionSourceFiles].sort(
4384
+ (a, b) => b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)
4385
+ )[0];
4386
+ pushIssue(issues, {
4387
+ id: "quality.change_hygiene.churn_concentration",
4388
+ dimension: "changeHygiene",
4389
+ target: mostChurn?.filePath ?? input.structural.targetPath,
4390
+ message: "Churn is concentrated in a narrow part of the codebase.",
4391
+ impact: round45(churnConcentration * 0.45)
4392
+ });
4393
+ }
4394
+ if (volatilityConcentration >= 0.45) {
4395
+ const volatileFile = [...evolutionSourceFiles].sort(
4396
+ (a, b) => b.recentVolatility - a.recentVolatility || a.filePath.localeCompare(b.filePath)
4397
+ )[0];
4398
+ pushIssue(issues, {
4399
+ id: "quality.change_hygiene.volatility_concentration",
4400
+ dimension: "changeHygiene",
4401
+ target: volatileFile?.filePath ?? input.structural.targetPath,
4402
+ message: "Recent volatility is concentrated in files that change frequently.",
4403
+ impact: round45(volatilityConcentration * 0.4)
4404
+ });
4405
+ }
4406
+ if (couplingDensity >= 0.35 || couplingIntensity >= 0.45) {
4407
+ const strongestPair = [...sourcePairs].sort(
4408
+ (a, b) => b.couplingScore - a.couplingScore || `${a.fileA}|${a.fileB}`.localeCompare(`${b.fileA}|${b.fileB}`)
4409
+ )[0];
4410
+ pushIssue(issues, {
4411
+ id: "quality.change_hygiene.coupling_density",
4412
+ dimension: "changeHygiene",
4413
+ target: strongestPair === void 0 ? input.structural.targetPath : `${strongestPair.fileA}<->${strongestPair.fileB}`,
4414
+ message: "Co-change relationships are dense, increasing coordination overhead.",
4415
+ impact: round45(average([couplingDensity, couplingIntensity]) * 0.35)
4416
+ });
4417
+ }
4418
+ }
4419
+ const modularityPenalty = clamp01(cyclePenalty * 0.55 + centralityConcentration * 0.45);
4420
+ const changeHygienePenalty = input.evolution.available ? clamp01(
4421
+ churnConcentration * 0.4 + volatilityConcentration * 0.35 + couplingDensity * 0.15 + couplingIntensity * 0.1
4422
+ ) : 0.25;
4423
+ const paths = filePaths(input.structural);
4424
+ const testFiles = paths.filter((path) => isTestPath(path)).length;
4425
+ const sourceFiles = paths.filter((path) => isSourcePath(path)).length;
4426
+ const testRatio = sourceFiles <= 0 ? 1 : testFiles / sourceFiles;
4427
+ const testPresencePenalty = sourceFiles <= 0 ? 0 : 1 - clamp01(testRatio / 0.3);
4428
+ if (sourceFiles > 0 && testRatio < 0.2) {
4429
+ pushIssue(issues, {
4430
+ id: "quality.test_health.low_test_presence",
4431
+ dimension: "testHealth",
4432
+ target: input.structural.targetPath,
4433
+ message: `Detected ${testFiles} test file(s) for ${sourceFiles} source file(s).`,
4434
+ severity: testRatio === 0 ? "error" : "warn",
4435
+ impact: round45(testPresencePenalty * 0.5)
4436
+ });
4437
+ }
4438
+ const todoFixmeCount = Math.max(0, input.todoFixmeCount ?? 0);
4439
+ const todoFixmePenalty = clamp01(todoFixmeCount / 120) * TODO_FIXME_MAX_IMPACT;
4440
+ if (todoFixmeCount > 0) {
4441
+ pushIssue(issues, {
4442
+ id: "quality.change_hygiene.todo_fixme_load",
4443
+ dimension: "changeHygiene",
4444
+ target: input.structural.targetPath,
4445
+ message: `Found ${todoFixmeCount} TODO/FIXME marker(s); cleanup debt is accumulating.`,
4446
+ impact: round45(todoFixmePenalty * 0.2)
4447
+ });
4448
+ }
4449
+ const modularityQuality = clamp01(1 - modularityPenalty);
4450
+ const changeHygieneQuality = clamp01(1 - clamp01(changeHygienePenalty + todoFixmePenalty));
4451
+ const testHealthQuality = clamp01(1 - testPresencePenalty);
4452
+ const normalizedScore = clamp01(
4453
+ modularityQuality * DIMENSION_WEIGHTS.modularity + changeHygieneQuality * DIMENSION_WEIGHTS.changeHygiene + testHealthQuality * DIMENSION_WEIGHTS.testHealth
4454
+ );
4455
+ const topIssues = [...issues].sort(
4456
+ (a, b) => b.impact - a.impact || a.id.localeCompare(b.id) || a.target.localeCompare(b.target)
4457
+ ).slice(0, 8).map(({ impact: _impact, ...issue }) => issue);
4458
+ return {
4459
+ qualityScore: toPercentage(normalizedScore),
4460
+ normalizedScore: round45(normalizedScore),
4461
+ dimensions: {
4462
+ modularity: toPercentage(modularityQuality),
4463
+ changeHygiene: toPercentage(changeHygieneQuality),
4464
+ testHealth: toPercentage(testHealthQuality)
4465
+ },
4466
+ topIssues
4467
+ };
4468
+ };
4469
+
4227
4470
  // ../risk-engine/dist/index.js
4228
4471
  var DEFAULT_RISK_ENGINE_CONFIG = {
4229
4472
  // Base dimensional influence. Risk is never dominated by a single dimension by default.
@@ -4309,8 +4552,8 @@ var DEFAULT_RISK_ENGINE_CONFIG = {
4309
4552
  }
4310
4553
  };
4311
4554
  var toUnitInterval = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
4312
- var round45 = (value) => Number(value.toFixed(4));
4313
- var average = (values) => {
4555
+ var round46 = (value) => Number(value.toFixed(4));
4556
+ var average2 = (values) => {
4314
4557
  if (values.length === 0) {
4315
4558
  return 0;
4316
4559
  }
@@ -4418,14 +4661,14 @@ var computeAggregatorAttenuation = (input) => {
4418
4661
  const fanOutSignal = toUnitInterval((fanOut - config.minFanOut) / Math.max(1, config.minFanOut));
4419
4662
  const lowChurnPerCommitSignal = 1 - toUnitInterval(churnPerCommit / config.maxChurnPerCommit);
4420
4663
  const lowChurnPerDependencySignal = 1 - toUnitInterval(churnPerDependency / config.maxChurnPerDependency);
4421
- const attenuationConfidence = average([
4664
+ const attenuationConfidence = average2([
4422
4665
  fanInSignal,
4423
4666
  fanOutSignal,
4424
4667
  lowChurnPerCommitSignal,
4425
4668
  lowChurnPerDependencySignal
4426
4669
  ]);
4427
4670
  const reduction = toUnitInterval(config.maxStructuralReduction) * attenuationConfidence;
4428
- return round45(toUnitInterval(1 - reduction));
4671
+ return round46(toUnitInterval(1 - reduction));
4429
4672
  };
4430
4673
  var dependencySignalWeights = {
4431
4674
  single_maintainer: 0.3,
@@ -4455,12 +4698,12 @@ var computeDependencySignalScore = (ownSignals, inheritedSignals, inheritedSigna
4455
4698
  }
4456
4699
  return toUnitInterval(weightedTotal / maxWeightedTotal);
4457
4700
  };
4458
- var clampConfidence = (value) => round45(toUnitInterval(value));
4701
+ var clampConfidence = (value) => round46(toUnitInterval(value));
4459
4702
  var computeExternalMetadataConfidence = (external) => {
4460
4703
  if (!external.available) {
4461
4704
  return 0;
4462
4705
  }
4463
- return round45(toUnitInterval(0.35 + external.metrics.metadataCoverage * 0.65));
4706
+ return round46(toUnitInterval(0.35 + external.metrics.metadataCoverage * 0.65));
4464
4707
  };
4465
4708
  var computeEvolutionHistoryConfidence = (structural, evolution, evolutionByFile) => {
4466
4709
  if (!evolution.available) {
@@ -4477,7 +4720,7 @@ var computeEvolutionHistoryConfidence = (structural, evolution, evolutionByFile)
4477
4720
  }
4478
4721
  }
4479
4722
  const coverage = coveredFiles / totalFiles;
4480
- return round45(toUnitInterval(0.3 + coverage * 0.7));
4723
+ return round46(toUnitInterval(0.3 + coverage * 0.7));
4481
4724
  };
4482
4725
  var buildFactorTraces = (totalScore, inputs) => {
4483
4726
  const positiveInputs = inputs.filter((input) => input.strength > 0);
@@ -4515,7 +4758,7 @@ var buildFactorTraces = (totalScore, inputs) => {
4515
4758
  continue;
4516
4759
  }
4517
4760
  if (index === scored.length - 1) {
4518
- const remaining = round45(totalScore - distributed);
4761
+ const remaining = round46(totalScore - distributed);
4519
4762
  traces[traceIndex] = {
4520
4763
  ...existing,
4521
4764
  contribution: Math.max(0, remaining)
@@ -4523,7 +4766,7 @@ var buildFactorTraces = (totalScore, inputs) => {
4523
4766
  distributed += Math.max(0, remaining);
4524
4767
  continue;
4525
4768
  }
4526
- const rounded = round45(current.contribution);
4769
+ const rounded = round46(current.contribution);
4527
4770
  traces[traceIndex] = {
4528
4771
  ...existing,
4529
4772
  contribution: rounded
@@ -4534,15 +4777,15 @@ var buildFactorTraces = (totalScore, inputs) => {
4534
4777
  };
4535
4778
  var buildReductionLevers = (factors) => factors.filter((factor) => factor.contribution > 0).sort((a, b) => b.contribution - a.contribution || a.factorId.localeCompare(b.factorId)).slice(0, 3).map((factor) => ({
4536
4779
  factorId: factor.factorId,
4537
- estimatedImpact: round45(factor.contribution)
4780
+ estimatedImpact: round46(factor.contribution)
4538
4781
  }));
4539
4782
  var buildTargetTrace = (targetType, targetId, totalScore, normalizedScore, factors) => {
4540
4783
  const dominantFactors = [...factors].filter((factor) => factor.contribution > 0).sort((a, b) => b.contribution - a.contribution || a.factorId.localeCompare(b.factorId)).slice(0, 3).map((factor) => factor.factorId);
4541
4784
  return {
4542
4785
  targetType,
4543
4786
  targetId,
4544
- totalScore: round45(totalScore),
4545
- normalizedScore: round45(normalizedScore),
4787
+ totalScore: round46(totalScore),
4788
+ normalizedScore: round46(normalizedScore),
4546
4789
  factors,
4547
4790
  dominantFactors,
4548
4791
  reductionLevers: buildReductionLevers(factors)
@@ -4616,14 +4859,14 @@ var computeDependencyScores = (external, config) => {
4616
4859
  ].filter((value) => value !== null).length;
4617
4860
  const confidence = toUnitInterval((0.5 + availableMetricCount * 0.125) * metadataConfidence);
4618
4861
  dependencyContexts.set(dependency.name, {
4619
- signalScore: round45(signalScore),
4620
- stalenessRisk: round45(stalenessRisk),
4621
- maintainerConcentrationRisk: round45(maintainerConcentrationRisk),
4622
- transitiveBurdenRisk: round45(transitiveBurdenRisk),
4623
- centralityRisk: round45(centralityRisk),
4624
- chainDepthRisk: round45(chainDepthRisk),
4625
- busFactorRisk: round45(busFactorRisk),
4626
- popularityDampener: round45(popularityDampener),
4862
+ signalScore: round46(signalScore),
4863
+ stalenessRisk: round46(stalenessRisk),
4864
+ maintainerConcentrationRisk: round46(maintainerConcentrationRisk),
4865
+ transitiveBurdenRisk: round46(transitiveBurdenRisk),
4866
+ centralityRisk: round46(centralityRisk),
4867
+ chainDepthRisk: round46(chainDepthRisk),
4868
+ busFactorRisk: round46(busFactorRisk),
4869
+ popularityDampener: round46(popularityDampener),
4627
4870
  rawMetrics: {
4628
4871
  daysSinceLastRelease: dependency.daysSinceLastRelease,
4629
4872
  maintainerCount: dependency.maintainerCount,
@@ -4633,12 +4876,12 @@ var computeDependencyScores = (external, config) => {
4633
4876
  busFactor: dependency.busFactor,
4634
4877
  weeklyDownloads: dependency.weeklyDownloads
4635
4878
  },
4636
- confidence: round45(confidence)
4879
+ confidence: round46(confidence)
4637
4880
  });
4638
4881
  return {
4639
4882
  dependency: dependency.name,
4640
- score: round45(normalizedScore * 100),
4641
- normalizedScore: round45(normalizedScore),
4883
+ score: round46(normalizedScore * 100),
4884
+ normalizedScore: round46(normalizedScore),
4642
4885
  ownRiskSignals: dependency.ownRiskSignals,
4643
4886
  inheritedRiskSignals: dependency.inheritedRiskSignals
4644
4887
  };
@@ -4647,7 +4890,7 @@ var computeDependencyScores = (external, config) => {
4647
4890
  );
4648
4891
  const normalizedValues = dependencyScores.map((score) => score.normalizedScore);
4649
4892
  const highDependencyRisk = dependencyScores.length === 0 ? 0 : percentile(normalizedValues, config.externalDimension.topDependencyPercentile);
4650
- const averageDependencyRisk = average(normalizedValues);
4893
+ const averageDependencyRisk = average2(normalizedValues);
4651
4894
  const depthRisk = halfLifeRisk(
4652
4895
  external.metrics.dependencyDepth,
4653
4896
  config.externalDimension.dependencyDepthHalfLife
@@ -4657,7 +4900,7 @@ var computeDependencyScores = (external, config) => {
4657
4900
  );
4658
4901
  return {
4659
4902
  dependencyScores,
4660
- repositoryExternalPressure: round45(repositoryExternalPressure),
4903
+ repositoryExternalPressure: round46(repositoryExternalPressure),
4661
4904
  dependencyContexts
4662
4905
  };
4663
4906
  };
@@ -4718,11 +4961,11 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
4718
4961
  continue;
4719
4962
  }
4720
4963
  files.sort((a, b) => a.localeCompare(b));
4721
- const averageRisk = average(
4964
+ const averageRisk = average2(
4722
4965
  files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
4723
4966
  );
4724
4967
  const cycleSizeRisk = toUnitInterval((files.length - 1) / 5);
4725
- const score = round45(toUnitInterval(averageRisk * 0.75 + cycleSizeRisk * 0.25) * 100);
4968
+ const score = round46(toUnitInterval(averageRisk * 0.75 + cycleSizeRisk * 0.25) * 100);
4726
4969
  cycleClusterCount += 1;
4727
4970
  clusters.push({
4728
4971
  id: `cycle:${cycleClusterCount}`,
@@ -4792,11 +5035,11 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
4792
5035
  const componentPairs = selectedPairs.filter(
4793
5036
  (pair) => fileSet.has(pair.fileA) && fileSet.has(pair.fileB)
4794
5037
  );
4795
- const meanFileRisk = average(
5038
+ const meanFileRisk = average2(
4796
5039
  files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
4797
5040
  );
4798
- const meanCoupling = average(componentPairs.map((pair) => pair.couplingScore));
4799
- const score = round45(toUnitInterval(meanFileRisk * 0.65 + meanCoupling * 0.35) * 100);
5041
+ const meanCoupling = average2(componentPairs.map((pair) => pair.couplingScore));
5042
+ const score = round46(toUnitInterval(meanFileRisk * 0.65 + meanCoupling * 0.35) * 100);
4800
5043
  couplingClusterCount += 1;
4801
5044
  clusters.push({
4802
5045
  id: `coupling:${couplingClusterCount}`,
@@ -4907,21 +5150,21 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
4907
5150
  const normalizedScore = saturatingComposite(baseline, interactions);
4908
5151
  return {
4909
5152
  file: filePath,
4910
- score: round45(normalizedScore * 100),
4911
- normalizedScore: round45(normalizedScore),
5153
+ score: round46(normalizedScore * 100),
5154
+ normalizedScore: round46(normalizedScore),
4912
5155
  factors: {
4913
- structural: round45(structuralFactor),
4914
- evolution: round45(evolutionFactor),
4915
- external: round45(externalFactor)
5156
+ structural: round46(structuralFactor),
5157
+ evolution: round46(evolutionFactor),
5158
+ external: round46(externalFactor)
4916
5159
  },
4917
- structuralCentrality: round45(structuralCentrality),
5160
+ structuralCentrality: round46(structuralCentrality),
4918
5161
  traceTerms: {
4919
- structuralBase: round45(structuralBase),
4920
- evolutionBase: round45(evolutionBase),
4921
- externalBase: round45(externalBase),
4922
- interactionStructuralEvolution: round45(interactionStructuralEvolution),
4923
- interactionCentralInstability: round45(interactionCentralInstability),
4924
- interactionDependencyAmplification: round45(interactionDependencyAmplification)
5162
+ structuralBase: round46(structuralBase),
5163
+ evolutionBase: round46(evolutionBase),
5164
+ externalBase: round46(externalBase),
5165
+ interactionStructuralEvolution: round46(interactionStructuralEvolution),
5166
+ interactionCentralInstability: round46(interactionCentralInstability),
5167
+ interactionDependencyAmplification: round46(interactionDependencyAmplification)
4925
5168
  },
4926
5169
  rawMetrics: {
4927
5170
  fanIn: file.fanIn,
@@ -4933,19 +5176,19 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
4933
5176
  recentVolatility: evolutionMetrics?.recentVolatility ?? null,
4934
5177
  topAuthorShare: evolutionMetrics?.topAuthorShare ?? null,
4935
5178
  busFactor: evolutionMetrics?.busFactor ?? null,
4936
- dependencyAffinity: round45(dependencyAffinity),
4937
- repositoryExternalPressure: round45(dependencyComputation.repositoryExternalPressure),
4938
- structuralAttenuation: round45(structuralAttenuation)
5179
+ dependencyAffinity: round46(dependencyAffinity),
5180
+ repositoryExternalPressure: round46(dependencyComputation.repositoryExternalPressure),
5181
+ structuralAttenuation: round46(structuralAttenuation)
4939
5182
  },
4940
5183
  normalizedMetrics: {
4941
- fanInRisk: round45(fanInRisk),
4942
- fanOutRisk: round45(fanOutRisk),
4943
- depthRisk: round45(depthRisk),
4944
- frequencyRisk: round45(frequencyRisk),
4945
- churnRisk: round45(churnRisk),
4946
- volatilityRisk: round45(volatilityRisk),
4947
- ownershipConcentrationRisk: round45(ownershipConcentrationRisk),
4948
- busFactorRisk: round45(busFactorRisk)
5184
+ fanInRisk: round46(fanInRisk),
5185
+ fanOutRisk: round46(fanOutRisk),
5186
+ depthRisk: round46(depthRisk),
5187
+ frequencyRisk: round46(frequencyRisk),
5188
+ churnRisk: round46(churnRisk),
5189
+ volatilityRisk: round46(volatilityRisk),
5190
+ ownershipConcentrationRisk: round46(ownershipConcentrationRisk),
5191
+ busFactorRisk: round46(busFactorRisk)
4949
5192
  }
4950
5193
  };
4951
5194
  }).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
@@ -5071,29 +5314,29 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5071
5314
  moduleFiles.set(moduleName, values);
5072
5315
  }
5073
5316
  const moduleScores = [...moduleFiles.entries()].map(([module, values]) => {
5074
- const averageScore = average(values);
5317
+ const averageScore = average2(values);
5075
5318
  const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
5076
5319
  const normalizedScore = toUnitInterval(averageScore * 0.65 + peakScore * 0.35);
5077
5320
  return {
5078
5321
  module,
5079
- score: round45(normalizedScore * 100),
5080
- normalizedScore: round45(normalizedScore),
5322
+ score: round46(normalizedScore * 100),
5323
+ normalizedScore: round46(normalizedScore),
5081
5324
  fileCount: values.length
5082
5325
  };
5083
5326
  }).sort((a, b) => b.score - a.score || a.module.localeCompare(b.module));
5084
5327
  if (collector !== void 0) {
5085
5328
  for (const [module, values] of moduleFiles.entries()) {
5086
- const averageScore = average(values);
5329
+ const averageScore = average2(values);
5087
5330
  const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
5088
5331
  const normalizedScore = toUnitInterval(averageScore * 0.65 + peakScore * 0.35);
5089
- const totalScore = round45(normalizedScore * 100);
5332
+ const totalScore = round46(normalizedScore * 100);
5090
5333
  const factors = buildFactorTraces(totalScore, [
5091
5334
  {
5092
5335
  factorId: "module.average_file_risk",
5093
5336
  family: "composite",
5094
5337
  strength: averageScore * 0.65,
5095
- rawMetrics: { averageFileRisk: round45(averageScore), fileCount: values.length },
5096
- normalizedMetrics: { normalizedModuleRisk: round45(normalizedScore) },
5338
+ rawMetrics: { averageFileRisk: round46(averageScore), fileCount: values.length },
5339
+ normalizedMetrics: { normalizedModuleRisk: round46(normalizedScore) },
5097
5340
  weight: 0.65,
5098
5341
  amplification: null,
5099
5342
  evidence: [{ kind: "repository_metric", metric: "moduleAggregation.average" }],
@@ -5103,8 +5346,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5103
5346
  factorId: "module.peak_file_risk",
5104
5347
  family: "composite",
5105
5348
  strength: peakScore * 0.35,
5106
- rawMetrics: { peakFileRisk: round45(peakScore), fileCount: values.length },
5107
- normalizedMetrics: { normalizedModuleRisk: round45(normalizedScore) },
5349
+ rawMetrics: { peakFileRisk: round46(peakScore), fileCount: values.length },
5350
+ normalizedMetrics: { normalizedModuleRisk: round46(normalizedScore) },
5108
5351
  weight: 0.35,
5109
5352
  amplification: null,
5110
5353
  evidence: [{ kind: "repository_metric", metric: "moduleAggregation.peak" }],
@@ -5127,12 +5370,12 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5127
5370
  const normalizedZoneScore = toUnitInterval(intensity * 0.7 + fileScore.normalizedScore * 0.3);
5128
5371
  return {
5129
5372
  file: fileScore.file,
5130
- score: round45(normalizedZoneScore * 100),
5373
+ score: round46(normalizedZoneScore * 100),
5131
5374
  externalPressure: fileScore.factors.external
5132
5375
  };
5133
5376
  }).filter((zone) => external.available && zone.externalPressure >= pressureThreshold).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, config.amplificationZone.maxZones).map((zone) => ({
5134
5377
  ...zone,
5135
- externalPressure: round45(zone.externalPressure)
5378
+ externalPressure: round46(zone.externalPressure)
5136
5379
  }));
5137
5380
  if (collector !== void 0 && external.available) {
5138
5381
  const dependencyByName = new Map(
@@ -5245,16 +5488,16 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5245
5488
  );
5246
5489
  }
5247
5490
  }
5248
- const structuralDimension = average(fileScores.map((fileScore) => fileScore.factors.structural));
5249
- const evolutionDimension = average(fileScores.map((fileScore) => fileScore.factors.evolution));
5491
+ const structuralDimension = average2(fileScores.map((fileScore) => fileScore.factors.structural));
5492
+ const evolutionDimension = average2(fileScores.map((fileScore) => fileScore.factors.evolution));
5250
5493
  const externalDimension = dependencyComputation.repositoryExternalPressure;
5251
5494
  const topCentralSlice = Math.max(1, Math.ceil(fileRiskContexts.length * 0.1));
5252
- const criticalInstability = average(
5495
+ const criticalInstability = average2(
5253
5496
  [...fileRiskContexts].sort(
5254
5497
  (a, b) => b.structuralCentrality * b.factors.evolution - a.structuralCentrality * a.factors.evolution || a.file.localeCompare(b.file)
5255
5498
  ).slice(0, topCentralSlice).map((context) => context.structuralCentrality * context.factors.evolution)
5256
5499
  );
5257
- const dependencyAmplification = average(
5500
+ const dependencyAmplification = average2(
5258
5501
  dependencyAmplificationZones.map(
5259
5502
  (zone) => toUnitInterval(zone.externalPressure * zone.score / 100)
5260
5503
  )
@@ -5265,15 +5508,15 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5265
5508
  criticalInstability * config.interactionWeights.centralInstability,
5266
5509
  dependencyAmplification * config.interactionWeights.dependencyAmplification
5267
5510
  ]);
5268
- const riskScore = round45(repositoryNormalizedScore * 100);
5511
+ const riskScore = round46(repositoryNormalizedScore * 100);
5269
5512
  if (collector !== void 0) {
5270
5513
  const repositoryFactors = buildFactorTraces(riskScore, [
5271
5514
  {
5272
5515
  factorId: "repository.structural",
5273
5516
  family: "structural",
5274
5517
  strength: structuralDimension * dimensionWeights.structural,
5275
- rawMetrics: { structuralDimension: round45(structuralDimension) },
5276
- normalizedMetrics: { dimensionWeight: round45(dimensionWeights.structural) },
5518
+ rawMetrics: { structuralDimension: round46(structuralDimension) },
5519
+ normalizedMetrics: { dimensionWeight: round46(dimensionWeights.structural) },
5277
5520
  weight: dimensionWeights.structural,
5278
5521
  amplification: null,
5279
5522
  evidence: [{ kind: "repository_metric", metric: "structuralDimension" }],
@@ -5283,8 +5526,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5283
5526
  factorId: "repository.evolution",
5284
5527
  family: "evolution",
5285
5528
  strength: evolutionDimension * dimensionWeights.evolution,
5286
- rawMetrics: { evolutionDimension: round45(evolutionDimension) },
5287
- normalizedMetrics: { dimensionWeight: round45(dimensionWeights.evolution) },
5529
+ rawMetrics: { evolutionDimension: round46(evolutionDimension) },
5530
+ normalizedMetrics: { dimensionWeight: round46(dimensionWeights.evolution) },
5288
5531
  weight: dimensionWeights.evolution,
5289
5532
  amplification: null,
5290
5533
  evidence: [{ kind: "repository_metric", metric: "evolutionDimension" }],
@@ -5294,8 +5537,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5294
5537
  factorId: "repository.external",
5295
5538
  family: "external",
5296
5539
  strength: externalDimension * dimensionWeights.external,
5297
- rawMetrics: { externalDimension: round45(externalDimension) },
5298
- normalizedMetrics: { dimensionWeight: round45(dimensionWeights.external) },
5540
+ rawMetrics: { externalDimension: round46(externalDimension) },
5541
+ normalizedMetrics: { dimensionWeight: round46(dimensionWeights.external) },
5299
5542
  weight: dimensionWeights.external,
5300
5543
  amplification: null,
5301
5544
  evidence: [{ kind: "repository_metric", metric: "externalDimension" }],
@@ -5306,19 +5549,19 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5306
5549
  family: "composite",
5307
5550
  strength: structuralDimension * evolutionDimension * config.interactionWeights.structuralEvolution + criticalInstability * config.interactionWeights.centralInstability + dependencyAmplification * config.interactionWeights.dependencyAmplification,
5308
5551
  rawMetrics: {
5309
- structuralEvolution: round45(
5552
+ structuralEvolution: round46(
5310
5553
  structuralDimension * evolutionDimension * config.interactionWeights.structuralEvolution
5311
5554
  ),
5312
- centralInstability: round45(
5555
+ centralInstability: round46(
5313
5556
  criticalInstability * config.interactionWeights.centralInstability
5314
5557
  ),
5315
- dependencyAmplification: round45(
5558
+ dependencyAmplification: round46(
5316
5559
  dependencyAmplification * config.interactionWeights.dependencyAmplification
5317
5560
  )
5318
5561
  },
5319
5562
  normalizedMetrics: {
5320
- criticalInstability: round45(criticalInstability),
5321
- dependencyAmplification: round45(dependencyAmplification)
5563
+ criticalInstability: round46(criticalInstability),
5564
+ dependencyAmplification: round46(dependencyAmplification)
5322
5565
  },
5323
5566
  weight: null,
5324
5567
  amplification: config.interactionWeights.structuralEvolution + config.interactionWeights.centralInstability + config.interactionWeights.dependencyAmplification,
@@ -5338,7 +5581,7 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5338
5581
  }
5339
5582
  return {
5340
5583
  riskScore,
5341
- normalizedScore: round45(repositoryNormalizedScore),
5584
+ normalizedScore: round46(repositoryNormalizedScore),
5342
5585
  hotspots,
5343
5586
  fragileClusters,
5344
5587
  dependencyAmplificationZones,
@@ -5461,6 +5704,28 @@ var evaluateRepositoryRisk = (input, options = {}) => {
5461
5704
  };
5462
5705
  };
5463
5706
 
5707
+ // src/application/todo-fixme-counter.ts
5708
+ import * as ts2 from "typescript";
5709
+ var markerRegex = /\b(?:TODO|FIXME)\b/gi;
5710
+ var countMarkers = (text) => text.match(markerRegex)?.length ?? 0;
5711
+ var countTodoFixmeInComments = (content) => {
5712
+ const scanner = ts2.createScanner(
5713
+ ts2.ScriptTarget.Latest,
5714
+ false,
5715
+ ts2.LanguageVariant.Standard,
5716
+ content
5717
+ );
5718
+ let total = 0;
5719
+ let token = scanner.scan();
5720
+ while (token !== ts2.SyntaxKind.EndOfFileToken) {
5721
+ if (token === ts2.SyntaxKind.SingleLineCommentTrivia || token === ts2.SyntaxKind.MultiLineCommentTrivia) {
5722
+ total += countMarkers(scanner.getTokenText());
5723
+ }
5724
+ token = scanner.scan();
5725
+ }
5726
+ return total;
5727
+ };
5728
+
5464
5729
  // src/application/run-analyze-command.ts
5465
5730
  var resolveTargetPath = (inputPath, cwd) => resolve3(cwd, inputPath ?? ".");
5466
5731
  var riskProfileConfig = {
@@ -5589,6 +5854,18 @@ var createEvolutionProgressReporter = (logger) => {
5589
5854
  }
5590
5855
  };
5591
5856
  };
5857
+ var collectTodoFixmeCount = async (targetPath, structural) => {
5858
+ const filePaths2 = [...structural.files].map((file) => file.relativePath).sort((a, b) => a.localeCompare(b));
5859
+ let total = 0;
5860
+ for (const relativePath of filePaths2) {
5861
+ try {
5862
+ const content = await readFile2(join4(targetPath, relativePath), "utf8");
5863
+ total += countTodoFixmeInComments(content);
5864
+ } catch {
5865
+ }
5866
+ }
5867
+ return total;
5868
+ };
5592
5869
  var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
5593
5870
  const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
5594
5871
  const targetPath = resolveTargetPath(inputPath, invocationCwd);
@@ -5631,10 +5908,14 @@ var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {},
5631
5908
  } else {
5632
5909
  logger.warn(`external analysis unavailable: ${external.reason}`);
5633
5910
  }
5911
+ logger.info("collecting quality text signals");
5912
+ const todoFixmeCount = await collectTodoFixmeCount(targetPath, structural);
5913
+ logger.debug(`quality text signals: todoFixmeCount=${todoFixmeCount}`);
5634
5914
  return {
5635
5915
  structural,
5636
5916
  evolution,
5637
- external
5917
+ external,
5918
+ todoFixmeCount
5638
5919
  };
5639
5920
  };
5640
5921
  var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
@@ -5647,18 +5928,30 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logg
5647
5928
  logger.info("computing risk summary");
5648
5929
  const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
5649
5930
  const risk = computeRepositoryRiskSummary({
5650
- ...analysisInputs,
5931
+ structural: analysisInputs.structural,
5932
+ evolution: analysisInputs.evolution,
5933
+ external: analysisInputs.external,
5651
5934
  ...riskConfig === void 0 ? {} : { config: riskConfig }
5652
5935
  });
5653
- logger.info(`analysis completed (riskScore=${risk.riskScore})`);
5936
+ const quality = computeRepositoryQualitySummary({
5937
+ structural: analysisInputs.structural,
5938
+ evolution: analysisInputs.evolution,
5939
+ todoFixmeCount: analysisInputs.todoFixmeCount
5940
+ });
5941
+ logger.info(
5942
+ `analysis completed (riskScore=${risk.riskScore}, qualityScore=${quality.qualityScore})`
5943
+ );
5654
5944
  return {
5655
- ...analysisInputs,
5656
- risk
5945
+ structural: analysisInputs.structural,
5946
+ evolution: analysisInputs.evolution,
5947
+ external: analysisInputs.external,
5948
+ risk,
5949
+ quality
5657
5950
  };
5658
5951
  };
5659
5952
 
5660
5953
  // src/application/run-check-command.ts
5661
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
5954
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
5662
5955
 
5663
5956
  // src/application/build-analysis-snapshot.ts
5664
5957
  var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logger) => {
@@ -5673,14 +5966,23 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
5673
5966
  const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
5674
5967
  const evaluation = evaluateRepositoryRisk(
5675
5968
  {
5676
- ...analysisInputs,
5969
+ structural: analysisInputs.structural,
5970
+ evolution: analysisInputs.evolution,
5971
+ external: analysisInputs.external,
5677
5972
  ...riskConfig === void 0 ? {} : { config: riskConfig }
5678
5973
  },
5679
5974
  { explain: options.includeTrace }
5680
5975
  );
5681
5976
  const summary = {
5682
- ...analysisInputs,
5683
- risk: evaluation.summary
5977
+ structural: analysisInputs.structural,
5978
+ evolution: analysisInputs.evolution,
5979
+ external: analysisInputs.external,
5980
+ risk: evaluation.summary,
5981
+ quality: computeRepositoryQualitySummary({
5982
+ structural: analysisInputs.structural,
5983
+ evolution: analysisInputs.evolution,
5984
+ todoFixmeCount: analysisInputs.todoFixmeCount
5985
+ })
5684
5986
  };
5685
5987
  return createSnapshot({
5686
5988
  analysis: summary,
@@ -5732,7 +6034,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
5732
6034
  let diff;
5733
6035
  if (options.baselinePath !== void 0) {
5734
6036
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
5735
- const baselineRaw = await readFile2(options.baselinePath, "utf8");
6037
+ const baselineRaw = await readFile3(options.baselinePath, "utf8");
5736
6038
  try {
5737
6039
  baseline = parseSnapshot(baselineRaw);
5738
6040
  } catch (error) {
@@ -5771,7 +6073,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
5771
6073
  };
5772
6074
 
5773
6075
  // src/application/run-ci-command.ts
5774
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
6076
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
5775
6077
  import { relative as relative2, resolve as resolve4 } from "path";
5776
6078
  var isPathOutsideBase = (value) => {
5777
6079
  return value === ".." || value.startsWith("../") || value.startsWith("..\\");
@@ -5871,7 +6173,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
5871
6173
  diff = compareSnapshots(current, baseline);
5872
6174
  } else if (options.baselinePath !== void 0) {
5873
6175
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
5874
- const baselineRaw = await readFile3(options.baselinePath, "utf8");
6176
+ const baselineRaw = await readFile4(options.baselinePath, "utf8");
5875
6177
  try {
5876
6178
  baseline = parseSnapshot(baselineRaw);
5877
6179
  } catch (error) {
@@ -5919,7 +6221,7 @@ ${ciMarkdown}`;
5919
6221
  };
5920
6222
 
5921
6223
  // src/application/run-report-command.ts
5922
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
6224
+ import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
5923
6225
  var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
5924
6226
  logger.info("building analysis snapshot");
5925
6227
  const current = await buildAnalysisSnapshot(
@@ -5941,7 +6243,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
5941
6243
  report = createReport(current);
5942
6244
  } else {
5943
6245
  logger.info(`loading baseline snapshot: ${options.comparePath}`);
5944
- const baselineRaw = await readFile4(options.comparePath, "utf8");
6246
+ const baselineRaw = await readFile5(options.comparePath, "utf8");
5945
6247
  const baseline = parseSnapshot(baselineRaw);
5946
6248
  const diff = compareSnapshots(current, baseline);
5947
6249
  report = createReport(current, diff);
@@ -5987,7 +6289,9 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
5987
6289
  const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
5988
6290
  const evaluation = evaluateRepositoryRisk(
5989
6291
  {
5990
- ...analysisInputs,
6292
+ structural: analysisInputs.structural,
6293
+ evolution: analysisInputs.evolution,
6294
+ external: analysisInputs.external,
5991
6295
  ...riskConfig === void 0 ? {} : { config: riskConfig }
5992
6296
  },
5993
6297
  { explain: true }
@@ -5996,10 +6300,19 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
5996
6300
  throw new Error("risk trace unavailable");
5997
6301
  }
5998
6302
  const summary = {
5999
- ...analysisInputs,
6000
- risk: evaluation.summary
6303
+ structural: analysisInputs.structural,
6304
+ evolution: analysisInputs.evolution,
6305
+ external: analysisInputs.external,
6306
+ risk: evaluation.summary,
6307
+ quality: computeRepositoryQualitySummary({
6308
+ structural: analysisInputs.structural,
6309
+ evolution: analysisInputs.evolution,
6310
+ todoFixmeCount: analysisInputs.todoFixmeCount
6311
+ })
6001
6312
  };
6002
- logger.info(`explanation completed (riskScore=${summary.risk.riskScore})`);
6313
+ logger.info(
6314
+ `explanation completed (riskScore=${summary.risk.riskScore}, qualityScore=${summary.quality.qualityScore})`
6315
+ );
6003
6316
  return {
6004
6317
  summary,
6005
6318
  trace: evaluation.trace,
@@ -6055,6 +6368,7 @@ var renderReportHighlightsText = (report) => {
6055
6368
  lines.push("Repository Summary");
6056
6369
  lines.push(` target: ${report.repository.targetPath}`);
6057
6370
  lines.push(` riskScore: ${report.repository.riskScore}`);
6371
+ lines.push(` qualityScore: ${report.quality.qualityScore}`);
6058
6372
  lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
6059
6373
  lines.push(` riskTier: ${report.repository.riskTier}`);
6060
6374
  lines.push("");
@@ -6070,6 +6384,7 @@ var renderReportHighlightsMarkdown = (report) => {
6070
6384
  lines.push("## Repository Summary");
6071
6385
  lines.push(`- target: \`${report.repository.targetPath}\``);
6072
6386
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
6387
+ lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
6073
6388
  lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
6074
6389
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
6075
6390
  lines.push("");
@@ -6087,6 +6402,7 @@ var renderCompactText = (report, explainSummary) => {
6087
6402
  lines.push("Repository");
6088
6403
  lines.push(` target: ${report.repository.targetPath}`);
6089
6404
  lines.push(` riskScore: ${report.repository.riskScore}`);
6405
+ lines.push(` qualityScore: ${report.quality.qualityScore}`);
6090
6406
  lines.push(` riskTier: ${report.repository.riskTier}`);
6091
6407
  lines.push(
6092
6408
  ` dimensions: structural=${report.repository.dimensionScores.structural ?? "n/a"}, evolution=${report.repository.dimensionScores.evolution ?? "n/a"}, external=${report.repository.dimensionScores.external ?? "n/a"}, interactions=${report.repository.dimensionScores.interactions ?? "n/a"}`
@@ -6112,6 +6428,7 @@ var renderCompactMarkdown = (report, explainSummary) => {
6112
6428
  lines.push("## Repository");
6113
6429
  lines.push(`- target: \`${report.repository.targetPath}\``);
6114
6430
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
6431
+ lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
6115
6432
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
6116
6433
  lines.push(
6117
6434
  `- dimensions: structural=\`${report.repository.dimensionScores.structural ?? "n/a"}\`, evolution=\`${report.repository.dimensionScores.evolution ?? "n/a"}\`, external=\`${report.repository.dimensionScores.external ?? "n/a"}\`, interactions=\`${report.repository.dimensionScores.interactions ?? "n/a"}\``
@@ -6316,7 +6633,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
6316
6633
  }
6317
6634
  const report = options.compare === void 0 ? createReport(snapshot) : createReport(
6318
6635
  snapshot,
6319
- compareSnapshots(snapshot, parseSnapshot(await readFile5(options.compare, "utf8")))
6636
+ compareSnapshots(snapshot, parseSnapshot(await readFile6(options.compare, "utf8")))
6320
6637
  );
6321
6638
  if (options.format === "json") {
6322
6639
  const analyzeSummaryOutput = formatAnalyzeOutput(explain.summary, "summary");
@@ -6360,6 +6677,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
6360
6677
  },
6361
6678
  report: {
6362
6679
  repository: report.repository,
6680
+ quality: report.quality,
6363
6681
  hotspots: report.hotspots.slice(0, 5),
6364
6682
  structural: report.structural,
6365
6683
  external: report.external