@chappibunny/repolens 1.16.2 → 1.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,63 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.16.4 (2026-11-12)
6
+
7
+ Fix found by the 2026-11-12 gap test of 1.16.3 on lukeautry/tsoa (ce4daec).
8
+
9
+ ### Upgrade notes
10
+
11
+ - **Fewer cycles and edges in TypeScript repositories.** Imports the TypeScript compiler removes are no longer runtime edges. On tsoa: circular dependencies 10 → 1, import edges 412 → 326, with 108 type-only imports and 17 type-only cycles listed separately. On trpc/trpc: edges 1,533 → 1,531, still no runtime cycles. Architecture baselines and drift reports will show the drop once; it is a measurement fix, not a code change.
12
+ - **Type-only cycles now include mixed cycles.** Before, only cycles made entirely of `import type` edges were listed; a cycle with one `import type` link and runtime links elsewhere was silently dropped. Both are now listed under "Type-only cycles" (trpc: 21 → 40), still Low and not counted.
13
+ - **Fewer orphan files.** A file connected only by type-only imports (a `types.ts` module) is no longer an orphan. On trpc: 126 → 81. The orphan share feeds the architecture health score, so that score can rise.
14
+
15
+ ### Dependency graph
16
+
17
+ - In `.ts` files, an import whose names are all interfaces or type aliases in the target file, or are all used only in type positions (annotations, generic arguments and constraints, `as` / `satisfies`, `implements`, type aliases, interfaces, `declare` blocks), is type-only (*inferred*). Value uses (`new`, calls, member access, `extends` of a class, `instanceof`, decorators, `typeof` in an expression, object shorthand, default values, `export { X }`, JSX tags) keep the import a runtime edge. A name that is both an interface and a value (declaration merging) is a value.
18
+ - `export { X } from` is type-only when every name is an interface or type alias in the target. `export * from`, side-effect imports, `import()`, and `require()` stay runtime edges. Imports inside `.d.ts` files are type-only.
19
+ - In `.tsx` files only the interface / type-alias check applies, because JSX text can look like code.
20
+ - Inference is off where the compiler keeps the import: `verbatimModuleSyntax`, `preserveValueImports`, `importsNotUsedAsValues: "preserve"`/`"error"`, or `erasableSyntaxOnly` (Node type stripping removes only `import type`) in the nearest `tsconfig.json`, and files with decorators under `emitDecoratorMetadata`. The `extends` chain is followed for relative paths and for packages installed in `node_modules` (bare names, `package.json` `tsconfig` field, parent folders).
21
+ - When part of the `extends` chain is not in the checkout (an uninstalled package, a generated `.nuxt/tsconfig.json`), or the file could not be tokenized cleanly, only interface / type-alias imports count as type-only; usage-based inference is skipped so a class used only as a type stays a runtime edge.
22
+ - Only top-level interfaces and type aliases of the target file count. A nested one (`namespace N { interface Foo }`, inside a function or `declare module`) does not make an import of `Foo` type-only, and names the target re-exports from another module (`export { Foo } from`, `export * as Foo from`) or aliases with `import Foo = require()` are treated as values.
23
+ - `import type`, `export type`, and all-`type` lists (`import { type A, type B }`) are type-only as before (*syntax*). A mixed `import { type A, b }` is type-only only when `b` is used only as a type.
24
+ - The dependency graph page and the MCP `get_dependency_graph` tool list "Type-only cycles" with each type-only link and whether it is *syntax* or *inferred*, plus "Type-only cycles" and "Type-only imports" rows in the overview table. The CLI log says "(N type-only cycle(s) not counted)". Health score, badge, and cycle findings count runtime cycles only.
25
+ - External packages are unchanged: an npm import used only as a type is still listed.
26
+
27
+ ### Output accuracy
28
+
29
+ - The executive summary insight no longer calls a lone medium-priority cycle "mutual recursion, probably intentional; verify. Priority: Low" (it said that whenever no cycle was high). It now reads "Small cycle; review. Priority: Medium.", matching the dependency graph page.
30
+
31
+ ## 1.16.3 (2026-11-11)
32
+
33
+ Fixes found by the 2026-11-11 gap test of 1.16.2 on trpc/trpc, App-vNext/Polly, google/gson, charmbracelet/glow, and pallets/itsdangerous.
34
+
35
+ ### Upgrade notes (read before publishing docs)
36
+
37
+ - **Monorepo graphs change.** Imports between declared workspace packages are now edges, not external packages. Expect higher edge counts, fewer orphans, new Hub Modules, fewer external packages, and different module roles on the first run after upgrading. On trpc/trpc: 913 → 1,533 edges, orphans 254 → 126, external packages 95 → 89. If you keep an architecture baseline, the next drift report lists these as changes; they are a measurement fix, not a code change.
38
+ - **`repolens badge` can now exit 2 where it used to print a score.** A repository whose product source has no JavaScript/TypeScript, Python, or Go (only a docs script, test helper, or vendored copy) is reported as unsupported; App-vNext/Polly used to get a false "100". A repository where those languages are under 10% of product source files also exits 2, because one number would not stand for it. CI steps that publish the badge should tolerate exit 2 or skip the badge for such repositories. Vue (`.vue`) and Svelte (`.svelte`) component files are not analyzed (no imports, complexity, or health from them), but they do not count toward that 10% share: a component library with a few `.ts` files and many components still gets a badge, scored on its JavaScript/TypeScript only, with a partial-coverage warning on stderr and in the docs naming the component files that were not analyzed.
39
+ - **Empty pages are no longer written.** API Surface (no routes, no external API integrations, no documented exports) and GraphQL Schema (no real GraphQL evidence) are skipped unless `documentation.sections` lists them. Pages published by earlier versions to Notion, Confluence, GitHub Wiki, or Obsidian are not deleted: they stay as last published until you remove them. The Markdown publisher overwrites files but does not delete an old `06-api-surface.md` or `11-graphql-schema.md` in the output directory.
40
+ - **GraphQL detection is narrower.** Libraries count only when imported. A bare mention of "hasura" in code or comments no longer reports Hasura; a `hasura/config.yaml` (or root `config.yaml`) with `metadata_directory`, or an `@hasura/*` / `hasura-*` import, does.
41
+
42
+ ### Dependency graph
43
+
44
+ - Workspace packages are internal. Members come only from declarations: root `package.json` `workspaces` (array or `{ packages }`, `!` exclusions honoured), `pnpm-workspace.yaml` `packages`, and `lerna.json` `packages`. A stray `package.json` (a test stub named `express`, a fixture) is not a workspace member. An import of `@scope/pkg` or `@scope/pkg/subpath` naming a member resolves to its source through `exports`, `main`, or `src/`, with `dist/`, `lib/`, and `build/` targets mapped back to `src/`. Targets and subpaths that leave the package (`..`, absolute paths, URLs, wildcards) are ignored. A dependency declared as `npm:alias@version` stays external. On trpc/trpc `@trpc/*` is gone from external packages, a Hub Modules section appears, and `packages/server` reads "Shared infrastructure (imported by 7 modules)" instead of "Connector (1 in, 2 out)". The MCP `get_dependency_graph` tool uses the same graph.
45
+ - Flow steps name a package entry `server/src` rather than a bare `src`.
46
+
47
+ ### Language coverage
48
+
49
+ - The dependency graph and code health cover JavaScript/TypeScript, Python, and Go. Coverage is measured on product source: tests, docs, tooling, examples, and vendored trees (`vendor`, `third_party`, `deps`, `external`, and similar) do not count.
50
+ - No supported product code at all: graph and health pages say the language is not supported yet instead of building a graph or score from a docs script. Polly no longer reports "no circular dependencies" and "Average health 100/100"; gson names Java/Kotlin instead of telling the user to check `scan.include`.
51
+ - Supported product code next to other languages (for example 5 Python files and 80 Rust files): the graph and health are built for the supported files, and the dependency graph, code health, executive summary, system overview, and architecture overview all say "Partial coverage: … covers N of M product source files". Cycle claims read "among analyzed files", and "Acyclic dependency graph" is not listed as a pattern.
52
+ - `repolens badge` counts every source file in one walk (up to 20,000, with a note when that limit is hit) and analyzes up to 2,500, so the coverage ratio is not skewed by the analysis cap.
53
+ - `demo --help`, `init --help`, and the README say detection is not full analysis.
54
+ - The language list is ordered by file count (Polly reads "C#, JavaScript"), and a statically typed primary language is no longer described as "Dynamic typing".
55
+
56
+ ### Output accuracy
57
+
58
+ - GraphQL libraries are detected from imports. SDL and resolver maps are read after a comment stripper that keeps strings, template literals (including `${}` expressions), and regex literals, so `'src/*.graphql'` or `' // '` in a string no longer erases real SDL. Comments that mention Apollo or urql and React Query `useQuery:` hook maps no longer produce a GraphQL page; trpc/trpc's `11-graphql-schema.md` is skipped.
59
+ - A module whose files are mostly tests says so ("Mostly tests and fixtures (204 of 212 files)") instead of "Application module".
60
+ - When API Surface is written with no routes, it no longer blames `scan.include`.
61
+
5
62
  ## 1.16.2
6
63
 
7
64
  ### Branding
package/README.md CHANGED
@@ -29,7 +29,7 @@ RepoLens scans your repository, generates living architecture documentation, and
29
29
 
30
30
  ## 🎬 Demo
31
31
 
32
- > **Try it now** — no installation required. `npx @chappibunny/repolens demo` previews the repository you point it at. It detects common languages (Python, Go, Rust, Java, JavaScript/TypeScript, and others). If the scan matches no source files, demo exits 2 and does not write empty docs. Code health scores JavaScript/TypeScript, Python, and Go.
32
+ > **Try it now** — no installation required. `npx @chappibunny/repolens demo` previews the repository you point it at. It detects common languages (Python, Go, Rust, Java, JavaScript/TypeScript, and others) and maps their modules and file structure. If the scan matches no source files, demo exits 2 and does not write empty docs. The dependency graph and code health cover JavaScript/TypeScript, Python, and Go only; a Java, Kotlin, or C# repository gets a "not supported yet" note on those pages, not a score.
33
33
 
34
34
  **[See it on real repos](https://chapibunny.github.io/repolens/)** — architecture pages RepoLens generated for Express, FastAPI, Django, Gin, mdBook, and other public projects. Deterministic mode, no API keys.
35
35
 
@@ -243,9 +243,9 @@ If `npx` reports `Unknown command: mcp`, an `.npmrc` with `min-release-age` (or
243
243
 
244
244
  ## GitHub Action
245
245
 
246
- `action.yml` is a composite action named **RepoLens Architecture Docs**. `publish`, `demo`, and `doctor` run the published npm package (`npx --force --yes @chappibunny/repolens@1.16.2`). `comment` and `badge` run from the action checkout. Inputs are `command` (`publish`, `demo`, `doctor`, `comment`, or `badge`), `config`, `node-version`, `pr-comment`, `version`, `badge-out`, `badge-url`, and `badge-link`. The action passes `--force` so a repository `devEngines` pin does not stop `npx` on npm versions that enforce `devEngines` (newer npm). Node 20's bundled npm does not enforce that field. From a shell, use the same flag: `npx --force --yes @chappibunny/repolens demo`. `--ignore-engines` does not skip `devEngines`.
246
+ `action.yml` is a composite action named **RepoLens Architecture Docs**. `publish`, `demo`, and `doctor` run the published npm package (`npx --force --yes @chappibunny/repolens@1.16.4`). `comment` and `badge` run from the action checkout. Inputs are `command` (`publish`, `demo`, `doctor`, `comment`, or `badge`), `config`, `node-version`, `pr-comment`, `version`, `badge-out`, `badge-url`, and `badge-link`. The action passes `--force` so a repository `devEngines` pin does not stop `npx` on npm versions that enforce `devEngines` (newer npm). Node 20's bundled npm does not enforce that field. From a shell, use the same flag: `npx --force --yes @chappibunny/repolens demo`. `--ignore-engines` does not skip `devEngines`.
247
247
 
248
- Publishing docs (the npm package at tag `v1.16.2`):
248
+ Publishing docs (the npm package at tag `v1.16.4`):
249
249
 
250
250
  ```yaml
251
251
  name: Architecture docs
@@ -261,7 +261,7 @@ jobs:
261
261
  runs-on: ubuntu-latest
262
262
  steps:
263
263
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
264
- - uses: CHAPIBUNNY/repolens@v1.16.2
264
+ - uses: CHAPIBUNNY/repolens@v1.16.4
265
265
  with:
266
266
  command: publish
267
267
  config: .repolens.yml
@@ -269,7 +269,7 @@ jobs:
269
269
  pr-comment: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
270
270
  ```
271
271
 
272
- On `v1.15.2`, `pr-comment: true` runs the architecture diff from the action checkout and turns the published package's older short summary off. The diff lists modules, files, dependencies, the code-health delta, and new hotspots. It edits one sticky comment (`<!-- repolens:architecture-diff -->`) instead of adding a new comment each push. The footer is "Generated by RepoLens" and links to this repository and the [gallery](https://chapibunny.github.io/repolens/). Tag `v1.15.1` does not contain `command: comment`. Pin `v1.16.2` or a commit SHA in production.
272
+ On `v1.15.2`, `pr-comment: true` runs the architecture diff from the action checkout and turns the published package's older short summary off. The diff lists modules, files, dependencies, the code-health delta, and new hotspots. It edits one sticky comment (`<!-- repolens:architecture-diff -->`) instead of adding a new comment each push. The footer is "Generated by RepoLens" and links to this repository and the [gallery](https://chapibunny.github.io/repolens/). Tag `v1.15.1` does not contain `command: comment`. Pin `v1.16.4` or a commit SHA in production.
273
273
 
274
274
  ```yaml
275
275
  name: RepoLens architecture comment
@@ -285,7 +285,7 @@ jobs:
285
285
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
286
286
  with:
287
287
  fetch-depth: 0
288
- - uses: CHAPIBUNNY/repolens@v1.16.2
288
+ - uses: CHAPIBUNNY/repolens@v1.16.4
289
289
  with:
290
290
  command: comment
291
291
  ```
@@ -294,7 +294,7 @@ jobs:
294
294
 
295
295
  `pr-comment: true` on `publish`, `demo`, or `doctor` (when the action ref contains this change) runs the same comment after the package and turns the package's older comment off, so the pull request still gets one comment.
296
296
 
297
- The action is listed on the GitHub Marketplace only after a release is published with **Publish this Action to the GitHub Marketplace** checked. Those steps are in [docs/RELEASE.md](docs/RELEASE.md). The tag `v1.16.2` has to exist before `uses: CHAPIBUNNY/repolens@v1.16.2` resolves, and `@chappibunny/repolens@1.16.2` has to be on npm before `publish` can install it. `command: comment` and `command: badge` do not install that package.
297
+ The action is listed on the GitHub Marketplace only after a release is published with **Publish this Action to the GitHub Marketplace** checked. Those steps are in [docs/RELEASE.md](docs/RELEASE.md). The tag `v1.16.4` has to exist before `uses: CHAPIBUNNY/repolens@v1.16.4` resolves, and `@chappibunny/repolens@1.16.4` has to be on npm before `publish` can install it. `command: comment` and `command: badge` do not install that package.
298
298
 
299
299
  ## Add the badge to your repo
300
300
 
@@ -330,7 +330,7 @@ jobs:
330
330
  runs-on: ubuntu-latest
331
331
  steps:
332
332
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
333
- - uses: CHAPIBUNNY/repolens@v1.16.2
333
+ - uses: CHAPIBUNNY/repolens@v1.16.4
334
334
  with:
335
335
  command: badge
336
336
  badge-url: https://USER.github.io/REPO/repolens-badge.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.16.2",
3
+ "version": "1.16.4",
4
4
  "mcpName": "io.github.CHAPIBUNNY/repolens",
5
5
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
6
6
  "license": "MIT",
@@ -1,5 +1,13 @@
1
1
  // Generate documentation sections using AI
2
2
 
3
+ import { coverageLine, coverageNotice, coverageScope } from "../analyzers/language-coverage.js";
4
+
5
+ // Partial language coverage is stated next to every graph-based score, so a Rust or
6
+ // C# majority never inherits "clean boundaries" from the JavaScript/Python/Go part.
7
+ function coverageBanner(depGraph) {
8
+ const lines = coverageNotice(depGraph?.coverage, "import analysis");
9
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
10
+ }
3
11
  import { generateText, isAIEnabled } from "./provider.js";
4
12
  import {
5
13
  SYSTEM_PROMPT,
@@ -285,6 +293,7 @@ The system is organized around **${context.domains.length} functional domains**,
285
293
  const healthEmoji = healthScore >= 80 ? "🟢" : healthScore >= 60 ? "🟡" : "🔴";
286
294
 
287
295
  output += `**Overall Health: ${healthEmoji} ${healthScore}/100**\n\n`;
296
+ output += coverageBanner(depGraph);
288
297
  output += `| Metric | Value | Status |\n`;
289
298
  output += `|--------|-------|--------|\n`;
290
299
  output += `| Internal imports | ${stats.totalEdges} edges | ${stats.totalEdges > 0 ? "✅ Connected" : "⚠️ Isolated"} |\n`;
@@ -296,7 +305,7 @@ The system is organized around **${context.domains.length} functional domains**,
296
305
  // Key insights
297
306
  output += `\n### Key Insights\n\n`;
298
307
  if (stats.cycles === 0) {
299
- output += `- ✅ **Clean module boundaries** — No circular dependencies detected, enabling safe refactoring and testing.\n`;
308
+ output += `- ✅ **Clean module boundaries** — No circular dependencies detected${coverageScope(depGraph.coverage)}, enabling safe refactoring and testing.\n`;
300
309
  } else {
301
310
  output += `- ${cycleInsight(stats)}\n`;
302
311
  }
@@ -311,6 +320,8 @@ The system is organized around **${context.domains.length} functional domains**,
311
320
  if (orphanRatio(stats) > ORPHAN_WARN_RATIO) {
312
321
  output += `- ⚠️ **Potential dead code** — ${stats.orphanFiles} files (${Math.round(stats.orphanFiles / stats.totalFiles * 100)}%) are not imported anywhere.\n`;
313
322
  }
323
+ } else if (depGraph?.coverage?.status === "unsupported") {
324
+ output += `Import-graph health is not available: ${coverageLine(depGraph.coverage, "import analysis")}\n`;
314
325
  }
315
326
 
316
327
  // Data flows summary
@@ -525,11 +536,12 @@ The system is built on a modern technology foundation:\n\n`;
525
536
 
526
537
  output += `## Dependency Graph\n\n`;
527
538
  output += `**Codebase Health: ${healthEmoji} ${healthScore}/100**\n\n`;
539
+ output += coverageBanner(depGraph);
528
540
  output += `| Metric | Value | Assessment |\n`;
529
541
  output += `|--------|-------|------------|\n`;
530
542
  output += `| Internal Imports | ${stats.totalEdges} | ${stats.totalEdges > 0 ? "Modules are connected" : "No internal imports"} |\n`;
531
543
  output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "Lean dependencies" : stats.externalDeps < 100 ? "Moderate dependencies" : "Many dependencies"} |\n`;
532
- output += `| Circular Dependencies | ${stats.cycles} | ${stats.cycles === 0 ? "✅ Clean" : "⚠️ Needs attention"} |\n`;
544
+ output += `| Circular Dependencies | ${stats.cycles} | ${stats.cycles === 0 ? `✅ Clean${coverageScope(depGraph.coverage)}` : "⚠️ Needs attention"} |\n`;
533
545
  output += `| Orphan Files | ${stats.orphanFiles} | ${orphanStatus(stats)} |\n`;
534
546
 
535
547
  if (stats.hubs && stats.hubs.length > 0) {
@@ -825,20 +837,21 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
825
837
 
826
838
  output += `\n## Dependency Health\n\n`;
827
839
  output += `**Architecture Health Score: ${healthEmoji} ${healthScore}/100**\n\n`;
840
+ output += coverageBanner(depGraph);
828
841
 
829
842
  output += `| Metric | Value | Status | Recommendation |\n`;
830
843
  output += `|--------|-------|--------|----------------|\n`;
831
844
  output += `| Import Edges | ${stats.totalEdges} | ${stats.totalEdges > 0 ? "✅" : "⚠️"} | ${stats.totalEdges > 0 ? "Healthy module connectivity" : "Consider connecting modules"} |\n`;
832
845
  output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "✅" : stats.externalDeps < 100 ? "⚠️" : "🔴"} | ${stats.externalDeps >= 100 ? "Audit for unused dependencies" : stats.externalDeps >= 50 ? "Review dependency necessity" : "Lean dependency set"} |\n`;
833
846
  const archCycle = cycleStatusCell(stats);
834
- output += `| Circular Deps | ${stats.cycles} | ${stats.cycles === 0 ? "✅" : archCycle.mark} | ${stats.cycles === 0 ? "Clean module boundaries" : archCycle.advice} |\n`;
847
+ output += `| Circular Deps | ${stats.cycles} | ${stats.cycles === 0 ? "✅" : archCycle.mark} | ${stats.cycles === 0 ? `Clean module boundaries${coverageScope(depGraph.coverage)}` : archCycle.advice} |\n`;
835
848
  output += `| Orphan Files | ${stats.orphanFiles} | ${orphanRatio(stats) > ORPHAN_WARN_RATIO ? "⚠️" : "✅"} | ${stats.orphanFiles === 0 ? "All code is connected" : orphanRatio(stats) > ORPHAN_WARN_RATIO ? "Review for dead code" : "A few unconnected files (examples, type tests, entry points) are normal"} |\n`;
836
849
 
837
850
  // Architectural strengths and concerns
838
851
  output += `\n### Architectural Strengths\n\n`;
839
852
  const strengths = [];
840
853
  if (stats.cycles === 0) {
841
- strengths.push("**Clean module boundaries** — No circular dependencies detected. This enables safe refactoring, isolated testing, and clear ownership boundaries.");
854
+ strengths.push(`**Clean module boundaries** — No circular dependencies detected${coverageScope(depGraph.coverage)}. This enables safe refactoring, isolated testing, and clear ownership boundaries.`);
842
855
  }
843
856
  if (stats.orphanFiles === 0) {
844
857
  strengths.push("**Fully connected codebase** — Every file is part of the dependency graph. No dead code or forgotten modules.");
@@ -1780,8 +1793,13 @@ function getFrameworkArchitecturalImplication(frameworks) {
1780
1793
  }
1781
1794
 
1782
1795
  // Helper: architectural implication of languages
1796
+ // Languages arrive most-files-first, so the first one is the primary language. A C#
1797
+ // repository with one docs script is not "dynamic typing".
1798
+ const STATIC_PRIMARY = new Set(["c#", "java", "kotlin", "scala", "swift", "c", "c++"]);
1799
+
1783
1800
  function getLanguageImplication(languages) {
1784
1801
  const langs = (languages || []).map(l => l.toLowerCase());
1802
+ if (STATIC_PRIMARY.has(langs[0])) return "Static typing; compiled language";
1785
1803
  if (langs.includes("typescript")) return "Static typing enables tooling and refactoring confidence";
1786
1804
  if (langs.includes("javascript")) return "Dynamic typing, flexible but requires discipline";
1787
1805
  if (langs.includes("python")) return "Readable syntax, rich ecosystem";
@@ -1892,6 +1910,7 @@ function getFrameworkLearningTip(frameworks) {
1892
1910
  // Helper: language learning tip
1893
1911
  function getLanguageLearningTip(languages) {
1894
1912
  const langs = (languages || []).map(l => l.toLowerCase());
1913
+ if (STATIC_PRIMARY.has(langs[0])) return "Strong typing — let the compiler and IDE guide navigation";
1895
1914
  if (langs.includes("typescript")) return "Strong typing — check types when debugging";
1896
1915
  if (langs.includes("javascript")) return "Dynamic typing — use console.log for runtime inspection";
1897
1916
  if (langs.includes("python")) return "Read docstrings and type hints";
@@ -3,6 +3,7 @@
3
3
  // (dep graph, JSDoc, security) into a per-module health score.
4
4
 
5
5
  import fs from "node:fs/promises";
6
+ import { computeLanguageCoverage, coverageLine, filesForAnalysis, unanalyzedLanguageNames } from "./language-coverage.js";
6
7
  import path from "node:path";
7
8
  import { info } from "../utils/logger.js";
8
9
 
@@ -250,8 +251,9 @@ function functionsForLanguage(language, source) {
250
251
  * @param {string} repoRoot - Absolute path to repo root
251
252
  * @returns {Promise<Object>} Complexity analysis results
252
253
  */
253
- export async function analyzeComplexity(files, repoRoot) {
254
- const sourceFiles = files
254
+ export async function analyzeComplexity(files, repoRoot, { coverageFiles = files } = {}) {
255
+ const coverage = computeLanguageCoverage(coverageFiles || files);
256
+ const sourceFiles = filesForAnalysis(files, coverage)
255
257
  .filter(f => languageForFile(f))
256
258
  .filter(f => !shouldSkip(f));
257
259
 
@@ -259,6 +261,7 @@ export async function analyzeComplexity(files, repoRoot) {
259
261
  files: [],
260
262
  functions: [],
261
263
  filesAnalyzed: sourceFiles.length,
264
+ coverage,
262
265
  };
263
266
 
264
267
  for (const file of sourceFiles) {
@@ -411,11 +414,20 @@ export function computeCodeHealth(complexityResult, depGraph, jsdocResult, secur
411
414
 
412
415
  const hotspots = modules.filter(m => m.score < 60).slice(0, 20);
413
416
 
414
- const summary = totalFiles > 0
417
+ const coverage = complexityResult.coverage || null;
418
+ let summary = totalFiles > 0
415
419
  ? `${totalFiles} files analyzed · Average health: ${avgScore}/100 · ${gradeDistribution.A} A, ${gradeDistribution.B} B, ${gradeDistribution.C} C, ${gradeDistribution.D} D, ${gradeDistribution.F} F`
416
420
  : "No source files analyzed.";
421
+ if (totalFiles > 0 && coverage?.status === "partial") {
422
+ summary += ` · ${unanalyzedLanguageNames(coverage)} not scored`;
423
+ }
424
+ if (totalFiles === 0 && coverage?.status === "unsupported") {
425
+ summary = `Not scored: ${coverageLine(coverage, "code health")}`;
426
+ }
417
427
 
418
- if (totalFiles === 0) {
428
+ if (totalFiles === 0 && coverage?.status === "unsupported") {
429
+ info(`Code health: not scored — ${coverageLine(coverage, "code health")}`);
430
+ } else if (totalFiles === 0) {
419
431
  info("Code health: no source files analyzed.");
420
432
  } else {
421
433
  info(`Code health: avg ${avgScore}/100 across ${totalFiles} files (${hotspots.length} hotspots)`);
@@ -424,6 +436,7 @@ export function computeCodeHealth(complexityResult, depGraph, jsdocResult, secur
424
436
  return {
425
437
  modules,
426
438
  summary,
439
+ coverage,
427
440
  stats: {
428
441
  totalFiles,
429
442
  avgScore,
@@ -406,7 +406,8 @@ function inferArchitecturalPatterns(modules, depGraph = null, projectKind = null
406
406
 
407
407
  // Structural patterns from dep graph (not keyword-based)
408
408
  if (depGraph?.stats) {
409
- if (depGraph.stats.cycles === 0 && depGraph.stats.totalEdges > 10) {
409
+ // Only a fully analyzed codebase can be called acyclic.
410
+ if (depGraph.stats.cycles === 0 && depGraph.stats.totalEdges > 10 && depGraph.coverage?.status !== "partial") {
410
411
  patterns.push("Acyclic dependency graph — clean layering");
411
412
  }
412
413
  }
@@ -162,6 +162,9 @@ export function cycleInsight(stats) {
162
162
  if (!hasCyclePriorities(stats)) {
163
163
  return `🔴 **Architectural concern** — ${stats.cycles} circular dependenc${stats.cycles === 1 ? "y" : "ies"} detected. These create tight coupling and make testing difficult.`;
164
164
  }
165
+ if ((stats.highCycles || 0) === 0 && (stats.mediumCycles || 0) > 0) {
166
+ return `🟡 **Circular dependencies** — ${stats.cycles} detected. Small cycle; review. Priority: Medium.`;
167
+ }
165
168
  if ((stats.highCycles || 0) === 0) {
166
169
  return `🟡 **Circular dependencies** — ${stats.cycles} detected. mutual recursion, probably intentional; verify. Priority: Low.`;
167
170
  }