@getcodesentinel/codesentinel 1.19.1 → 1.21.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
@@ -1753,6 +1753,7 @@ var analyzeDependencyCandidateFromRegistry = async (input) => {
1753
1753
  };
1754
1754
 
1755
1755
  // ../reporter/dist/index.js
1756
+ import { basename, posix } from "path";
1756
1757
  var SNAPSHOT_SCHEMA_VERSION = "codesentinel.snapshot.v1";
1757
1758
  var REPORT_SCHEMA_VERSION = "codesentinel.report.v1";
1758
1759
  var RISK_MODEL_VERSION = "deterministic-v1";
@@ -1914,6 +1915,11 @@ var compareSnapshots = (current, baseline) => {
1914
1915
  }
1915
1916
  };
1916
1917
  };
1918
+ var toPosixDirname = (value) => {
1919
+ const normalized = value.replaceAll("\\", "/");
1920
+ const directory = posix.dirname(normalized);
1921
+ return directory === "." ? "root" : directory;
1922
+ };
1917
1923
  var findTraceTarget = (snapshot, targetType, targetId) => snapshot.trace?.targets.find(
1918
1924
  (target) => target.targetType === targetType && target.targetId === targetId
1919
1925
  );
@@ -1959,14 +1965,27 @@ var suggestedActions = (target) => {
1959
1965
  }
1960
1966
  return [...new Set(actions)].slice(0, 3);
1961
1967
  };
1962
- var hotspotItems = (snapshot) => snapshot.analysis.risk.hotspots.slice(0, 10).map((hotspot) => {
1968
+ var hotspotReason = (factors) => {
1969
+ if (factors.length === 0) {
1970
+ return "Limited trace data available for this hotspot.";
1971
+ }
1972
+ return factors.slice(0, 2).map((factor) => `${factor.label} (${factor.contribution})`).join(" + ");
1973
+ };
1974
+ var hotspotItems = (snapshot) => snapshot.analysis.risk.hotspots.slice(0, 10).map((hotspot, index) => {
1963
1975
  const fileScore = snapshot.analysis.risk.fileScores.find((item) => item.file === hotspot.file);
1976
+ const evolutionMetrics = snapshot.analysis.evolution.available ? snapshot.analysis.evolution.files.find((item) => item.filePath === hotspot.file) : void 0;
1964
1977
  const traceTarget = findTraceTarget(snapshot, "file", hotspot.file);
1965
1978
  const factors = toRenderedFactors(traceTarget);
1966
1979
  return {
1980
+ rank: index + 1,
1967
1981
  target: hotspot.file,
1982
+ module: toPosixDirname(hotspot.file),
1968
1983
  score: hotspot.score,
1969
1984
  normalizedScore: fileScore?.normalizedScore ?? round43(hotspot.score / 100),
1985
+ commitCount: evolutionMetrics?.commitCount ?? null,
1986
+ churnTotal: evolutionMetrics?.churnTotal ?? null,
1987
+ riskContributions: hotspot.factors,
1988
+ reason: hotspotReason(factors),
1970
1989
  topFactors: factors,
1971
1990
  suggestedActions: suggestedActions(traceTarget),
1972
1991
  biggestLevers: (traceTarget?.reductionLevers ?? []).slice(0, 3).map((lever) => `${factorLabel(lever.factorId)} (${lever.estimatedImpact})`)
@@ -1987,6 +2006,51 @@ var repositoryConfidence = (snapshot) => {
1987
2006
  );
1988
2007
  return round43(weighted / weight);
1989
2008
  };
2009
+ var normalizeDependencyScope = (scope) => {
2010
+ switch (scope) {
2011
+ case "prod":
2012
+ case "dev":
2013
+ return scope;
2014
+ default:
2015
+ return "unknown";
2016
+ }
2017
+ };
2018
+ var topStructuralFiles = (snapshot, selector) => [...snapshot.analysis.structural.files].sort((a, b) => selector(b) - selector(a) || a.relativePath.localeCompare(b.relativePath)).slice(0, 5).map((file) => ({
2019
+ file: file.relativePath,
2020
+ module: toPosixDirname(file.relativePath),
2021
+ value: selector(file)
2022
+ }));
2023
+ var cycleDetails = (snapshot) => snapshot.analysis.structural.cycles.map((cycle, index) => {
2024
+ const nodes = [...cycle.nodes].sort((a, b) => a.localeCompare(b));
2025
+ return {
2026
+ id: `cycle-${index + 1}`,
2027
+ size: nodes.length,
2028
+ nodes,
2029
+ path: nodes.join(" -> ")
2030
+ };
2031
+ });
2032
+ var riskyDependencies = (snapshot) => {
2033
+ if (!snapshot.analysis.external.available) {
2034
+ return [];
2035
+ }
2036
+ const dependencyByName = new Map(
2037
+ snapshot.analysis.external.dependencies.map((dependency) => [dependency.name, dependency])
2038
+ );
2039
+ return snapshot.analysis.risk.dependencyScores.map((score) => {
2040
+ const dependency = dependencyByName.get(score.dependency);
2041
+ const riskSignals = [.../* @__PURE__ */ new Set([...score.ownRiskSignals, ...score.inheritedRiskSignals])];
2042
+ return {
2043
+ name: score.dependency,
2044
+ score: score.score,
2045
+ normalizedScore: score.normalizedScore,
2046
+ dependencyScope: normalizeDependencyScope(dependency?.dependencyScope),
2047
+ direct: dependency?.direct ?? false,
2048
+ resolvedVersion: dependency?.resolvedVersion ?? null,
2049
+ riskSignals,
2050
+ reason: riskSignals.length === 0 ? "Derived from aggregate dependency risk signals." : riskSignals.join(", ")
2051
+ };
2052
+ }).filter((dependency) => dependency.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, 20);
2053
+ };
1990
2054
  var repositoryDimensionScores = (snapshot) => {
1991
2055
  const target = findTraceTarget(snapshot, "repository", snapshot.analysis.structural.targetPath);
1992
2056
  if (target === void 0) {
@@ -2019,6 +2083,7 @@ var createReport = (snapshot, diff) => {
2019
2083
  schemaVersion: REPORT_SCHEMA_VERSION,
2020
2084
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2021
2085
  repository: {
2086
+ name: basename(snapshot.analysis.structural.targetPath) || snapshot.analysis.structural.targetPath,
2022
2087
  targetPath: snapshot.analysis.structural.targetPath,
2023
2088
  riskScore: snapshot.analysis.risk.riskScore,
2024
2089
  normalizedScore: snapshot.analysis.risk.normalizedScore,
@@ -2034,6 +2099,12 @@ var createReport = (snapshot, diff) => {
2034
2099
  cycles: snapshot.analysis.structural.cycles.map(
2035
2100
  (cycle) => [...cycle.nodes].sort((a, b) => a.localeCompare(b)).join(" -> ")
2036
2101
  ),
2102
+ cycleDetails: cycleDetails(snapshot),
2103
+ fanInOutExtremes: {
2104
+ highestFanIn: topStructuralFiles(snapshot, (file) => file.fanIn),
2105
+ highestFanOut: topStructuralFiles(snapshot, (file) => file.fanOut),
2106
+ deepestFiles: topStructuralFiles(snapshot, (file) => file.depth)
2107
+ },
2037
2108
  fragileClusters: snapshot.analysis.risk.fragileClusters.map((cluster) => ({
2038
2109
  id: cluster.id,
2039
2110
  kind: cluster.kind,
@@ -2057,7 +2128,8 @@ var createReport = (snapshot, diff) => {
2057
2128
  ),
2058
2129
  abandonedDependencies: [...external.abandonedDependencies].sort(
2059
2130
  (a, b) => a.localeCompare(b)
2060
- )
2131
+ ),
2132
+ riskyDependencies: riskyDependencies(snapshot)
2061
2133
  },
2062
2134
  appendix: {
2063
2135
  snapshotSchemaVersion: snapshot.schemaVersion,
@@ -2298,7 +2370,7 @@ var formatReport = (report, format) => {
2298
2370
 
2299
2371
  // ../governance/dist/index.js
2300
2372
  import { mkdirSync, rmSync } from "fs";
2301
- import { basename, join as join5, resolve } from "path";
2373
+ import { basename as basename2, join as join5, resolve } from "path";
2302
2374
  import { execFile } from "child_process";
2303
2375
  import { promisify } from "util";
2304
2376
  var EXIT_CODES = {
@@ -2911,9 +2983,9 @@ var resolveAutoBaselineRef = async (input) => {
2911
2983
 
2912
2984
  // src/index.ts
2913
2985
  import { readFileSync as readFileSync2 } from "fs";
2914
- import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
2915
- import { dirname as dirname2, resolve as resolve5 } from "path";
2916
- import { fileURLToPath } from "url";
2986
+ import { readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
2987
+ import { dirname as dirname3, resolve as resolve6 } from "path";
2988
+ import { fileURLToPath as fileURLToPath2 } from "url";
2917
2989
 
2918
2990
  // src/application/format-analyze-output.ts
2919
2991
  var toHealthTier2 = (score) => {
@@ -3558,7 +3630,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
3558
3630
  );
3559
3631
  return "skip";
3560
3632
  }
3561
- return await new Promise((resolve6) => {
3633
+ return await new Promise((resolve7) => {
3562
3634
  emitKeypressEvents(stdin);
3563
3635
  let selectedIndex = 0;
3564
3636
  const previousRawMode = stdin.isRaw;
@@ -3583,7 +3655,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
3583
3655
  } else {
3584
3656
  stderr.write("\n");
3585
3657
  }
3586
- resolve6(choice);
3658
+ resolve7(choice);
3587
3659
  };
3588
3660
  const onKeypress = (_str, key) => {
3589
3661
  if (key.ctrl === true && key.name === "c") {
@@ -3777,7 +3849,7 @@ var promptSelection = async (currentVersion, actions) => {
3777
3849
  if (!stdin2.isTTY || !stderr2.isTTY || typeof stdin2.setRawMode !== "function") {
3778
3850
  return "exit";
3779
3851
  }
3780
- return await new Promise((resolve6) => {
3852
+ return await new Promise((resolve7) => {
3781
3853
  emitKeypressEvents2(stdin2);
3782
3854
  let selectedIndex = 0;
3783
3855
  const previousRawMode = stdin2.isRaw;
@@ -3792,7 +3864,7 @@ var promptSelection = async (currentVersion, actions) => {
3792
3864
  stdin2.setRawMode(previousRawMode);
3793
3865
  clearTerminal();
3794
3866
  showCursor2();
3795
- resolve6(selection);
3867
+ resolve7(selection);
3796
3868
  };
3797
3869
  const onKeypress = (_str, key) => {
3798
3870
  if (key.ctrl === true && key.name === "c") {
@@ -3885,7 +3957,7 @@ var waitForReturnToMenu = async () => {
3885
3957
  }
3886
3958
  stderr2.write(`
3887
3959
  ${PROMPT_PADDING}Press enter to return to the menu...`);
3888
- await new Promise((resolve6) => {
3960
+ await new Promise((resolve7) => {
3889
3961
  emitKeypressEvents2(stdin2);
3890
3962
  const previousRawMode = stdin2.isRaw;
3891
3963
  const cleanup = () => {
@@ -3894,7 +3966,7 @@ ${PROMPT_PADDING}Press enter to return to the menu...`);
3894
3966
  stdin2.setRawMode(previousRawMode);
3895
3967
  showCursor2();
3896
3968
  stderr2.write("\n");
3897
- resolve6();
3969
+ resolve7();
3898
3970
  };
3899
3971
  const onKeypress = (_str, key) => {
3900
3972
  if (key.ctrl === true && key.name === "c") {
@@ -3912,7 +3984,7 @@ ${PROMPT_PADDING}Press enter to return to the menu...`);
3912
3984
  });
3913
3985
  };
3914
3986
  var runCliCommand = async (scriptPath2, args) => {
3915
- return await new Promise((resolve6, reject) => {
3987
+ return await new Promise((resolve7, reject) => {
3916
3988
  const child = spawn2(process.execPath, [...process.execArgv, scriptPath2, ...args], {
3917
3989
  stdio: ["inherit", "pipe", "pipe"],
3918
3990
  env: {
@@ -3926,7 +3998,7 @@ var runCliCommand = async (scriptPath2, args) => {
3926
3998
  reject(error);
3927
3999
  });
3928
4000
  child.on("close", (code) => {
3929
- resolve6(code ?? 1);
4001
+ resolve7(code ?? 1);
3930
4002
  });
3931
4003
  });
3932
4004
  };
@@ -4683,6 +4755,26 @@ var finalizeAuthorDistribution = (authorCommits) => {
4683
4755
  share: round44(commits / totalCommits)
4684
4756
  })).sort((a, b) => b.commits - a.commits || a.authorId.localeCompare(b.authorId));
4685
4757
  };
4758
+ var finalizeAuthorChurnDistribution = (authorChurn) => {
4759
+ const entries = [...authorChurn.entries()].map(([authorId, churn]) => {
4760
+ const churnAdded = churn.churnAdded;
4761
+ const churnDeleted = churn.churnDeleted;
4762
+ return {
4763
+ authorId,
4764
+ churnAdded,
4765
+ churnDeleted,
4766
+ churnTotal: churnAdded + churnDeleted
4767
+ };
4768
+ });
4769
+ const totalChurn = entries.reduce((sum, entry) => sum + entry.churnTotal, 0);
4770
+ if (totalChurn === 0) {
4771
+ return [];
4772
+ }
4773
+ return entries.map((entry) => ({
4774
+ ...entry,
4775
+ share: round44(entry.churnTotal / totalChurn)
4776
+ })).sort((a, b) => b.churnTotal - a.churnTotal || a.authorId.localeCompare(b.authorId));
4777
+ };
4686
4778
  var buildCouplingMatrix = (coChangeByPair, fileCommitCount, consideredCommits, skippedLargeCommits, maxCouplingPairs) => {
4687
4779
  const allPairs = [];
4688
4780
  for (const [key, coChangeCommits] of coChangeByPair.entries()) {
@@ -4751,10 +4843,19 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4751
4843
  recentCommitCount: 0,
4752
4844
  churnAdded: 0,
4753
4845
  churnDeleted: 0,
4754
- authors: /* @__PURE__ */ new Map()
4846
+ authorsByCommits: /* @__PURE__ */ new Map(),
4847
+ authorsByChurn: /* @__PURE__ */ new Map()
4755
4848
  };
4756
4849
  current.churnAdded += fileChange.additions;
4757
4850
  current.churnDeleted += fileChange.deletions;
4851
+ const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
4852
+ const authorChurn = current.authorsByChurn.get(effectiveAuthorId) ?? {
4853
+ churnAdded: 0,
4854
+ churnDeleted: 0
4855
+ };
4856
+ authorChurn.churnAdded += fileChange.additions;
4857
+ authorChurn.churnDeleted += fileChange.deletions;
4858
+ current.authorsByChurn.set(effectiveAuthorId, authorChurn);
4758
4859
  fileStats.set(fileChange.filePath, current);
4759
4860
  }
4760
4861
  for (const filePath of uniqueFiles) {
@@ -4767,7 +4868,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4767
4868
  current.recentCommitCount += 1;
4768
4869
  }
4769
4870
  const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
4770
- current.authors.set(effectiveAuthorId, (current.authors.get(effectiveAuthorId) ?? 0) + 1);
4871
+ current.authorsByCommits.set(
4872
+ effectiveAuthorId,
4873
+ (current.authorsByCommits.get(effectiveAuthorId) ?? 0) + 1
4874
+ );
4771
4875
  }
4772
4876
  const orderedFiles = [...uniqueFiles].sort((a, b) => a.localeCompare(b));
4773
4877
  if (orderedFiles.length > 1) {
@@ -4790,8 +4894,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4790
4894
  }
4791
4895
  }
4792
4896
  const files = [...fileStats.entries()].map(([filePath, stats]) => {
4793
- const authorDistribution = finalizeAuthorDistribution(stats.authors);
4794
- const topAuthorShare = authorDistribution[0]?.share ?? 0;
4897
+ const authorDistributionByCommits = finalizeAuthorDistribution(stats.authorsByCommits);
4898
+ const authorDistributionByChurn = finalizeAuthorChurnDistribution(stats.authorsByChurn);
4899
+ const topAuthorShareByCommits = authorDistributionByCommits[0]?.share ?? 0;
4900
+ const topAuthorShareByChurn = authorDistributionByChurn[0]?.share ?? 0;
4795
4901
  return {
4796
4902
  filePath,
4797
4903
  commitCount: stats.commitCount,
@@ -4801,9 +4907,18 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4801
4907
  churnTotal: stats.churnAdded + stats.churnDeleted,
4802
4908
  recentCommitCount: stats.recentCommitCount,
4803
4909
  recentVolatility: stats.commitCount === 0 ? 0 : round44(stats.recentCommitCount / stats.commitCount),
4804
- topAuthorShare,
4805
- busFactor: computeBusFactor(authorDistribution, config.busFactorCoverageThreshold),
4806
- authorDistribution
4910
+ topAuthorShareByCommits,
4911
+ busFactorByCommits: computeBusFactor(
4912
+ authorDistributionByCommits,
4913
+ config.busFactorCoverageThreshold
4914
+ ),
4915
+ authorDistributionByCommits,
4916
+ topAuthorShareByChurn,
4917
+ busFactorByChurn: computeBusFactor(
4918
+ authorDistributionByChurn,
4919
+ config.busFactorCoverageThreshold
4920
+ ),
4921
+ authorDistributionByChurn
4807
4922
  };
4808
4923
  }).sort((a, b) => a.filePath.localeCompare(b.filePath));
4809
4924
  const fileCommitCount = new Map(files.map((file) => [file.filePath, file.commitCount]));
@@ -5558,15 +5673,15 @@ var computeRepositoryHealthSummary = (input) => {
5558
5673
  let singleContributorFiles = 0;
5559
5674
  let trackedFiles = 0;
5560
5675
  for (const file of evolutionSourceFiles) {
5561
- if (file.commitCount <= 0 || file.authorDistribution.length === 0) {
5676
+ if (file.commitCount <= 0 || file.authorDistributionByCommits.length === 0) {
5562
5677
  continue;
5563
5678
  }
5564
5679
  trackedFiles += 1;
5565
- const dominantShare = clamp01(file.authorDistribution[0]?.share ?? 0);
5566
- if (file.authorDistribution.length === 1 || dominantShare >= 0.9) {
5680
+ const dominantShare = clamp01(file.authorDistributionByCommits[0]?.share ?? 0);
5681
+ if (file.authorDistributionByCommits.length === 1 || dominantShare >= 0.9) {
5567
5682
  singleContributorFiles += 1;
5568
5683
  }
5569
- for (const author of file.authorDistribution) {
5684
+ for (const author of file.authorDistributionByCommits) {
5570
5685
  const commits = Math.max(0, author.commits);
5571
5686
  if (commits <= 0) {
5572
5687
  continue;
@@ -6329,7 +6444,7 @@ var computeEvolutionScales = (evolutionByFile, config) => {
6329
6444
  config.quantileClamp.upper
6330
6445
  ),
6331
6446
  busFactor: buildQuantileScale(
6332
- evolutionFiles.map((metrics) => metrics.busFactor),
6447
+ evolutionFiles.map((metrics) => metrics.busFactorByCommits),
6333
6448
  config.quantileClamp.lower,
6334
6449
  config.quantileClamp.upper
6335
6450
  )
@@ -6518,9 +6633,9 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6518
6633
  evolutionScales.churnTotal
6519
6634
  );
6520
6635
  volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
6521
- ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShare);
6636
+ ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShareByCommits);
6522
6637
  busFactorRisk = toUnitInterval(
6523
- 1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor)
6638
+ 1 - normalizeWithScale(evolutionMetrics.busFactorByCommits, evolutionScales.busFactor)
6524
6639
  );
6525
6640
  const evolutionWeights = config.evolutionFactorWeights;
6526
6641
  evolutionFactor = toUnitInterval(
@@ -6577,8 +6692,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6577
6692
  commitCount: evolutionMetrics?.commitCount ?? null,
6578
6693
  churnTotal: evolutionMetrics?.churnTotal ?? null,
6579
6694
  recentVolatility: evolutionMetrics?.recentVolatility ?? null,
6580
- topAuthorShare: evolutionMetrics?.topAuthorShare ?? null,
6581
- busFactor: evolutionMetrics?.busFactor ?? null,
6695
+ topAuthorShareByCommits: evolutionMetrics?.topAuthorShareByCommits ?? null,
6696
+ busFactorByCommits: evolutionMetrics?.busFactorByCommits ?? null,
6582
6697
  dependencyAffinity: round46(dependencyAffinity),
6583
6698
  repositoryExternalPressure: round46(dependencyComputation.repositoryExternalPressure),
6584
6699
  structuralAttenuation: round46(structuralAttenuation)
@@ -6646,8 +6761,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6646
6761
  commitCount: context.rawMetrics.commitCount,
6647
6762
  churnTotal: context.rawMetrics.churnTotal,
6648
6763
  recentVolatility: context.rawMetrics.recentVolatility,
6649
- topAuthorShare: context.rawMetrics.topAuthorShare,
6650
- busFactor: context.rawMetrics.busFactor
6764
+ topAuthorShareByCommits: context.rawMetrics.topAuthorShareByCommits,
6765
+ busFactorByCommits: context.rawMetrics.busFactorByCommits
6651
6766
  },
6652
6767
  normalizedMetrics: {
6653
6768
  frequencyRisk: context.normalizedMetrics.frequencyRisk,
@@ -7597,7 +7712,114 @@ ${ciMarkdown}`;
7597
7712
  };
7598
7713
 
7599
7714
  // src/application/run-report-command.ts
7600
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
7715
+ import { join as join8 } from "path";
7716
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
7717
+
7718
+ // src/application/html-report.ts
7719
+ import { mkdir as mkdir3, readFile as readFile5, rm, stat as stat2, writeFile as writeFile5 } from "fs/promises";
7720
+ import { dirname as dirname2, join as join7, resolve as resolve5 } from "path";
7721
+ import { fileURLToPath } from "url";
7722
+ var HTML_REPORT_DIR = ".codesentinel/report";
7723
+ var getBundledHtmlAppPath = () => resolve5(dirname2(fileURLToPath(import.meta.url)), "html-report-app");
7724
+ var serializeReportBootstrap = (report) => `window.__CODESENTINEL_REPORT__ = ${JSON.stringify(report).replaceAll("</", "<\\/")};
7725
+ `;
7726
+ var escapeInlineScript = (script) => script.replaceAll("</script", "<\\/script").replaceAll("<script", "\\x3Cscript").replaceAll("<!--", "\\x3C!--").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
7727
+ var ensureDirectory = async (directoryPath) => {
7728
+ await mkdir3(directoryPath, { recursive: true });
7729
+ };
7730
+ var readReferencedAssets = async (appPath, indexHtml) => {
7731
+ const stylesheetPaths = [
7732
+ ...indexHtml.matchAll(/<link[^>]+rel=["']stylesheet["'][^>]+href=["']([^"']+)["'][^>]*>/g)
7733
+ ].map((match) => match[1]).filter((value) => value !== void 0);
7734
+ const styles = await Promise.all(
7735
+ stylesheetPaths.map(async (href) => readFile5(resolve5(appPath, href), "utf8"))
7736
+ );
7737
+ const scriptPaths = [...indexHtml.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*><\/script>/g)].map((match) => match[1]).filter((value) => value !== void 0);
7738
+ const scripts = await Promise.all(
7739
+ scriptPaths.map(async (src) => readFile5(resolve5(appPath, src), "utf8"))
7740
+ );
7741
+ return { styles, scripts };
7742
+ };
7743
+ var inlineBuiltHtml = async (appPath, report) => {
7744
+ const indexHtml = await readFile5(resolve5(appPath, "index.html"), "utf8");
7745
+ const { styles, scripts } = await readReferencedAssets(appPath, indexHtml);
7746
+ const htmlWithoutExternalAssets = indexHtml.replace(/\s*<link[^>]+rel=["']stylesheet["'][^>]*>\s*/g, "").replace(/\s*<script[^>]+src=["'][^"']+["'][^>]*><\/script>\s*/g, "");
7747
+ const inlineStyles = styles.map((style) => `<style>
7748
+ ${style}
7749
+ </style>`).join("\n");
7750
+ const bootstrapScript = `<script>
7751
+ ${serializeReportBootstrap(report)}</script>`;
7752
+ const inlineScripts = scripts.map((script) => `<script>
7753
+ ${escapeInlineScript(script)}
7754
+ </script>`).join("\n");
7755
+ return htmlWithoutExternalAssets.replace("</head>", () => `${inlineStyles === "" ? "" : `${inlineStyles}
7756
+ `}</head>`).replace(
7757
+ "</body>",
7758
+ () => `${bootstrapScript}
7759
+ ${inlineScripts === "" ? "" : `${inlineScripts}
7760
+ `}</body>`
7761
+ );
7762
+ };
7763
+ var assertHtmlAppAssets = async (assetPath) => {
7764
+ const assetStats = await stat2(assetPath).catch(() => void 0);
7765
+ if (assetStats === void 0 || !assetStats.isDirectory()) {
7766
+ throw new Error(
7767
+ `html_report_assets_missing: expected built app at ${assetPath}. Run the workspace build first.`
7768
+ );
7769
+ }
7770
+ };
7771
+ var resolveHtmlReportOutputPath = (repositoryPath, outputPath) => {
7772
+ const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
7773
+ return resolve5(invocationCwd, outputPath ?? join7(repositoryPath, HTML_REPORT_DIR));
7774
+ };
7775
+ var writeHtmlReportBundle = async (report, options) => {
7776
+ const bundledAppPath = options.bundledAppPath ?? getBundledHtmlAppPath();
7777
+ await assertHtmlAppAssets(bundledAppPath);
7778
+ const outputPath = resolveHtmlReportOutputPath(options.repositoryPath, options.outputPath);
7779
+ await rm(outputPath, { recursive: true, force: true });
7780
+ await ensureDirectory(outputPath);
7781
+ await writeFile5(
7782
+ resolve5(outputPath, "index.html"),
7783
+ await inlineBuiltHtml(bundledAppPath, report),
7784
+ "utf8"
7785
+ );
7786
+ return outputPath;
7787
+ };
7788
+
7789
+ // src/application/open-path.ts
7790
+ import { spawn as spawn3 } from "child_process";
7791
+ import { platform } from "os";
7792
+ var openCommandForPlatform = (targetPath) => {
7793
+ switch (platform()) {
7794
+ case "darwin":
7795
+ return { command: "open", args: [targetPath] };
7796
+ case "win32":
7797
+ return { command: "cmd", args: ["/c", "start", "", targetPath] };
7798
+ case "linux":
7799
+ return { command: "xdg-open", args: [targetPath] };
7800
+ default:
7801
+ return void 0;
7802
+ }
7803
+ };
7804
+ var openPath = async (targetPath) => {
7805
+ const command = openCommandForPlatform(targetPath);
7806
+ if (command === void 0) {
7807
+ return false;
7808
+ }
7809
+ return new Promise((resolve7) => {
7810
+ const child = spawn3(command.command, command.args, {
7811
+ detached: true,
7812
+ stdio: "ignore"
7813
+ });
7814
+ child.once("error", () => resolve7(false));
7815
+ child.once("spawn", () => {
7816
+ child.unref();
7817
+ resolve7(true);
7818
+ });
7819
+ });
7820
+ };
7821
+
7822
+ // src/application/run-report-command.ts
7601
7823
  var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
7602
7824
  logger.info("building analysis snapshot");
7603
7825
  const current = await buildAnalysisSnapshot(
@@ -7611,7 +7833,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
7611
7833
  logger
7612
7834
  );
7613
7835
  if (options.snapshotPath !== void 0) {
7614
- await writeFile5(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
7836
+ await writeFile6(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
7615
7837
  logger.info(`snapshot written: ${options.snapshotPath}`);
7616
7838
  }
7617
7839
  let report;
@@ -7619,17 +7841,39 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
7619
7841
  report = createReport(current);
7620
7842
  } else {
7621
7843
  logger.info(`loading baseline snapshot: ${options.comparePath}`);
7622
- const baselineRaw = await readFile5(options.comparePath, "utf8");
7844
+ const baselineRaw = await readFile6(options.comparePath, "utf8");
7623
7845
  const baseline = parseSnapshot(baselineRaw);
7624
7846
  const diff = compareSnapshots(current, baseline);
7625
7847
  report = createReport(current, diff);
7626
7848
  }
7849
+ if (options.format === "html") {
7850
+ const bundlePath = await writeHtmlReportBundle(report, {
7851
+ repositoryPath: current.analysis.structural.targetPath,
7852
+ ...options.outputPath === void 0 ? {} : { outputPath: options.outputPath }
7853
+ });
7854
+ if (options.open === true) {
7855
+ const opened = await openPath(join8(bundlePath, "index.html"));
7856
+ if (!opened) {
7857
+ logger.warn("unable to open html report automatically on this platform");
7858
+ }
7859
+ }
7860
+ logger.info(`html report written: ${bundlePath}`);
7861
+ return {
7862
+ report,
7863
+ rendered: bundlePath,
7864
+ outputPath: bundlePath
7865
+ };
7866
+ }
7627
7867
  const rendered = formatReport(report, options.format);
7628
7868
  if (options.outputPath !== void 0) {
7629
- await writeFile5(options.outputPath, rendered, "utf8");
7869
+ await writeFile6(options.outputPath, rendered, "utf8");
7630
7870
  logger.info(`report written: ${options.outputPath}`);
7631
7871
  }
7632
- return { report, rendered };
7872
+ return {
7873
+ report,
7874
+ rendered,
7875
+ ...options.outputPath === void 0 ? {} : { outputPath: options.outputPath }
7876
+ };
7633
7877
  };
7634
7878
 
7635
7879
  // src/application/run-explain-command.ts
@@ -7699,7 +7943,7 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
7699
7943
 
7700
7944
  // src/index.ts
7701
7945
  var program = new Command();
7702
- var packageJsonPath = resolve5(dirname2(fileURLToPath(import.meta.url)), "../package.json");
7946
+ var packageJsonPath = resolve6(dirname3(fileURLToPath2(import.meta.url)), "../package.json");
7703
7947
  var { version } = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
7704
7948
  var parseRecentWindowDays = (value) => {
7705
7949
  const parsed = Number.parseInt(value, 10);
@@ -7945,8 +8189,8 @@ program.command("report").argument("[path]", "path to the project to analyze").a
7945
8189
  "log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
7946
8190
  ).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
7947
8191
  ).addOption(
7948
- new Option("--format <mode>", "output format: text, json, md").choices(["text", "json", "md"]).default("md")
7949
- ).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").addOption(
8192
+ new Option("--format <mode>", "output format: text, json, md, html").choices(["text", "json", "md", "html"]).default("md")
8193
+ ).option("--output <path>", "write rendered report to a file path").option("--open", "open the generated HTML report in the default browser").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").addOption(
7950
8194
  new Option(
7951
8195
  "--recent-window-days <days>",
7952
8196
  "git recency window in days used for evolution volatility metrics"
@@ -7962,14 +8206,20 @@ program.command("report").argument("[path]", "path to the project to analyze").a
7962
8206
  ...options.output === void 0 ? {} : { outputPath: options.output },
7963
8207
  ...options.compare === void 0 ? {} : { comparePath: options.compare },
7964
8208
  ...options.snapshot === void 0 ? {} : { snapshotPath: options.snapshot },
8209
+ ...options.open === void 0 ? {} : { open: options.open },
7965
8210
  includeTrace: options.trace,
7966
8211
  scoringProfile: options.scoringProfile,
7967
8212
  recentWindowDays: options.recentWindowDays
7968
8213
  },
7969
8214
  logger
7970
8215
  );
7971
- if (options.output === void 0) {
8216
+ if (options.output === void 0 && options.format !== "html") {
7972
8217
  process.stdout.write(`${result.rendered}
8218
+ `);
8219
+ return;
8220
+ }
8221
+ if (options.format === "html") {
8222
+ process.stdout.write(`${result.outputPath ?? result.rendered}
7973
8223
  `);
7974
8224
  }
7975
8225
  }
@@ -7988,7 +8238,9 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
7988
8238
  new Option("--format <mode>", "combined output format: text, md, json").choices(["text", "md", "json"]).default("md")
7989
8239
  ).addOption(
7990
8240
  new Option("--detail <level>", "run detail level: compact (default), standard, full").choices(["compact", "standard", "full"]).default("compact")
7991
- ).option("--file <path>", "explain a specific file target").option("--module <name>", "explain a specific module target").option("--top <count>", "number of top hotspots to explain when no target is selected", "5").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").addOption(
8241
+ ).option("--file <path>", "explain a specific file target").option("--module <name>", "explain a specific module target").option("--top <count>", "number of top hotspots to explain when no target is selected", "5").option("--compare <baseline>", "compare against a baseline snapshot JSON file").option("--snapshot <path>", "write current snapshot JSON artifact").addOption(
8242
+ new Option("--report <format>", "write an additional report bundle during the run").choices(["html"]).default(void 0)
8243
+ ).option("--report-output <path>", "output path for the generated report bundle").option("--open", "open the generated HTML report in the default browser").option("--no-trace", "disable trace embedding in generated snapshot").addOption(
7992
8244
  new Option(
7993
8245
  "--recent-window-days <days>",
7994
8246
  "git recency window in days used for evolution volatility metrics"
@@ -8015,13 +8267,28 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
8015
8267
  ...options.trace === true ? { trace: explain.trace } : {}
8016
8268
  });
8017
8269
  if (options.snapshot !== void 0) {
8018
- await writeFile6(options.snapshot, JSON.stringify(snapshot, null, 2), "utf8");
8270
+ await writeFile7(options.snapshot, JSON.stringify(snapshot, null, 2), "utf8");
8019
8271
  logger.info(`snapshot written: ${options.snapshot}`);
8020
8272
  }
8021
8273
  const report = options.compare === void 0 ? createReport(snapshot) : createReport(
8022
8274
  snapshot,
8023
- compareSnapshots(snapshot, parseSnapshot(await readFile6(options.compare, "utf8")))
8275
+ compareSnapshots(snapshot, parseSnapshot(await readFile7(options.compare, "utf8")))
8024
8276
  );
8277
+ if (options.report === "html") {
8278
+ const htmlOutputPath = await writeHtmlReportBundle(report, {
8279
+ repositoryPath: explain.summary.structural.targetPath,
8280
+ ...options.reportOutput === void 0 ? {} : { outputPath: options.reportOutput }
8281
+ });
8282
+ if (options.open === true) {
8283
+ const opened = await openPath(`${htmlOutputPath}/index.html`);
8284
+ if (!opened) {
8285
+ logger.warn("unable to open html report automatically on this platform");
8286
+ }
8287
+ }
8288
+ logger.info(`html report written: ${htmlOutputPath}`);
8289
+ } else if (options.open === true) {
8290
+ logger.warn("--open has no effect unless --report html is set");
8291
+ }
8025
8292
  if (options.format === "json") {
8026
8293
  const analyzeSummaryOutput = formatAnalyzeOutput(explain.summary, "summary");
8027
8294
  if (options.detail === "compact") {