@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 +61 -0
- package/README.md +17 -14
- package/app.yml +23 -0
- package/github-app-manifest.json +22 -0
- package/llms-full.txt +1 -0
- package/llms-install.md +2 -0
- package/llms.txt +1 -0
- package/package.json +8 -6
- package/src/ai/generate-sections.js +11 -8
- package/src/analyzers/context-builder.js +11 -3
- package/src/analyzers/cycle-priority.js +169 -0
- package/src/analyzers/dependency-graph.js +100 -11
- package/src/analyzers/import-parsers.js +48 -8
- package/src/badge.js +3 -0
- package/src/cli-args.js +4 -0
- package/src/cli.js +11 -0
- package/src/core/config-schema.js +4 -0
- package/src/core/scan-defaults.js +36 -0
- package/src/core/scan.js +64 -21
- package/src/docs/llms-text.js +2 -0
- package/src/github-app/auth.js +130 -0
- package/src/github-app/checkout.js +70 -0
- package/src/github-app/handler.js +170 -0
- package/src/github-app/manifest.js +36 -0
- package/src/github-app/run.js +67 -0
- package/src/github-app/server.js +120 -0
- package/src/github-app/signature.js +22 -0
- package/src/help.js +44 -0
- package/src/renderers/renderAnalysis.js +16 -4
- package/src/renderers/renderDiff.js +5 -3
- package/src/renderers/renderMap.js +7 -5
- package/src/utils/path-roles.js +29 -1
- package/src/utils/route-params.js +50 -0
- package/src/utils/yaml.js +11 -6
|
@@ -366,15 +366,20 @@ export function renderDependencyGraph(depResult) {
|
|
|
366
366
|
lines.push("");
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
// Circular dependencies
|
|
369
|
+
// Circular dependencies. Priority comes from the cycle heuristic in cycle-priority.js.
|
|
370
370
|
if (depResult.cycles.length > 0) {
|
|
371
|
+
const reports = Array.isArray(depResult.cycleReports) ? depResult.cycleReports : [];
|
|
371
372
|
lines.push("## Circular Dependencies");
|
|
372
373
|
lines.push("");
|
|
373
|
-
lines.push(`**${depResult.cycles.length}** circular dependency chain${depResult.cycles.length === 1 ? " was" : "s were"} detected.
|
|
374
|
+
lines.push(`**${depResult.cycles.length}** circular dependency chain${depResult.cycles.length === 1 ? " was" : "s were"} detected. A large cycle across three or more folders is a high-priority refactor. A two-folder recursive-descent pair (many imports and calls both ways) is mutual recursion, probably intentional; verify. A cycle that exists only through type-only imports is not a runtime cycle.`);
|
|
374
375
|
lines.push("");
|
|
375
|
-
|
|
376
|
+
const limit = Math.min(depResult.cycles.length, 20);
|
|
377
|
+
for (let i = 0; i < limit; i++) {
|
|
376
378
|
const cycle = depResult.cycles[i];
|
|
377
|
-
|
|
379
|
+
const report = reports[i];
|
|
380
|
+
const pathText = Array.isArray(cycle) ? cycle.join("` → `") : String(cycle);
|
|
381
|
+
lines.push(`${i + 1}. \`${pathText}\``);
|
|
382
|
+
if (report?.priorityLine) lines.push(` ${report.priorityLine}`);
|
|
378
383
|
}
|
|
379
384
|
if (depResult.cycles.length > 20) {
|
|
380
385
|
lines.push(`\n*...and ${depResult.cycles.length - 20} more cycles*`);
|
|
@@ -382,6 +387,13 @@ export function renderDependencyGraph(depResult) {
|
|
|
382
387
|
lines.push("");
|
|
383
388
|
}
|
|
384
389
|
|
|
390
|
+
if (Array.isArray(depResult.typeOnlyReports) && depResult.typeOnlyReports.length > 0) {
|
|
391
|
+
lines.push("## Type-only cycles");
|
|
392
|
+
lines.push("");
|
|
393
|
+
lines.push(`**${depResult.typeOnlyReports.length}** cycle${depResult.typeOnlyReports.length === 1 ? " exists" : "s exist"} only through \`import type\` / \`export type\`. They are not runtime dependencies. Priority: Low — type-only imports; verify.`);
|
|
394
|
+
lines.push("");
|
|
395
|
+
}
|
|
396
|
+
|
|
385
397
|
// External dependencies
|
|
386
398
|
if (depResult.externalDeps.length > 0) {
|
|
387
399
|
lines.push("## External Dependencies");
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { replaceBracketParams } from "../utils/route-params.js";
|
|
2
|
+
|
|
1
3
|
function isRouteFile(file) {
|
|
2
4
|
return (
|
|
3
5
|
file.includes("/pages/api/") ||
|
|
@@ -15,10 +17,10 @@ function routePathFromFile(file) {
|
|
|
15
17
|
const appIndex = file.indexOf("/app/");
|
|
16
18
|
const relative = file.slice(appIndex + 5);
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
const cleaned = relative
|
|
19
21
|
.replace(/\/page\.(ts|tsx|js|jsx)$/, "")
|
|
20
|
-
.replace(/\/route\.(ts|tsx|js|jsx)$/, "")
|
|
21
|
-
|
|
22
|
+
.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
23
|
+
return `/${replaceBracketParams(cleaned)}`;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (file.includes("/pages/api/")) {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { replaceLeadingAppGroup, replaceSlashGroups } from "../utils/route-params.js";
|
|
2
|
+
|
|
1
3
|
function sanitizeNodeId(value) {
|
|
2
4
|
return value.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3
5
|
}
|
|
4
6
|
|
|
5
7
|
function normalizeLabel(value) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.replace(/^app\/\((.*?)\)/, "app/$1")
|
|
9
|
-
.replace(/\/\((.*?)\)/g, "/$1")
|
|
10
|
-
.replace(/\/+/g, "/");
|
|
8
|
+
const withoutSrc = value.replace(/^src\//, "");
|
|
9
|
+
return replaceSlashGroups(replaceLeadingAppGroup(withoutSrc)).replace(/\/+/g, "/");
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
function buildModuleGraph(modules, depGraph) {
|
|
@@ -96,9 +95,11 @@ function buildModuleGraph(modules, depGraph) {
|
|
|
96
95
|
*/
|
|
97
96
|
function findModuleForFile(fileKey, modules) {
|
|
98
97
|
const normalized = fileKey.replace(/\\/g, "/");
|
|
98
|
+
const flat = modules.find((mod) => mod.flat) || modules.find((mod) => mod.key === "root");
|
|
99
99
|
let bestMatch = null;
|
|
100
100
|
for (const mod of modules) {
|
|
101
101
|
const modKey = mod.key;
|
|
102
|
+
if (flat && modKey === flat.key) continue;
|
|
102
103
|
// Direct match (with or without extension)
|
|
103
104
|
if (normalized === modKey || normalized === modKey.replace(/\.[^/.]+$/, "")) {
|
|
104
105
|
return modKey;
|
|
@@ -110,6 +111,7 @@ function findModuleForFile(fileKey, modules) {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
}
|
|
114
|
+
if (!bestMatch && flat && !normalized.includes("/")) return flat.key;
|
|
113
115
|
return bestMatch;
|
|
114
116
|
}
|
|
115
117
|
|
package/src/utils/path-roles.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// so tests and examples never drive hub lists, coupling advice, or severity.
|
|
4
4
|
|
|
5
5
|
const TEST_DIRECTORIES = new Set([
|
|
6
|
-
"test", "tests", "spec", "specs", "__tests__", "__mocks__", "fixtures", "
|
|
6
|
+
"test", "tests", "spec", "specs", "__tests__", "__mocks__", "fixtures", "fixture", "__fixtures__",
|
|
7
|
+
"testdata", "e2e", "e2e-fixture", "e2e-fixtures", "cypress", "__snapshots__",
|
|
7
8
|
]);
|
|
8
9
|
|
|
9
10
|
// Directories holding tooling, samples, and docs. Code in here describes a build script
|
|
@@ -31,12 +32,39 @@ export function isTestPath(filePath) {
|
|
|
31
32
|
const parts = segments(filePath);
|
|
32
33
|
if (directorySegments(filePath).some((segment) => TEST_DIRECTORIES.has(segment))) return true;
|
|
33
34
|
const base = parts[parts.length - 1] || "";
|
|
35
|
+
if (base.endsWith(".snap") || base.includes(".snap.")) return true;
|
|
34
36
|
if (/^(test_|spec_)/.test(base)) return true;
|
|
35
37
|
if (/(_test|_spec)\.[a-z0-9]+$/.test(base)) return true;
|
|
36
38
|
if (/\.(test|spec)\.[a-z0-9]+$/.test(base)) return true;
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Snapshot, sample, fixture, example, and generated trees.
|
|
44
|
+
* These are dev tooling, like tests: they describe the suite or a demo, not the product.
|
|
45
|
+
* Scan ignores them unless `scan.include_dev_artifacts` is true.
|
|
46
|
+
*/
|
|
47
|
+
export const DEV_ARTIFACT_DIRECTORIES = new Set([
|
|
48
|
+
"__snapshots__",
|
|
49
|
+
"sample", "samples",
|
|
50
|
+
"example", "examples",
|
|
51
|
+
"fixture", "fixtures", "__fixtures__",
|
|
52
|
+
"e2e-fixture", "e2e-fixtures",
|
|
53
|
+
"generated",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export function isSnapshotPath(filePath) {
|
|
57
|
+
const parts = segments(filePath);
|
|
58
|
+
if (parts.some((segment) => segment === "__snapshots__")) return true;
|
|
59
|
+
const base = parts[parts.length - 1] || "";
|
|
60
|
+
return base.endsWith(".snap") || base.includes(".snap.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isDevArtifactPath(filePath) {
|
|
64
|
+
if (isSnapshotPath(filePath)) return true;
|
|
65
|
+
return directorySegments(filePath).some((segment) => DEV_ARTIFACT_DIRECTORIES.has(segment));
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
/** True when a directory above the file is a tooling, example, or docs directory. */
|
|
41
69
|
export function isToolingPath(filePath) {
|
|
42
70
|
return directorySegments(filePath).some((segment) => TOOLING_DIRECTORIES.has(segment));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear replacements for route and label text.
|
|
3
|
+
*
|
|
4
|
+
* A lazy `.*?` between two markers is polynomial: on a long string with no
|
|
5
|
+
* closer, each start position scans to the end (CodeQL js/polynomial-redos).
|
|
6
|
+
* These walks stop at the first closer and never re-scan, which matches the
|
|
7
|
+
* old `String.prototype.replace` result.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function replaceMarked(input, open, close, mapInner) {
|
|
11
|
+
const value = String(input ?? "");
|
|
12
|
+
let out = "";
|
|
13
|
+
let cursor = 0;
|
|
14
|
+
while (cursor < value.length) {
|
|
15
|
+
const start = value.indexOf(open, cursor);
|
|
16
|
+
if (start === -1) {
|
|
17
|
+
out += value.slice(cursor);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
const end = value.indexOf(close, start + open.length);
|
|
21
|
+
if (end === -1) {
|
|
22
|
+
out += value.slice(cursor);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
out += value.slice(cursor, start);
|
|
26
|
+
out += mapInner(value.slice(start + open.length, end));
|
|
27
|
+
cursor = end + close.length;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `[param]` → `:param`, including the first `]` only. Global, non-overlapping. */
|
|
33
|
+
export function replaceBracketParams(value) {
|
|
34
|
+
return replaceMarked(value, "[", "]", (inner) => `:${inner}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `/(group)` → `/group`. Used for Next.js route groups in diagram labels. */
|
|
38
|
+
export function replaceSlashGroups(value) {
|
|
39
|
+
return replaceMarked(value, "/(", ")", (inner) => `/${inner}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A leading `app/(group)` → `app/group`. Later groups use {@link replaceSlashGroups}. */
|
|
43
|
+
export function replaceLeadingAppGroup(value) {
|
|
44
|
+
const input = String(value ?? "");
|
|
45
|
+
const prefix = "app/(";
|
|
46
|
+
if (!input.startsWith(prefix)) return input;
|
|
47
|
+
const end = input.indexOf(")", prefix.length);
|
|
48
|
+
if (end === -1) return input;
|
|
49
|
+
return `app/${input.slice(prefix.length, end)}${input.slice(end + 1)}`;
|
|
50
|
+
}
|
package/src/utils/yaml.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { CORE_SCHEMA, load, mergeTag } from "js-yaml";
|
|
2
2
|
|
|
3
3
|
const MAX_YAML_CHARS = 1_000_000;
|
|
4
4
|
const MAX_YAML_ALIASES = 2000;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Parse YAML without executing custom tags.
|
|
8
|
-
* js-yaml
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* js-yaml 5 `load` uses the YAML 1.2 Core schema (`true`/`false` are booleans;
|
|
9
|
+
* the YAML 1.1 spellings `yes` and `no` stay strings). The merge key (`<<`)
|
|
10
|
+
* is added back so configs that relied on js-yaml 4's merge still parse.
|
|
11
|
+
* A size cap and `maxAliases` stop a billion-laughs document.
|
|
12
|
+
* An empty file returns `undefined`, matching the previous loader.
|
|
12
13
|
*/
|
|
13
14
|
export function loadYaml(raw) {
|
|
14
15
|
if (typeof raw !== "string") {
|
|
@@ -17,11 +18,15 @@ export function loadYaml(raw) {
|
|
|
17
18
|
if (raw.length > MAX_YAML_CHARS) {
|
|
18
19
|
throw new Error(`YAML document exceeds ${MAX_YAML_CHARS} characters`);
|
|
19
20
|
}
|
|
21
|
+
if (raw.trim() === "") return undefined;
|
|
20
22
|
|
|
21
23
|
const aliasUses = raw.match(/(^|[\s\[\{,])\*[A-Za-z_][\w-]*/g);
|
|
22
24
|
if (aliasUses && aliasUses.length > MAX_YAML_ALIASES) {
|
|
23
25
|
throw new Error(`YAML document has too many aliases (${aliasUses.length})`);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
return
|
|
28
|
+
return load(raw, {
|
|
29
|
+
schema: CORE_SCHEMA.withTags(mergeTag),
|
|
30
|
+
maxAliases: MAX_YAML_ALIASES,
|
|
31
|
+
});
|
|
27
32
|
}
|