@chappibunny/repolens 1.15.5 → 1.16.1

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,67 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.16.1
6
+
7
+ Fixes three problems found by running 1.16.0 on orval-labs/orval (a TypeScript monorepo whose test snapshots and samples dwarf the packages).
8
+
9
+ ### Scan defaults
10
+
11
+ - Snapshot directories (`__snapshots__`, `*.snap`), sample / fixture / example / e2e-fixture directories, and `generated/` trees are dev tooling. They are left out of the file count, orphan detection, and the type graph unless `scan.include_dev_artifacts: true`. Other `scan.ignore` patterns still apply. Orval's second copy of the snapshot tree lives in `tests/generated`, so `**/generated/**` is part of that default (it was already in the `init` ignore list).
12
+ - `repolens badge` skips the same directories. It does not read `.repolens.yml`.
13
+
14
+ ### Cycle priority
15
+
16
+ - A cycle is no longer always "Priority: High refactor".
17
+ - **Low — mutual recursion, probably intentional; verify** when the cycle stays inside two parent directories that import each other both ways (at least two runtime edges each way) and both directions call the imported bindings (at least two calls each way). Orval's core getters and resolvers are this shape.
18
+ - **Low — type-only imports; verify** when every edge is `import type` or `export type`. Those cycles are not runtime edges and are not counted in `stats.cycles`.
19
+ - **High refactor** is kept for a cycle that touches three or more parent directories.
20
+ - A two-directory cycle with only one import each way is **Medium**.
21
+ - The health score subtracts points for high and medium cycles. Low-priority cycles do not. Stats that do not carry the priority split keep the previous per-cycle penalty.
22
+ - The heuristic is documented in `src/analyzers/cycle-priority.js`, [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), and [docs/KNOWN_ISSUES.md](docs/KNOWN_ISSUES.md).
23
+
24
+ ### npx and devEngines
25
+
26
+ - npm versions that enforce `devEngines` (newer npm) check that field on the package.json in the current directory before `npx` starts the requested package. A repo that pins a Node or package-manager the host does not meet exits `EBADDEVENGINES`. Node 20's bundled npm does not enforce `devEngines`. RepoLens never gets to run when the check fails, so this cannot be fixed inside the CLI.
27
+ - `--ignore-engines` is not an npm flag. On npm versions that enforce `devEngines`, npm warns that it is an unknown config and still errors.
28
+ - `--force` turns that failure into a warning and runs the command. The GitHub Action passes `--force` on its `npx` step. A manual run is `npx --force --yes @chappibunny/repolens demo`. A global install, or `node node_modules/@chappibunny/repolens/bin/repolens.js`, never asks npm to read the scanned repo's `devEngines`.
29
+ - See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
30
+
31
+ ## 1.16.0
32
+
33
+ ### GitHub App
34
+
35
+ - `repolens app` self-hosts a GitHub App webhook. It checks `X-Hub-Signature-256`, acknowledges `installation` and `installation_repositories`, and on `pull_request` `opened`, `synchronize`, `reopened`, and `ready_for_review` runs the existing architecture diff. It posts a Check Run named RepoLens (the same summary as the sticky comment) and updates that comment. A finished diff is a successful check. The check fails only when the analysis throws
36
+ - Manifest: `app.yml` and `github-app-manifest.json`, also `repolens app --manifest`. Permissions are checks write, pull requests write, contents read, and metadata read. The hook URL in the repo is a placeholder (`https://example.com/api/github/webhooks`). This release does not register an app and CI does not need new secrets
37
+ - The server answers 202 before the clone and scoring finish, because GitHub allows about 10 seconds for the HTTP response. Setup is in [docs/GITHUB_APP.md](docs/GITHUB_APP.md)
38
+
39
+ ### Security
40
+
41
+ - Route parameters in `src/renderers/renderDiff.js` no longer use `/\[(.*?)\]/g`. That lazy pattern is polynomial on a long string of `[` with no `]` (CodeQL `js/polynomial-redos`, alert #6). The replacement is a linear scan, including the same pattern in `src/core/scan.js` and the route-group labels in `src/renderers/renderMap.js`. A regression test feeds 100,000 brackets and expects a fast result
42
+
43
+ ### Output accuracy
44
+
45
+ Hands-on `repolens demo` on expressjs/express, pallets/flask, and spf13/cobra:
46
+
47
+ - Express 5 has no `main` field. A description that says library, SDK, parser, toolkit, or framework is a library. Express reads as a library, not an API service
48
+ - `app.get('query parser fn')` is a setting lookup, not a route. Comment lines are skipped. Routes in tests, examples, and docs are not the product API. The Express route map is skipped
49
+ - A flat package at the repository root is named after the directory (`cobra`, not `root`). The system map connects that package to `doc/` (one relationship). Files with no directory belong to that flat module
50
+ - External imports that appear only in tests or examples are development tooling even when there is no `package.json`. Flask no longer lists `site_app` and the other test fixtures as runtime packages. Cobra lists `pflag` and `mousetrap` as runtime, and `go-md2man` and `yaml` as doc tooling
51
+ - Flask hubs are `globals.py`, `helpers.py`, `typing.py`, and `signals.py`. The three cycles (`__init__` → `app` → `cli`, and the templating / sansio chains) are real import cycles
52
+
53
+ ### Dependencies
54
+
55
+ - `js-yaml` 4.3.2 → 5.5.0. Call the named `load` export. The YAML 1.2 Core schema still treats `true`/`false` as booleans and `yes`/`no` as strings. The merge key (`<<`) is enabled. `maxAliases` caps alias expansion. An empty file still returns `undefined`
56
+ - `@sentry/node` 10.76.2 → 11.9.0 (`^11.8.0`). It is still optional. Its own engines exclude Node 18 (`>=20.19` or `>=22.12`). `npm ci` on Node 18 warns and continues, and a failed import still disables telemetry instead of crashing the CLI
57
+ - Vitest 4.1.12 → 5.0.5. Its declared engines are `^22.12 || ^24 || >=26`. The runner starts on Node 20 and does not start on Node 18 (`util.styleText`). CI still runs `npm test` on Node 20 and 22, and `scripts/node18-runtime.js` on Node 18. `engines` stays `>=18`
58
+ - `dotenv` 18.0.8 → 18.0.9 and `@modelcontextprotocol/sdk` 1.32.1 → 1.32.2
59
+ - GitHub Actions: `actions/checkout` pin is the v6.0.3 release commit `df4cb1c`, `actions/setup-node` v7.1.0 (`949feb2`), `softprops/action-gh-release` v3.0.3 (`efb3536`). `npm audit` reports no vulnerabilities
60
+
61
+ ### Docs
62
+
63
+ - Roadmap item 1 (GitHub App) is shipped in 1.16.0. Java and C# language support is now What's Next item 1
64
+ - The note that a chat answer only covers what someone thought to ask, and that grounded AI Q&A sits on top of the full map, stays in the roadmap, README, known issues, and copilot instructions
65
+
5
66
  ## 1.15.5
6
67
 
7
68
  Fixes twelve output problems found by running 1.15.4 on mattiasw/ExifReader (a plain-JavaScript library with a flat `src/` and a build-helper `bin/`), and the two leftovers from 1.15.4.
package/README.md CHANGED
@@ -119,6 +119,7 @@ Run `npx @chappibunny/repolens migrate` to automatically update your workflow fi
119
119
  | 🤖 **Autonomous** | Runs on every push via GitHub Actions — docs stay evergreen |
120
120
  | 👥 **Multi-Audience** | Technical docs + stakeholder-readable overviews from one scan |
121
121
  | 📤 **Multi-Publisher** | Notion, Confluence, GitHub Wiki, Obsidian, Markdown — together or one at a time |
122
+ | 🗺️ **Shows what you didn't ask** | An LLM only answers the question it is given, and a newcomer doesn't know what to ask. RepoLens publishes the whole map (every module, cycle and hub) on every PR, the same way each time, with no tokens spent per question |
122
123
  | 🧠 **AI-Assisted** | Optional AI with zero-hallucination policy (structured context only) |
123
124
  | 🔍 **Smart Detection** | Frameworks, domains, data flows, dependencies, drift — all automatic |
124
125
  | 🔌 **Extensible** | Plugin system for custom renderers, publishers, and hooks |
@@ -189,6 +190,7 @@ Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki,
189
190
  | `npx @chappibunny/repolens mcp` | Start the stdio MCP server (`repolens-mcp` is the same server) |
190
191
  | `npx @chappibunny/repolens badge` | shields.io code-health JSON, SVG, or markdown. Exits 2 when nothing is scored |
191
192
  | `npx @chappibunny/repolens comment --base <ref> --head <ref>` | Architecture diff (files, modules, dependencies, health delta, new hotspots) |
193
+ | `npx @chappibunny/repolens app` | Self-host the GitHub App webhook. `--manifest` prints the manifest |
192
194
 
193
195
  > **Note:** If you installed globally with `npm install -g @chappibunny/repolens`, you can use the shorter `repolens <command>` form. Init's next-steps text uses that shorter form when the command on your PATH is the global install.
194
196
  > **Warning:** Do not run bare `npx repolens` — there is an unrelated `repolens@0.0.1` placeholder package on npm. Always use the scoped name `@chappibunny/repolens`.
@@ -199,7 +201,7 @@ Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki,
199
201
  |---|---|
200
202
  | `0` | Success, including a cancelled wizard or help with no other work |
201
203
  | `1` | Runtime error, or an unknown command |
202
- | `2` | Invalid flags, missing config, target directory not found, a failed `doctor` check (including 0 files matched), `demo` or `publish` scanned 0 files, `watch` has nothing to watch (missing `module_roots`, or `scan.include` matches no files), `badge` scored 0 source files, `comment` is missing a base or head ref, `publish --strict` is missing publisher credentials, or `feedback` has no terminal |
204
+ | `2` | Invalid flags, missing config, target directory not found, a failed `doctor` check (including 0 files matched), `demo` or `publish` scanned 0 files, `watch` has nothing to watch (missing `module_roots`, or `scan.include` matches no files), `badge` scored 0 source files, `comment` is missing a base or head ref, `publish --strict` is missing publisher credentials, `app` is missing its id, webhook secret, or private key, or `feedback` has no terminal |
203
205
 
204
206
  `--target` must already exist. A missing path exits 2 with `Target directory not found: <path>` before any scan. `repolens <command> --help` prints that command's flags. The ASCII banner is omitted when stdout is not a terminal or `CI` is set.
205
207
 
@@ -241,9 +243,9 @@ If `npx` reports `Unknown command: mcp`, an `.npmrc` with `min-release-age` (or
241
243
 
242
244
  ## GitHub Action
243
245
 
244
- `action.yml` is a composite action named **RepoLens Architecture Docs**. `publish`, `demo`, and `doctor` run the published npm package (`npx --yes @chappibunny/repolens@1.15.5`). `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`.
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.1`). `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`.
245
247
 
246
- Publishing docs (the npm package at tag `v1.15.5`):
248
+ Publishing docs (the npm package at tag `v1.16.1`):
247
249
 
248
250
  ```yaml
249
251
  name: Architecture docs
@@ -258,8 +260,8 @@ jobs:
258
260
  docs:
259
261
  runs-on: ubuntu-latest
260
262
  steps:
261
- - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
262
- - uses: CHAPIBUNNY/repolens@v1.15.5
263
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
264
+ - uses: CHAPIBUNNY/repolens@v1.16.1
263
265
  with:
264
266
  command: publish
265
267
  config: .repolens.yml
@@ -267,7 +269,7 @@ jobs:
267
269
  pr-comment: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
268
270
  ```
269
271
 
270
- 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.15.5` 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.1` or a commit SHA in production.
271
273
 
272
274
  ```yaml
273
275
  name: RepoLens architecture comment
@@ -280,10 +282,10 @@ jobs:
280
282
  architecture:
281
283
  runs-on: ubuntu-latest
282
284
  steps:
283
- - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
285
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
284
286
  with:
285
287
  fetch-depth: 0
286
- - uses: CHAPIBUNNY/repolens@v1.15.5
288
+ - uses: CHAPIBUNNY/repolens@v1.16.1
287
289
  with:
288
290
  command: comment
289
291
  ```
@@ -292,7 +294,7 @@ jobs:
292
294
 
293
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.
294
296
 
295
- 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.15.5` has to exist before `uses: CHAPIBUNNY/repolens@v1.15.5` resolves, and `@chappibunny/repolens@1.15.5` 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.1` has to exist before `uses: CHAPIBUNNY/repolens@v1.16.1` resolves, and `@chappibunny/repolens@1.16.1` has to be on npm before `publish` can install it. `command: comment` and `command: badge` do not install that package.
296
298
 
297
299
  ## Add the badge to your repo
298
300
 
@@ -327,8 +329,8 @@ jobs:
327
329
  badge:
328
330
  runs-on: ubuntu-latest
329
331
  steps:
330
- - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
331
- - uses: CHAPIBUNNY/repolens@v1.15.5
332
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
333
+ - uses: CHAPIBUNNY/repolens@v1.16.1
332
334
  with:
333
335
  command: badge
334
336
  badge-url: https://USER.github.io/REPO/repolens-badge.json
@@ -357,7 +359,7 @@ Gallery repositories already publish this. Each one has a [score page](https://c
357
359
 
358
360
  Published system maps are Unicode diagrams drawn from import edges. RepoLens does not emit Mermaid.
359
361
 
360
- The publisher set is Notion, Confluence, GitHub Wiki, Obsidian, and Markdown (`src/publishers`, 7 files). 1.14.0 adds the MCP server, the Marketplace action, and the docs badge. 1.14.1 sets the registry name to `io.github.CHAPIBUNNY/repolens`. 1.14.2 adds the gallery link to the badge line, `glama.json`, and `CITATION.cff`. 1.14.3 adds Cline setup docs and `llms-install.md`. 1.15.0 detects repository languages in `init --quick`, refuses empty scans, and adds per-command help. 1.15.1 makes `watch` exit 2 when those globs match nothing, and scores code health for Python and Go as well as JavaScript and TypeScript. 1.15.2 reads Python and Go imports in the dependency graph, skips comment and type-only imports, adds `repolens badge` and `repolens comment`, and adds gallery explained pages plus `llms.txt`. 1.15.3 skips Python `TYPE_CHECKING` imports, omits Node.js built-ins from external packages, groups Go hubs by package, and ignores test files as architecture-pattern evidence. 1.15.4 names the project from `package.json`, scores maturity from version, history, tests, CI, and docs, reports circular dependencies as findings, drops self-imports and noise data flows, and stops classifying `index` and `core` files by name. 1.15.5 reads a flat `src/` as one module, tells a library from a CLI by its `package.json`, splits runtime from development packages, keeps tests, examples, and build scripts out of hubs and security severity, and skips empty Route Map and GraphQL pages. Layout and counts are in [ARCHITECTURE.md](docs/ARCHITECTURE.md).
362
+ The publisher set is Notion, Confluence, GitHub Wiki, Obsidian, and Markdown (`src/publishers`, 7 files). 1.14.0 adds the MCP server, the Marketplace action, and the docs badge. 1.14.1 sets the registry name to `io.github.CHAPIBUNNY/repolens`. 1.14.2 adds the gallery link to the badge line, `glama.json`, and `CITATION.cff`. 1.14.3 adds Cline setup docs and `llms-install.md`. 1.15.0 detects repository languages in `init --quick`, refuses empty scans, and adds per-command help. 1.15.1 makes `watch` exit 2 when those globs match nothing, and scores code health for Python and Go as well as JavaScript and TypeScript. 1.15.2 reads Python and Go imports in the dependency graph, skips comment and type-only imports, adds `repolens badge` and `repolens comment`, and adds gallery explained pages plus `llms.txt`. 1.15.3 skips Python `TYPE_CHECKING` imports, omits Node.js built-ins from external packages, groups Go hubs by package, and ignores test files as architecture-pattern evidence. 1.15.4 names the project from `package.json`, scores maturity from version, history, tests, CI, and docs, reports circular dependencies as findings, drops self-imports and noise data flows, and stops classifying `index` and `core` files by name. 1.15.5 reads a flat `src/` as one module, tells a library from a CLI by its `package.json`, splits runtime from development packages, keeps tests, examples, and build scripts out of hubs and security severity, and skips empty Route Map and GraphQL pages. 1.16.0 adds `repolens app` (a self-hosted GitHub App webhook), names a flat package after its directory, treats a framework description as a library when `main` is absent, and keeps test and example routes out of the product API. 1.16.1 skips snapshot, sample, fixture, example, and generated trees in the file count, orphan list, and type graph, and labels a call-heavy two-folder cycle as mutual recursion rather than a high-priority refactor. Layout and counts are in [ARCHITECTURE.md](docs/ARCHITECTURE.md).
361
363
 
362
364
  ### System Overview (Technical Profile)
363
365
 
@@ -419,6 +421,7 @@ When you open a pull request, RepoLens posts:
419
421
  | [Migration](docs/MIGRATION.md) | Upgrading from v0.3.0 or earlier |
420
422
  | [Stability](docs/STABILITY.md) | API contract and semver guarantees |
421
423
  | [MCP server](docs/MCP.md) | stdio server for Cursor, Claude, and VS Code |
424
+ | [GitHub App](docs/GITHUB_APP.md) | Self-host the webhook: manifest, env vars, check run |
422
425
 
423
426
  ---
424
427
 
@@ -445,11 +448,11 @@ v1.0+ features complete — CLI, config schema, and plugin interface are frozen.
445
448
  - [x] MCP server (`repolens mcp`) for Cursor, Claude, and VS Code
446
449
  - [x] GitHub Action `RepoLens Architecture Docs` (`action.yml`)
447
450
  - [x] Docs by RepoLens badge on generated Markdown
451
+ - [x] GitHub App webhook (`repolens app`) for a check run and the architecture comment
448
452
 
449
453
  **Next:**
450
- - [ ] GitHub App for automated setup and status checks
451
454
  - [ ] Java and C# language support
452
- - [ ] Grounded AI explanations and Q&A (`repolens ask`, PR comment, MCP, VS Code hover)
455
+ - [ ] Grounded AI explanations and Q&A (`repolens ask`, PR comment, MCP, VS Code hover), layered on the always-published map and not a replacement for it
453
456
  - [ ] Slack notifications
454
457
  - [ ] Custom webhook support
455
458
  - [ ] Cross-repository architecture analysis
package/app.yml ADDED
@@ -0,0 +1,23 @@
1
+ # GitHub App manifest for RepoLens.
2
+ # Register from github-app-manifest.json (same fields) after you replace
3
+ # hook_attributes.url with the public URL of `repolens app`.
4
+ # Permissions stay minimal: checks write, pull requests write, contents read,
5
+ # metadata read. This file does not create the app.
6
+
7
+ name: RepoLens
8
+ description: Posts an architecture diff as a check run and one sticky pull request comment.
9
+ url: https://github.com/CHAPIBUNNY/repolens
10
+ hook_attributes:
11
+ url: https://example.com/api/github/webhooks
12
+ active: true
13
+ redirect_url: https://github.com/CHAPIBUNNY/repolens
14
+ public: false
15
+ default_permissions:
16
+ checks: write
17
+ pull_requests: write
18
+ contents: read
19
+ metadata: read
20
+ default_events:
21
+ - installation
22
+ - installation_repositories
23
+ - pull_request
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "RepoLens",
3
+ "description": "Posts an architecture diff as a check run and one sticky pull request comment.",
4
+ "url": "https://github.com/CHAPIBUNNY/repolens",
5
+ "hook_attributes": {
6
+ "url": "https://example.com/api/github/webhooks",
7
+ "active": true
8
+ },
9
+ "redirect_url": "https://github.com/CHAPIBUNNY/repolens",
10
+ "public": false,
11
+ "default_permissions": {
12
+ "checks": "write",
13
+ "pull_requests": "write",
14
+ "contents": "read",
15
+ "metadata": "read"
16
+ },
17
+ "default_events": [
18
+ "installation",
19
+ "installation_repositories",
20
+ "pull_request"
21
+ ]
22
+ }
package/llms-full.txt CHANGED
@@ -24,6 +24,7 @@ Agent-oriented install steps, including the Cline MCP snippet, are in llms-insta
24
24
  - `repolens watch` regenerates docs on change and exits 2 when nothing matches.
25
25
  - `repolens badge` prints a shields.io endpoint JSON document for the code-health score (0–100). `--format svg` prints a square SVG. `--format markdown --url https://host/repolens-badge.json` prints a README snippet. `--format all --out repolens-badge.json` writes JSON and SVG. Exit 2 when no JavaScript, TypeScript, Python, or Go files were scored. The score is not reported as 100 when zero files match.
26
26
  - `repolens comment --base <ref> --head <ref>` compares those commits. It prints markdown locally. In GitHub Actions, with `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and a pull_request event, it creates or updates one comment.
27
+ - `repolens app` self-hosts a GitHub App webhook. It verifies `X-Hub-Signature-256`, posts a Check Run named RepoLens, and updates the sticky architecture comment. `repolens app --manifest` prints the manifest and needs no secrets. Listening needs `REPOLENS_APP_ID`, `REPOLENS_WEBHOOK_SECRET`, and a private key. See docs/GITHUB_APP.md.
27
28
  - `repolens mcp` starts the stdio MCP server. `repolens-mcp` is the same entry.
28
29
 
29
30
  Exit codes: 0 success, 1 runtime error or unknown command, 2 validation failure (bad flags, missing config, empty scan, and the other cases in the README).
package/llms-install.md CHANGED
@@ -21,3 +21,5 @@
21
21
  12. CLI (1.15.3): Python imports under `if TYPE_CHECKING:` (including `typing.TYPE_CHECKING` and `import typing as t`) are not cycle edges. Node.js built-ins are not external packages. Go hub rows are packages. Test files are not architecture-pattern evidence, and "API route pattern" requires a route directory.
22
22
  13. CLI (1.15.4): the project name comes from `package.json` or the real directory (never `.`). Maturity uses version, history, tests, CI, and docs. Circular dependencies are findings, not architectural patterns. A package's own name is not an external dependency. `index` and `main` files are not classified by name. Noise data flows are dropped.
23
23
  14. CLI (1.15.5): a flat `src/` is one module. A package with `main`, `module`, or `exports` is a library, not a CLI. Hubs and security severity ignore tests, examples, and build scripts. External packages are split into runtime and development tooling. Route Map and GraphQL Schema are skipped when empty.
24
+ 15. CLI (1.16.0): `repolens app` self-hosts a GitHub App webhook (manifest in `app.yml`, check run plus the sticky architecture comment). A framework description is a library even without `main`. A flat package is named after its directory. Test and example routes are not the product API. Test-only imports are dev tooling on Python and Go.
25
+ 16. CLI (1.16.1): snapshot, sample, fixture, example, and generated directories are out of the file count, orphan list, and type graph unless `scan.include_dev_artifacts` is true. A two-folder call-heavy cycle is "mutual recursion, probably intentional; verify", not "Priority: High refactor". On npm versions that enforce `devEngines` (newer npm), `npx` inside a repo with `devEngines` needs `--force`. Node 20's bundled npm does not enforce that field. `--ignore-engines` does not skip the check.
package/llms.txt CHANGED
@@ -17,6 +17,7 @@ RepoLens is the npm package `@chappibunny/repolens` (Node.js 18+). It is a CLI,
17
17
  - [publish](https://github.com/CHAPIBUNNY/repolens/blob/main/docs/INSTALLATION.md): `npx @chappibunny/repolens publish` scans and publishes
18
18
  - [badge](https://github.com/CHAPIBUNNY/repolens/blob/main/README.md): `npx @chappibunny/repolens badge` prints shields.io code-health JSON, SVG, or markdown
19
19
  - [comment](https://github.com/CHAPIBUNNY/repolens/blob/main/README.md): `npx @chappibunny/repolens comment --base <ref> --head <ref>` prints an architecture diff
20
+ - [app](https://github.com/CHAPIBUNNY/repolens/blob/main/docs/GITHUB_APP.md): `npx @chappibunny/repolens app` self-hosts the GitHub App webhook. `--manifest` prints the manifest. No credentials are required to print it
20
21
  - [doctor](https://github.com/CHAPIBUNNY/repolens/blob/main/docs/INSTALLATION.md): `npx @chappibunny/repolens doctor` validates setup
21
22
  - [mcp](https://github.com/CHAPIBUNNY/repolens/blob/main/docs/MCP.md): `npx -y @chappibunny/repolens mcp` starts the stdio server
22
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.15.5",
3
+ "version": "1.16.1",
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",
@@ -18,7 +18,9 @@
18
18
  "repolens-mcp": "bin/repolens-mcp.js"
19
19
  },
20
20
  "files": [
21
+ "app.yml",
21
22
  "bin",
23
+ "github-app-manifest.json",
22
24
  "src",
23
25
  "package.json",
24
26
  "README.md",
@@ -86,18 +88,18 @@
86
88
  "release:major": "npm version major"
87
89
  },
88
90
  "dependencies": {
89
- "@modelcontextprotocol/sdk": "^1.32.1",
90
- "dotenv": "^18.0.8",
91
+ "@modelcontextprotocol/sdk": "^1.32.2",
92
+ "dotenv": "^18.0.9",
91
93
  "fast-glob": "^3.3.3",
92
- "js-yaml": "^4.3.2",
94
+ "js-yaml": "^5.5.0",
93
95
  "zod": "4.6.7"
94
96
  },
95
97
  "optionalDependencies": {
96
- "@sentry/node": "^10.42.0"
98
+ "@sentry/node": "^11.8.0"
97
99
  },
98
100
  "devDependencies": {
99
101
  "tinyexec": "^1.3.1",
100
- "vitest": "^4.1.11"
102
+ "vitest": "^5.0.5"
101
103
  },
102
104
  "overrides": {
103
105
  "yauzl": ">=3.2.1",
@@ -16,6 +16,7 @@ import { identifyFlowDependencies, filterNoiseFlows } from "../analyzers/flow-in
16
16
  import { isCliProject, isLibraryProject } from "../analyzers/context-builder.js";
17
17
  import { scoreMaturity } from "../analyzers/maturity.js";
18
18
  import { isNonProductPath, isTestPath } from "../utils/path-roles.js";
19
+ import { cycleConcern, cycleHealthPenalty, cycleInsight, cycleStatusCell } from "../analyzers/cycle-priority.js";
19
20
  import { info, warn } from "../utils/logger.js";
20
21
 
21
22
  // Strip conversational patterns that LLMs sometimes inject into documentation
@@ -288,7 +289,8 @@ The system is organized around **${context.domains.length} functional domains**,
288
289
  output += `|--------|-------|--------|\n`;
289
290
  output += `| Internal imports | ${stats.totalEdges} edges | ${stats.totalEdges > 0 ? "✅ Connected" : "⚠️ Isolated"} |\n`;
290
291
  output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "✅ Manageable" : stats.externalDeps < 100 ? "⚠️ Consider audit" : "🔴 High dependency count"} |\n`;
291
- output += `| Circular Dependencies | ${stats.cycles === 0 ? "cycle-free" : stats.cycles} | ${stats.cycles === 0 ? "✅ None" : `🔴 ${stats.cycles} detected`} |\n`;
292
+ const cycleCell = cycleStatusCell(stats);
293
+ output += `| Circular Dependencies | ${stats.cycles === 0 ? "cycle-free" : stats.cycles} | ${stats.cycles === 0 ? "✅ None" : `${cycleCell.mark} ${stats.cycles} detected`} |\n`;
292
294
  output += `| Orphan Files | ${stats.orphanFiles} | ${orphanStatus(stats)} |\n`;
293
295
 
294
296
  // Key insights
@@ -296,7 +298,7 @@ The system is organized around **${context.domains.length} functional domains**,
296
298
  if (stats.cycles === 0) {
297
299
  output += `- ✅ **Clean module boundaries** — No circular dependencies detected, enabling safe refactoring and testing.\n`;
298
300
  } else {
299
- output += `- 🔴 **Architectural concern** — ${stats.cycles} circular dependenc${stats.cycles === 1 ? "y" : "ies"} detected. These create tight coupling and make testing difficult.\n`;
301
+ output += `- ${cycleInsight(stats)}\n`;
300
302
  }
301
303
  if (stats.hubs && stats.hubs.length > 0) {
302
304
  const topHub = stats.hubs[0];
@@ -828,7 +830,8 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
828
830
  output += `|--------|-------|--------|----------------|\n`;
829
831
  output += `| Import Edges | ${stats.totalEdges} | ${stats.totalEdges > 0 ? "✅" : "⚠️"} | ${stats.totalEdges > 0 ? "Healthy module connectivity" : "Consider connecting modules"} |\n`;
830
832
  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`;
831
- output += `| Circular Deps | ${stats.cycles} | ${stats.cycles === 0 ? "✅" : "🔴"} | ${stats.cycles === 0 ? "Clean module boundaries" : "Refactor to break cycles"} |\n`;
833
+ const archCycle = cycleStatusCell(stats);
834
+ output += `| Circular Deps | ${stats.cycles} | ${stats.cycles === 0 ? "✅" : archCycle.mark} | ${stats.cycles === 0 ? "Clean module boundaries" : archCycle.advice} |\n`;
832
835
  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`;
833
836
 
834
837
  // Architectural strengths and concerns
@@ -856,9 +859,8 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
856
859
 
857
860
  // Concerns
858
861
  const concerns = [];
859
- if (stats.cycles > 0) {
860
- concerns.push(`**Circular dependencies detected** — ${stats.cycles} circular dependenc${stats.cycles === 1 ? 'y' : 'ies'} create tight coupling, complicate testing, and hinder refactoring. Priority: High.`);
861
- }
862
+ const concern = cycleConcern(stats);
863
+ if (concern) concerns.push(concern);
862
864
  if (stats.orphanFiles > stats.totalFiles * 0.2) {
863
865
  concerns.push(`**High orphan file ratio** — ${stats.orphanFiles}/${stats.totalFiles} files (${Math.round(stats.orphanFiles / stats.totalFiles * 100)}%) are not imported. Consider removing dead code. Priority: Medium.`);
864
866
  }
@@ -1625,8 +1627,9 @@ function externalPackagesLabel(stats) {
1625
1627
  function calculateHealthScore(stats, context) {
1626
1628
  let score = 100;
1627
1629
 
1628
- // Circular dependencies are a major concern
1629
- if (stats.cycles > 0) score -= Math.min(30, stats.cycles * 10);
1630
+ // High and medium cycles cost points. Mutual recursion and type-only cycles do not.
1631
+ // A stats object without the priority split keeps the older per-cycle penalty.
1632
+ score -= cycleHealthPenalty(stats);
1630
1633
 
1631
1634
  // Orphan files indicate potential dead code
1632
1635
  const ratio = orphanRatio(stats);
@@ -30,20 +30,28 @@ export function computeModuleDepMetrics(depGraph) {
30
30
 
31
31
  /**
32
32
  * Find the module a file belongs to: the module with the longest key that is a path
33
- * prefix of the file. Files at the repository root belong to `root`.
33
+ * prefix of the file. Files at the repository root belong to the flat module
34
+ * (the directory name, or the legacy key `root`).
34
35
  */
36
+ function flatModuleKey(modules) {
37
+ const marked = modules.find((mod) => mod.flat);
38
+ if (marked) return marked.key;
39
+ return modules.some((mod) => mod.key === "root") ? "root" : null;
40
+ }
41
+
35
42
  function buildModuleLookup(modules) {
36
43
  const keys = modules.map((m) => m.key).sort((x, y) => y.length - x.length);
44
+ const flatKey = flatModuleKey(modules);
37
45
  const cache = new Map();
38
46
  return (file) => {
39
47
  const norm = String(file || "").replace(/\\/g, "/");
40
48
  if (cache.has(norm)) return cache.get(norm);
41
49
  let found = null;
42
50
  for (const key of keys) {
43
- if (key === "root") continue;
51
+ if (key === flatKey) continue;
44
52
  if (norm === key || norm.startsWith(`${key}/`)) { found = key; break; }
45
53
  }
46
- if (!found && !norm.includes("/") && keys.includes("root")) found = "root";
54
+ if (!found && !norm.includes("/") && flatKey) found = flatKey;
47
55
  cache.set(norm, found);
48
56
  return found;
49
57
  };
@@ -0,0 +1,169 @@
1
+ // Cycle priority.
2
+ //
3
+ // Not every circular import is a refactor. The heuristic:
4
+ //
5
+ // 1. Type-only. Every edge in the cycle is `import type` or `export type`.
6
+ // There is no runtime cycle. Priority: Low. Wording: type-only imports; verify.
7
+ // These stay out of the runtime cycle list (`cycles` / `stats.cycles`).
8
+ //
9
+ // 2. Recursive-descent pair (mutual recursion). The cycle stays inside two parent
10
+ // directories, and those two directories import each other in both directions
11
+ // with many edges (at least 2 each way) and both directions are call-heavy
12
+ // (at least 2 calls of the imported bindings each way). Schema getters calling
13
+ // resolvers that call getters is this shape. The pair is small: two folders,
14
+ // not a chain across the repo. Priority: Low. Wording: mutual recursion,
15
+ // probably intentional; verify.
16
+ //
17
+ // 3. Large multi-module cycle. The cycle touches 3 or more parent directories.
18
+ // Priority: High refactor.
19
+ //
20
+ // 4. Anything else (a short accidental cycle, one import each way, no real call
21
+ // traffic) is Medium: review, not a blanket high-priority refactor.
22
+ //
23
+ // "Folder" is the file's parent directory. `index.ts` stays in its own directory
24
+ // after the graph strips the `/index` suffix. Call counts use ESM bindings
25
+ // (`import { fn }` then `fn(` or `ns.fn(`). A `require()` edge has no binding
26
+ // names, so it does not count as call-heavy.
27
+
28
+ export const MUTUAL_EDGE_MINIMUM = 2;
29
+ export const MUTUAL_CALL_MINIMUM = 2;
30
+ export const HIGH_FOLDER_MINIMUM = 3;
31
+
32
+ export function folderOfFile(file) {
33
+ const norm = String(file || "").replace(/\\/g, "/");
34
+ const slash = norm.lastIndexOf("/");
35
+ if (slash <= 0) return ".";
36
+ return norm.slice(0, slash);
37
+ }
38
+
39
+ function cycleNodes(path) {
40
+ if (!Array.isArray(path) || path.length < 2) return [];
41
+ return path[0] === path[path.length - 1] ? path.slice(0, -1) : path.slice();
42
+ }
43
+
44
+ function report(path, priority, kind, label) {
45
+ return { path, priority, kind, label, priorityLine: priorityLine(priority, label) };
46
+ }
47
+
48
+ export function priorityLine(priority, label) {
49
+ if (priority === "high") return "Priority: High refactor";
50
+ const text = label || "review";
51
+ const name = priority === "low" ? "Low" : "Medium";
52
+ return `Priority: ${name} — ${text}`;
53
+ }
54
+
55
+ /**
56
+ * Classify one runtime cycle.
57
+ * `edges` are runtime import edges: { from, to, fromFile, toFile, calls }.
58
+ * `fileForKey(key)` returns the source path used for the parent directory.
59
+ */
60
+ export function classifyRuntimeCycle(path, edges, fileForKey) {
61
+ const nodes = cycleNodes(path);
62
+ const folders = nodes.map((key) => folderOfFile(fileForKey(key)));
63
+ const uniqueFolders = [...new Set(folders)];
64
+
65
+ if (uniqueFolders.length >= HIGH_FOLDER_MINIMUM) {
66
+ return report(path, "high", "multi-module", "large multi-module cycle");
67
+ }
68
+
69
+ if (uniqueFolders.length === 2 && isRecursiveDescentPair(uniqueFolders[0], uniqueFolders[1], edges)) {
70
+ return report(path, "low", "mutual-recursion", "mutual recursion, probably intentional; verify");
71
+ }
72
+
73
+ return report(path, "medium", "small", "small cycle; review");
74
+ }
75
+
76
+ export function isRecursiveDescentPair(folderA, folderB, edges) {
77
+ let ab = 0;
78
+ let ba = 0;
79
+ let callsAB = 0;
80
+ let callsBA = 0;
81
+ for (const edge of edges || []) {
82
+ if (edge.typeOnly) continue;
83
+ const from = folderOfFile(edge.fromFile || edge.from);
84
+ const to = folderOfFile(edge.toFile || edge.to);
85
+ if (from === folderA && to === folderB) {
86
+ ab += 1;
87
+ callsAB += edge.calls || 0;
88
+ } else if (from === folderB && to === folderA) {
89
+ ba += 1;
90
+ callsBA += edge.calls || 0;
91
+ }
92
+ }
93
+ return ab >= MUTUAL_EDGE_MINIMUM
94
+ && ba >= MUTUAL_EDGE_MINIMUM
95
+ && callsAB >= MUTUAL_CALL_MINIMUM
96
+ && callsBA >= MUTUAL_CALL_MINIMUM;
97
+ }
98
+
99
+ export function classifyRuntimeCycles(cycles, edges, fileForKey) {
100
+ return (cycles || []).map((path) => classifyRuntimeCycle(path, edges, fileForKey));
101
+ }
102
+
103
+ export function typeOnlyReport(path) {
104
+ return report(path, "low", "type-only", "type-only imports; verify");
105
+ }
106
+
107
+ export function priorityCounts(reports) {
108
+ const counts = { high: 0, medium: 0, low: 0 };
109
+ for (const item of reports || []) {
110
+ if (item.priority === "high") counts.high += 1;
111
+ else if (item.priority === "low") counts.low += 1;
112
+ else counts.medium += 1;
113
+ }
114
+ return counts;
115
+ }
116
+
117
+ export function hasCyclePriorities(stats) {
118
+ return Boolean(stats) && (stats.highCycles != null || stats.mediumCycles != null || stats.lowCycles != null);
119
+ }
120
+
121
+ /** Points removed from the architecture health score. Low-priority cycles do not count. */
122
+ export function cycleHealthPenalty(stats) {
123
+ if (!stats) return 0;
124
+ if (!hasCyclePriorities(stats)) {
125
+ return stats.cycles > 0 ? Math.min(30, stats.cycles * 10) : 0;
126
+ }
127
+ const high = stats.highCycles || 0;
128
+ const medium = stats.mediumCycles || 0;
129
+ return Math.min(30, high * 10 + medium * 5);
130
+ }
131
+
132
+ export function cycleStatusCell(stats) {
133
+ if (!stats || stats.cycles === 0) return { mark: "✅", advice: "Clean module boundaries" };
134
+ if (hasCyclePriorities(stats) && (stats.highCycles || 0) === 0 && (stats.lowCycles || 0) > 0 && !(stats.mediumCycles > 0)) {
135
+ return { mark: "🟡", advice: "mutual recursion, probably intentional; verify" };
136
+ }
137
+ if (hasCyclePriorities(stats) && (stats.highCycles || 0) === 0) {
138
+ return { mark: "🟡", advice: "Small cycle; review" };
139
+ }
140
+ return { mark: "🔴", advice: "Refactor to break cycles" };
141
+ }
142
+
143
+ export function cycleConcern(stats) {
144
+ if (!stats || !(stats.cycles > 0)) return null;
145
+ const noun = `circular dependenc${stats.cycles === 1 ? "y" : "ies"}`;
146
+ if (!hasCyclePriorities(stats)) {
147
+ return `**Circular dependencies detected** — ${stats.cycles} ${noun} create tight coupling, complicate testing, and hinder refactoring. Priority: High.`;
148
+ }
149
+ if ((stats.highCycles || 0) > 0) {
150
+ const high = stats.highCycles;
151
+ const highNoun = `circular dependenc${high === 1 ? "y" : "ies"}`;
152
+ return `**Circular dependencies detected** — ${high} large multi-module ${highNoun}. Priority: High refactor.`;
153
+ }
154
+ if ((stats.lowCycles || 0) > 0 && !(stats.mediumCycles > 0)) {
155
+ return `**Circular dependencies detected** — ${stats.cycles} ${noun}. mutual recursion, probably intentional; verify. Priority: Low.`;
156
+ }
157
+ return `**Circular dependencies detected** — ${stats.cycles} ${noun}. Priority: Medium.`;
158
+ }
159
+
160
+ export function cycleInsight(stats) {
161
+ if (!stats || !(stats.cycles > 0)) return null;
162
+ if (!hasCyclePriorities(stats)) {
163
+ return `🔴 **Architectural concern** — ${stats.cycles} circular dependenc${stats.cycles === 1 ? "y" : "ies"} detected. These create tight coupling and make testing difficult.`;
164
+ }
165
+ if ((stats.highCycles || 0) === 0) {
166
+ return `🟡 **Circular dependencies** — ${stats.cycles} detected. mutual recursion, probably intentional; verify. Priority: Low.`;
167
+ }
168
+ return `🔴 **Architectural concern** — ${stats.highCycles} large multi-module circular dependenc${stats.highCycles === 1 ? "y" : "ies"} detected. Priority: High refactor.`;
169
+ }