@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 +57 -0
- package/README.md +8 -8
- package/package.json +1 -1
- package/src/ai/generate-sections.js +23 -4
- package/src/analyzers/complexity-analyzer.js +17 -4
- package/src/analyzers/context-builder.js +2 -1
- package/src/analyzers/cycle-priority.js +3 -0
- package/src/analyzers/dependency-graph.js +265 -34
- package/src/analyzers/flow-inference.js +10 -1
- package/src/analyzers/graphql-analyzer.js +80 -32
- package/src/analyzers/import-parsers.js +47 -14
- package/src/analyzers/language-coverage.js +222 -0
- package/src/analyzers/ts-elision.js +921 -0
- package/src/analyzers/workspace-packages.js +105 -0
- package/src/badge.js +59 -18
- package/src/core/scan.js +10 -4
- package/src/docs/generate-doc-set.js +11 -3
- package/src/help.js +8 -3
- package/src/renderers/render.js +14 -2
- package/src/renderers/renderAnalysis.js +34 -2
- package/src/utils/js-lexer.js +146 -0
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,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 ?
|
|
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 ?
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|