@praneeth_54/agentdoctor 0.3.0-beta → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -1
- package/README.md +64 -31
- package/dist/agents/inspect.js +58 -51
- package/dist/cli/commands/brain-mcp.d.ts +9 -0
- package/dist/cli/commands/brain-mcp.js +38 -0
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/fix.js +22 -6
- package/dist/cli/commands/scan.d.ts +5 -0
- package/dist/cli/commands/scan.js +25 -6
- package/dist/cli/commands/verify.d.ts +6 -0
- package/dist/cli/commands/verify.js +25 -11
- package/dist/cli/program.js +61 -7
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/fix/apply.d.ts +5 -1
- package/dist/core/fix/apply.js +34 -14
- package/dist/core/fix/plan.d.ts +1 -1
- package/dist/core/fix/plan.js +110 -3
- package/dist/core/fix/render.d.ts +2 -0
- package/dist/core/fix/render.js +57 -9
- package/dist/core/fix/run.js +5 -1
- package/dist/core/fix/types.d.ts +1 -1
- package/dist/core/fix/types.js +5 -1
- package/dist/core/fix/writers/claude-settings.d.ts +16 -0
- package/dist/core/fix/writers/claude-settings.js +100 -0
- package/dist/core/fix/writers/codex-config.d.ts +22 -0
- package/dist/core/fix/writers/codex-config.js +183 -0
- package/dist/core/fix/writers/cursorignore.js +2 -8
- package/dist/core/path-resolution/index.d.ts +5 -0
- package/dist/core/path-resolution/index.js +5 -0
- package/dist/core/path-resolution/prepare.d.ts +33 -0
- package/dist/core/path-resolution/prepare.js +98 -0
- package/dist/core/policy/evaluate.d.ts +44 -0
- package/dist/core/policy/evaluate.js +120 -0
- package/dist/core/rules/claude-deny.d.ts +8 -0
- package/dist/core/rules/claude-deny.js +39 -0
- package/dist/core/rules/codex-deny.d.ts +5 -0
- package/dist/core/rules/codex-deny.js +42 -0
- package/dist/core/rules/context/generated-directory.js +43 -9
- package/dist/core/rules/context/large-log-file.js +27 -10
- package/dist/core/rules/ignore.js +2 -5
- package/dist/core/rules/instructions/missing-path-reference.d.ts +5 -1
- package/dist/core/rules/instructions/missing-path-reference.js +24 -6
- package/dist/core/rules/path-kind.d.ts +15 -2
- package/dist/core/rules/path-kind.js +113 -10
- package/dist/core/rules/security/env-file-exposure.js +9 -24
- package/dist/core/rules/text-cache.js +46 -40
- package/dist/core/understanding/architecture/index.d.ts +4 -0
- package/dist/core/understanding/architecture/index.js +3 -0
- package/dist/core/understanding/architecture/infer.d.ts +6 -0
- package/dist/core/understanding/architecture/infer.js +39 -0
- package/dist/core/understanding/architecture/models.d.ts +23 -0
- package/dist/core/understanding/architecture/models.js +127 -0
- package/dist/core/understanding/architecture/patterns/distribution.d.ts +2 -0
- package/dist/core/understanding/architecture/patterns/distribution.js +115 -0
- package/dist/core/understanding/architecture/patterns/index.d.ts +2 -0
- package/dist/core/understanding/architecture/patterns/index.js +8 -0
- package/dist/core/understanding/architecture/patterns/layered.d.ts +2 -0
- package/dist/core/understanding/architecture/patterns/layered.js +279 -0
- package/dist/core/understanding/architecture/patterns/structural.d.ts +2 -0
- package/dist/core/understanding/architecture/patterns/structural.js +197 -0
- package/dist/core/understanding/architecture/rules.d.ts +4 -0
- package/dist/core/understanding/architecture/rules.js +6 -0
- package/dist/core/understanding/architecture/types.d.ts +52 -0
- package/dist/core/understanding/architecture/types.js +1 -0
- package/dist/core/understanding/brain/build.d.ts +13 -0
- package/dist/core/understanding/brain/build.js +158 -0
- package/dist/core/understanding/brain/claims/index.d.ts +3 -0
- package/dist/core/understanding/brain/claims/index.js +2 -0
- package/dist/core/understanding/brain/claims/lifecycle.d.ts +16 -0
- package/dist/core/understanding/brain/claims/lifecycle.js +78 -0
- package/dist/core/understanding/brain/claims/types.d.ts +32 -0
- package/dist/core/understanding/brain/claims/types.js +25 -0
- package/dist/core/understanding/brain/components/index.d.ts +17 -0
- package/dist/core/understanding/brain/components/index.js +97 -0
- package/dist/core/understanding/brain/components/types.d.ts +16 -0
- package/dist/core/understanding/brain/components/types.js +8 -0
- package/dist/core/understanding/brain/confidence.d.ts +19 -0
- package/dist/core/understanding/brain/confidence.js +39 -0
- package/dist/core/understanding/brain/contract.d.ts +17 -0
- package/dist/core/understanding/brain/contract.js +100 -0
- package/dist/core/understanding/brain/contradictions/index.d.ts +12 -0
- package/dist/core/understanding/brain/contradictions/index.js +57 -0
- package/dist/core/understanding/brain/contradictions/types.d.ts +12 -0
- package/dist/core/understanding/brain/contradictions/types.js +8 -0
- package/dist/core/understanding/brain/delta.d.ts +25 -0
- package/dist/core/understanding/brain/delta.js +116 -0
- package/dist/core/understanding/brain/evidence/index.d.ts +3 -0
- package/dist/core/understanding/brain/evidence/index.js +2 -0
- package/dist/core/understanding/brain/evidence/redact.d.ts +4 -0
- package/dist/core/understanding/brain/evidence/redact.js +36 -0
- package/dist/core/understanding/brain/evidence/types.d.ts +34 -0
- package/dist/core/understanding/brain/evidence/types.js +35 -0
- package/dist/core/understanding/brain/explain.d.ts +17 -0
- package/dist/core/understanding/brain/explain.js +33 -0
- package/dist/core/understanding/brain/index.d.ts +28 -0
- package/dist/core/understanding/brain/index.js +16 -0
- package/dist/core/understanding/brain/migrate.d.ts +21 -0
- package/dist/core/understanding/brain/migrate.js +47 -0
- package/dist/core/understanding/brain/query.d.ts +60 -0
- package/dist/core/understanding/brain/query.js +92 -0
- package/dist/core/understanding/brain/security.d.ts +3 -0
- package/dist/core/understanding/brain/security.js +43 -0
- package/dist/core/understanding/brain/storage/index.d.ts +2 -0
- package/dist/core/understanding/brain/storage/index.js +1 -0
- package/dist/core/understanding/brain/storage/store.d.ts +53 -0
- package/dist/core/understanding/brain/storage/store.js +305 -0
- package/dist/core/understanding/brain/trace.d.ts +24 -0
- package/dist/core/understanding/brain/trace.js +207 -0
- package/dist/core/understanding/brain/types.d.ts +32 -0
- package/dist/core/understanding/brain/types.js +8 -0
- package/dist/core/understanding/brain/version.d.ts +13 -0
- package/dist/core/understanding/brain/version.js +7 -0
- package/dist/core/understanding/delta/compare.d.ts +26 -0
- package/dist/core/understanding/delta/compare.js +74 -0
- package/dist/core/understanding/delta/index.d.ts +2 -0
- package/dist/core/understanding/delta/index.js +1 -0
- package/dist/core/understanding/dependencies/discover.d.ts +7 -0
- package/dist/core/understanding/dependencies/discover.js +550 -0
- package/dist/core/understanding/dependencies/extract.d.ts +10 -0
- package/dist/core/understanding/dependencies/extract.js +189 -0
- package/dist/core/understanding/dependencies/index.d.ts +4 -0
- package/dist/core/understanding/dependencies/index.js +3 -0
- package/dist/core/understanding/dependencies/models.d.ts +12 -0
- package/dist/core/understanding/dependencies/models.js +125 -0
- package/dist/core/understanding/dependencies/types.d.ts +27 -0
- package/dist/core/understanding/dependencies/types.js +1 -0
- package/dist/core/understanding/domain/discover.d.ts +14 -0
- package/dist/core/understanding/domain/discover.js +90 -0
- package/dist/core/understanding/domain/index.d.ts +2 -0
- package/dist/core/understanding/domain/index.js +1 -0
- package/dist/core/understanding/entrypoints/discover.d.ts +6 -0
- package/dist/core/understanding/entrypoints/discover.js +62 -0
- package/dist/core/understanding/entrypoints/extract.d.ts +19 -0
- package/dist/core/understanding/entrypoints/extract.js +43 -0
- package/dist/core/understanding/entrypoints/index.d.ts +4 -0
- package/dist/core/understanding/entrypoints/index.js +3 -0
- package/dist/core/understanding/entrypoints/models.d.ts +26 -0
- package/dist/core/understanding/entrypoints/models.js +216 -0
- package/dist/core/understanding/entrypoints/types.d.ts +20 -0
- package/dist/core/understanding/entrypoints/types.js +1 -0
- package/dist/core/understanding/index.d.ts +32 -0
- package/dist/core/understanding/index.js +18 -0
- package/dist/core/understanding/mind/build.d.ts +16 -0
- package/dist/core/understanding/mind/build.js +134 -0
- package/dist/core/understanding/mind/index.d.ts +6 -0
- package/dist/core/understanding/mind/index.js +3 -0
- package/dist/core/understanding/mind/query.d.ts +33 -0
- package/dist/core/understanding/mind/query.js +39 -0
- package/dist/core/understanding/mind/types.d.ts +24 -0
- package/dist/core/understanding/mind/types.js +7 -0
- package/dist/core/understanding/model/builder.d.ts +9 -0
- package/dist/core/understanding/model/builder.js +209 -0
- package/dist/core/understanding/model/ids.d.ts +7 -0
- package/dist/core/understanding/model/ids.js +48 -0
- package/dist/core/understanding/model/index.d.ts +7 -0
- package/dist/core/understanding/model/index.js +6 -0
- package/dist/core/understanding/model/schema.d.ts +12 -0
- package/dist/core/understanding/model/schema.js +33 -0
- package/dist/core/understanding/model/serializer.d.ts +9 -0
- package/dist/core/understanding/model/serializer.js +46 -0
- package/dist/core/understanding/model/types.d.ts +110 -0
- package/dist/core/understanding/model/types.js +1 -0
- package/dist/core/understanding/model/validator.d.ts +5 -0
- package/dist/core/understanding/model/validator.js +126 -0
- package/dist/core/understanding/model/version.d.ts +9 -0
- package/dist/core/understanding/model/version.js +7 -0
- package/dist/core/understanding/ownership/discover.d.ts +12 -0
- package/dist/core/understanding/ownership/discover.js +242 -0
- package/dist/core/understanding/ownership/index.d.ts +2 -0
- package/dist/core/understanding/ownership/index.js +1 -0
- package/dist/core/understanding/ownership/types.d.ts +23 -0
- package/dist/core/understanding/ownership/types.js +1 -0
- package/dist/core/understanding/query/engine.d.ts +24 -0
- package/dist/core/understanding/query/engine.js +35 -0
- package/dist/core/understanding/query/errors.d.ts +13 -0
- package/dist/core/understanding/query/errors.js +26 -0
- package/dist/core/understanding/query/executor.d.ts +8 -0
- package/dist/core/understanding/query/executor.js +34 -0
- package/dist/core/understanding/query/index.d.ts +7 -0
- package/dist/core/understanding/query/index.js +5 -0
- package/dist/core/understanding/query/models.d.ts +102 -0
- package/dist/core/understanding/query/models.js +1 -0
- package/dist/core/understanding/query/query.d.ts +68 -0
- package/dist/core/understanding/query/query.js +145 -0
- package/dist/core/understanding/query/registry.d.ts +8 -0
- package/dist/core/understanding/query/registry.js +340 -0
- package/dist/core/understanding/query/types.d.ts +63 -0
- package/dist/core/understanding/query/types.js +1 -0
- package/dist/core/understanding/relationships/discover.d.ts +11 -0
- package/dist/core/understanding/relationships/discover.js +517 -0
- package/dist/core/understanding/relationships/extract.d.ts +14 -0
- package/dist/core/understanding/relationships/extract.js +165 -0
- package/dist/core/understanding/relationships/index.d.ts +4 -0
- package/dist/core/understanding/relationships/index.js +3 -0
- package/dist/core/understanding/relationships/models.d.ts +21 -0
- package/dist/core/understanding/relationships/models.js +185 -0
- package/dist/core/understanding/relationships/types.d.ts +42 -0
- package/dist/core/understanding/relationships/types.js +1 -0
- package/dist/core/understanding/risks/discover.d.ts +8 -0
- package/dist/core/understanding/risks/discover.js +155 -0
- package/dist/core/understanding/risks/index.d.ts +2 -0
- package/dist/core/understanding/risks/index.js +1 -0
- package/dist/core/understanding/risks/types.d.ts +26 -0
- package/dist/core/understanding/risks/types.js +1 -0
- package/dist/core/understanding/shared/domain-lexicon.d.ts +9 -0
- package/dist/core/understanding/shared/domain-lexicon.js +107 -0
- package/dist/core/understanding/shared/index.d.ts +2 -0
- package/dist/core/understanding/shared/index.js +2 -0
- package/dist/core/understanding/shared/tokens.d.ts +7 -0
- package/dist/core/understanding/shared/tokens.js +141 -0
- package/dist/core/understanding/snapshot/identity.d.ts +24 -0
- package/dist/core/understanding/snapshot/identity.js +79 -0
- package/dist/core/understanding/snapshot/index.d.ts +2 -0
- package/dist/core/understanding/snapshot/index.js +1 -0
- package/dist/core/understanding/types/index.d.ts +22 -0
- package/dist/core/understanding/types/index.js +1 -0
- package/dist/core/understanding/understand/formatter.d.ts +10 -0
- package/dist/core/understanding/understand/formatter.js +115 -0
- package/dist/core/understanding/understand/index.d.ts +4 -0
- package/dist/core/understanding/understand/index.js +3 -0
- package/dist/core/understanding/understand/service.d.ts +17 -0
- package/dist/core/understanding/understand/service.js +76 -0
- package/dist/core/understanding/understand/summary.d.ts +14 -0
- package/dist/core/understanding/understand/summary.js +39 -0
- package/dist/core/understanding/understand/types.d.ts +31 -0
- package/dist/core/understanding/understand/types.js +1 -0
- package/dist/discovery/files.js +7 -2
- package/dist/discovery/log-like.d.ts +2 -0
- package/dist/discovery/log-like.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mcp/brain/compile.d.ts +9 -0
- package/dist/mcp/brain/compile.js +42 -0
- package/dist/mcp/brain/errors.d.ts +12 -0
- package/dist/mcp/brain/errors.js +23 -0
- package/dist/mcp/brain/index.d.ts +11 -0
- package/dist/mcp/brain/index.js +8 -0
- package/dist/mcp/brain/provenance.d.ts +29 -0
- package/dist/mcp/brain/provenance.js +59 -0
- package/dist/mcp/brain/schemas.d.ts +13 -0
- package/dist/mcp/brain/schemas.js +105 -0
- package/dist/mcp/brain/security/root.d.ts +6 -0
- package/dist/mcp/brain/security/root.js +66 -0
- package/dist/mcp/brain/server.d.ts +12 -0
- package/dist/mcp/brain/server.js +46 -0
- package/dist/mcp/brain/session.d.ts +33 -0
- package/dist/mcp/brain/session.js +117 -0
- package/dist/mcp/brain/tools/handlers.d.ts +13 -0
- package/dist/mcp/brain/tools/handlers.js +483 -0
- package/dist/mcp/brain/tools/registry.d.ts +9 -0
- package/dist/mcp/brain/tools/registry.js +191 -0
- package/dist/reporters/github/annotations.d.ts +8 -0
- package/dist/reporters/github/annotations.js +42 -0
- package/dist/reporters/github/emit.d.ts +23 -0
- package/dist/reporters/github/emit.js +31 -0
- package/dist/reporters/github/summary.d.ts +20 -0
- package/dist/reporters/github/summary.js +111 -0
- package/dist/reporters/terminal/report.d.ts +5 -0
- package/dist/reporters/terminal/report.js +68 -5
- package/dist/reporters/verify/terminal.d.ts +4 -0
- package/dist/reporters/verify/terminal.js +26 -0
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +34 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +16 -0
- package/package.json +15 -1
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { discoverFiles } from "../../../discovery/files.js";
|
|
3
|
+
import { readTextFile } from "../../../utils/fs.js";
|
|
4
|
+
import { discoverDependencies } from "../dependencies/index.js";
|
|
5
|
+
import { extractReferences } from "../dependencies/extract.js";
|
|
6
|
+
import { isSourceFile } from "../dependencies/models.js";
|
|
7
|
+
import { discoverDomains } from "../domain/index.js";
|
|
8
|
+
import { discoverEntrypoints } from "../entrypoints/index.js";
|
|
9
|
+
import { domainForToken, tokenizeRelativePath } from "../shared/index.js";
|
|
10
|
+
import { classifyComponent, extractSemanticSignals, featureNameFromPath, pascalCaseName, } from "./extract.js";
|
|
11
|
+
import { STRENGTH_BOOST, clampConfidence, findRolePairRule, maxStrength } from "./models.js";
|
|
12
|
+
function dirnamePosix(relativePath) {
|
|
13
|
+
const idx = relativePath.lastIndexOf("/");
|
|
14
|
+
return idx < 0 ? "" : relativePath.slice(0, idx);
|
|
15
|
+
}
|
|
16
|
+
function joinPosix(...parts) {
|
|
17
|
+
const stack = [];
|
|
18
|
+
for (const part of parts.join("/").split("/")) {
|
|
19
|
+
if (!part || part === ".") {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (part === "..") {
|
|
23
|
+
stack.pop();
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
stack.push(part);
|
|
27
|
+
}
|
|
28
|
+
return stack.join("/");
|
|
29
|
+
}
|
|
30
|
+
function stripExtension(relativePath) {
|
|
31
|
+
return relativePath.replace(/\.[a-z0-9]+$/i, "");
|
|
32
|
+
}
|
|
33
|
+
function buildResolveMap(files) {
|
|
34
|
+
const map = new Map();
|
|
35
|
+
for (const relativePath of files) {
|
|
36
|
+
map.set(relativePath, relativePath);
|
|
37
|
+
const noExt = stripExtension(relativePath);
|
|
38
|
+
if (!map.has(noExt)) {
|
|
39
|
+
map.set(noExt, relativePath);
|
|
40
|
+
}
|
|
41
|
+
if (relativePath.endsWith("/index.ts") ||
|
|
42
|
+
relativePath.endsWith("/index.js") ||
|
|
43
|
+
relativePath.endsWith("/mod.rs") ||
|
|
44
|
+
relativePath.endsWith("/__init__.py")) {
|
|
45
|
+
const dir = dirnamePosix(relativePath);
|
|
46
|
+
if (dir && !map.has(dir)) {
|
|
47
|
+
map.set(dir, relativePath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return map;
|
|
52
|
+
}
|
|
53
|
+
function lookupResolved(resolveMap, candidate) {
|
|
54
|
+
const normalized = candidate.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
55
|
+
if (!normalized) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const candidates = [normalized];
|
|
59
|
+
if (/\.(js|jsx|mjs|cjs)$/i.test(normalized)) {
|
|
60
|
+
candidates.push(normalized.replace(/\.jsx$/i, ".tsx").replace(/\.js$/i, ".ts"));
|
|
61
|
+
candidates.push(stripExtension(normalized));
|
|
62
|
+
}
|
|
63
|
+
const extensions = [
|
|
64
|
+
"",
|
|
65
|
+
".ts",
|
|
66
|
+
".tsx",
|
|
67
|
+
".js",
|
|
68
|
+
".jsx",
|
|
69
|
+
".dart",
|
|
70
|
+
".java",
|
|
71
|
+
".go",
|
|
72
|
+
".py",
|
|
73
|
+
".rs",
|
|
74
|
+
".php",
|
|
75
|
+
"/index.ts",
|
|
76
|
+
"/index.js",
|
|
77
|
+
"/mod.rs",
|
|
78
|
+
];
|
|
79
|
+
for (const entry of candidates) {
|
|
80
|
+
for (const ext of extensions) {
|
|
81
|
+
const hit = resolveMap.get(entry + ext);
|
|
82
|
+
if (hit) {
|
|
83
|
+
return hit;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function resolveSpecifier(fromFile, specifier, resolveMap, files) {
|
|
90
|
+
if (specifier.startsWith(".")) {
|
|
91
|
+
return lookupResolved(resolveMap, joinPosix(dirnamePosix(fromFile), specifier));
|
|
92
|
+
}
|
|
93
|
+
// Dart package-relative and simple basename within same tree
|
|
94
|
+
if (specifier.endsWith(".dart") && !specifier.includes(":")) {
|
|
95
|
+
const base = joinPosix(dirnamePosix(fromFile), specifier);
|
|
96
|
+
const hit = lookupResolved(resolveMap, base);
|
|
97
|
+
if (hit) {
|
|
98
|
+
return hit;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Python absolute package imports (only from .py sources)
|
|
102
|
+
if (fromFile.endsWith(".py") && /^[a-zA-Z_][\w.]*$/.test(specifier)) {
|
|
103
|
+
const asPath = specifier.replace(/\./g, "/");
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
const stripped = stripExtension(file);
|
|
106
|
+
if (stripped === asPath || stripped.endsWith(`/${asPath}`)) {
|
|
107
|
+
return file;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Java package → path
|
|
112
|
+
if (fromFile.endsWith(".java") && /^[a-z]+(\.[a-zA-Z_][\w]*)+$/.test(specifier)) {
|
|
113
|
+
const asPath = specifier.replace(/\./g, "/");
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
if (stripExtension(file).endsWith(`/${asPath}`) || file.endsWith(`/${asPath}.java`)) {
|
|
116
|
+
return file;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Suffix match for PHP use statements
|
|
121
|
+
if (specifier.includes("/") || specifier.includes("\\")) {
|
|
122
|
+
const asPath = specifier.replace(/\\/g, "/").replace(/^App\//, "app/");
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
if (stripExtension(file).endsWith(`/${asPath}`) ||
|
|
125
|
+
stripExtension(file).endsWith(`/${asPath.split("/").pop()}`)) {
|
|
126
|
+
return file;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function addRelationship(bucket, edge) {
|
|
133
|
+
if (edge.source === edge.target) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (edge.evidence.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const key = `${edge.source}\0${edge.target}\0${edge.relationship}`;
|
|
140
|
+
const existing = bucket.get(key);
|
|
141
|
+
if (!existing) {
|
|
142
|
+
bucket.set(key, {
|
|
143
|
+
...edge,
|
|
144
|
+
evidence: [...edge.evidence].sort((a, b) => a.localeCompare(b)),
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
existing.confidence = clampConfidence(Math.max(existing.confidence, edge.confidence));
|
|
149
|
+
existing.strength = maxStrength(existing.strength, edge.strength);
|
|
150
|
+
for (const item of edge.evidence) {
|
|
151
|
+
if (!existing.evidence.includes(item)) {
|
|
152
|
+
existing.evidence.push(item);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
existing.evidence.sort((a, b) => a.localeCompare(b));
|
|
156
|
+
if (edge.bidirectional) {
|
|
157
|
+
existing.bidirectional = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function scoreRelationship(options) {
|
|
161
|
+
const { base, strength, evidenceCount, injectionBoost } = options;
|
|
162
|
+
let value = base + STRENGTH_BOOST[strength];
|
|
163
|
+
if (injectionBoost) {
|
|
164
|
+
value += 0.02;
|
|
165
|
+
}
|
|
166
|
+
value += Math.min(evidenceCount - 1, 3) * 0.005;
|
|
167
|
+
return clampConfidence(value);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Discover semantic component relationships using deterministic heuristics.
|
|
171
|
+
* Reuses dependency, entrypoint, and domain discovery. Isolated from scan/CLI.
|
|
172
|
+
*/
|
|
173
|
+
export async function discoverRelationships(options = {}) {
|
|
174
|
+
const started = performance.now();
|
|
175
|
+
const cwd = options.cwd ?? process.cwd();
|
|
176
|
+
const minConfidence = options.minConfidence ?? 0.75;
|
|
177
|
+
const maxReadBytes = options.maxReadBytes ?? 256 * 1024;
|
|
178
|
+
const discovery = await discoverFiles({ root: cwd });
|
|
179
|
+
const fileSet = new Set(discovery.files.map((f) => f.relativePath));
|
|
180
|
+
const resolveMap = buildResolveMap(fileSet);
|
|
181
|
+
const [dependencies, entrypoints, domains] = await Promise.all([
|
|
182
|
+
options.dependencies
|
|
183
|
+
? Promise.resolve(options.dependencies)
|
|
184
|
+
: discoverDependencies({ cwd, maxReadBytes }),
|
|
185
|
+
options.entrypoints
|
|
186
|
+
? Promise.resolve(options.entrypoints)
|
|
187
|
+
: discoverEntrypoints({ cwd, maxReadBytes }),
|
|
188
|
+
options.domains ? Promise.resolve(options.domains) : discoverDomains({ cwd }),
|
|
189
|
+
]);
|
|
190
|
+
const components = new Map();
|
|
191
|
+
const contentByFile = new Map();
|
|
192
|
+
let filesInspected = 0;
|
|
193
|
+
for (const file of discovery.files) {
|
|
194
|
+
if (!isSourceFile(file.relativePath)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const content = await readTextFile(file.absolutePath, maxReadBytes);
|
|
198
|
+
if (content === null) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
filesInspected += 1;
|
|
202
|
+
contentByFile.set(file.relativePath, content);
|
|
203
|
+
const component = classifyComponent(file.relativePath, content);
|
|
204
|
+
if (component) {
|
|
205
|
+
components.set(file.relativePath, component);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const byName = new Map();
|
|
209
|
+
for (const component of components.values()) {
|
|
210
|
+
if (!byName.has(component.name)) {
|
|
211
|
+
byName.set(component.name, component);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const bucket = new Map();
|
|
215
|
+
// DEPENDS_ON from dependency graph (module/package level)
|
|
216
|
+
for (const dep of dependencies.dependencies) {
|
|
217
|
+
const strength = dep.type === "dynamic-import" ? "weak" : dep.type === "package" ? "strong" : "medium";
|
|
218
|
+
addRelationship(bucket, {
|
|
219
|
+
source: dep.from,
|
|
220
|
+
target: dep.to,
|
|
221
|
+
relationship: "DEPENDS_ON",
|
|
222
|
+
confidence: scoreRelationship({
|
|
223
|
+
base: Math.min(dep.confidence, 0.95),
|
|
224
|
+
strength,
|
|
225
|
+
evidenceCount: dep.evidence.length,
|
|
226
|
+
}),
|
|
227
|
+
evidence: dep.evidence.map((e) => `dependency: ${e}`),
|
|
228
|
+
strength,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
// File-level import → semantic role relationships
|
|
232
|
+
for (const [relativePath, content] of contentByFile) {
|
|
233
|
+
const sourceComponent = components.get(relativePath);
|
|
234
|
+
const refs = extractReferences(relativePath, content);
|
|
235
|
+
const signals = extractSemanticSignals(content);
|
|
236
|
+
for (const ref of refs) {
|
|
237
|
+
const targetPath = resolveSpecifier(relativePath, ref.specifier, resolveMap, fileSet);
|
|
238
|
+
if (!targetPath) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const targetComponent = components.get(targetPath);
|
|
242
|
+
if (!sourceComponent || !targetComponent) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const rule = findRolePairRule(sourceComponent.role, targetComponent.role);
|
|
246
|
+
if (!rule) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const injection = signals.some((s) => s.kind === "constructor-injection" &&
|
|
250
|
+
(s.relatedName === targetComponent.name ||
|
|
251
|
+
targetPath.toLowerCase().includes((s.relatedName ?? "").toLowerCase())));
|
|
252
|
+
const callSignal = signals.some((s) => s.kind === "call" && s.relatedName === targetComponent.name);
|
|
253
|
+
let relationship = rule.relationship;
|
|
254
|
+
let strength = rule.strength;
|
|
255
|
+
if (injection) {
|
|
256
|
+
strength = "strong";
|
|
257
|
+
}
|
|
258
|
+
if (callSignal && relationship === "USES") {
|
|
259
|
+
relationship = "CALLS";
|
|
260
|
+
strength = maxStrength(strength, "medium");
|
|
261
|
+
}
|
|
262
|
+
const evidence = [`imports ${path.posix.basename(targetPath)}`];
|
|
263
|
+
if (injection) {
|
|
264
|
+
evidence.push("constructor injection");
|
|
265
|
+
}
|
|
266
|
+
if (callSignal) {
|
|
267
|
+
evidence.push(`calls ${targetComponent.name}`);
|
|
268
|
+
}
|
|
269
|
+
evidence.push(`${sourceComponent.role} → ${targetComponent.role}`);
|
|
270
|
+
addRelationship(bucket, {
|
|
271
|
+
source: sourceComponent.name,
|
|
272
|
+
target: targetComponent.name,
|
|
273
|
+
relationship,
|
|
274
|
+
confidence: scoreRelationship({
|
|
275
|
+
base: rule.baseConfidence,
|
|
276
|
+
strength,
|
|
277
|
+
evidenceCount: evidence.length,
|
|
278
|
+
injectionBoost: injection,
|
|
279
|
+
}),
|
|
280
|
+
evidence,
|
|
281
|
+
strength,
|
|
282
|
+
});
|
|
283
|
+
const sourceFeature = featureNameFromPath(relativePath);
|
|
284
|
+
const targetFeature = featureNameFromPath(targetPath);
|
|
285
|
+
if (sourceFeature &&
|
|
286
|
+
targetFeature &&
|
|
287
|
+
sourceFeature !== targetFeature &&
|
|
288
|
+
(relationship === "USES" || relationship === "CALLS")) {
|
|
289
|
+
addRelationship(bucket, {
|
|
290
|
+
source: sourceFeature,
|
|
291
|
+
target: targetFeature,
|
|
292
|
+
relationship: "CONSUMES",
|
|
293
|
+
confidence: scoreRelationship({
|
|
294
|
+
base: 0.82,
|
|
295
|
+
strength: "medium",
|
|
296
|
+
evidenceCount: 1,
|
|
297
|
+
}),
|
|
298
|
+
evidence: [
|
|
299
|
+
`${sourceComponent.name} ${relationship.toLowerCase()} ${targetComponent.name}`,
|
|
300
|
+
`${relativePath} → ${targetPath}`,
|
|
301
|
+
],
|
|
302
|
+
strength: "medium",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// IMPLEMENTS from explicit language signals
|
|
307
|
+
for (const signal of signals) {
|
|
308
|
+
if (signal.kind !== "implements" || !signal.relatedName || !sourceComponent) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
addRelationship(bucket, {
|
|
312
|
+
source: sourceComponent.name,
|
|
313
|
+
target: signal.relatedName,
|
|
314
|
+
relationship: "IMPLEMENTS",
|
|
315
|
+
confidence: scoreRelationship({
|
|
316
|
+
base: 0.97,
|
|
317
|
+
strength: "strong",
|
|
318
|
+
evidenceCount: 1,
|
|
319
|
+
}),
|
|
320
|
+
evidence: [signal.label, `in ${relativePath}`],
|
|
321
|
+
strength: "strong",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// CONTAINS / OWNS: feature folder contains classified components
|
|
326
|
+
for (const component of components.values()) {
|
|
327
|
+
const feature = featureNameFromPath(component.file);
|
|
328
|
+
if (!feature || feature === component.name) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
addRelationship(bucket, {
|
|
332
|
+
source: feature,
|
|
333
|
+
target: component.name,
|
|
334
|
+
relationship: "CONTAINS",
|
|
335
|
+
confidence: scoreRelationship({
|
|
336
|
+
base: 0.86,
|
|
337
|
+
strength: "strong",
|
|
338
|
+
evidenceCount: 1,
|
|
339
|
+
}),
|
|
340
|
+
evidence: [`${component.file} lives under feature ${feature}`],
|
|
341
|
+
strength: "strong",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Package.json workspace roots OWNS/CONTAINS features
|
|
345
|
+
for (const file of discovery.files) {
|
|
346
|
+
if (!file.relativePath.endsWith("package.json")) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const pkgDir = dirnamePosix(file.relativePath);
|
|
350
|
+
const pkgLabel = pkgDir
|
|
351
|
+
? pascalCaseName(pkgDir.split("/").filter(Boolean).pop() ?? "Package")
|
|
352
|
+
: "RootPackage";
|
|
353
|
+
for (const component of components.values()) {
|
|
354
|
+
if (pkgDir && !component.file.startsWith(`${pkgDir}/`)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!pkgDir && component.file.includes("/")) {
|
|
358
|
+
// root package only owns top-level when no nested package claims it
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (component.role === "Module") {
|
|
362
|
+
addRelationship(bucket, {
|
|
363
|
+
source: pkgLabel,
|
|
364
|
+
target: component.name,
|
|
365
|
+
relationship: "CONTAINS",
|
|
366
|
+
confidence: 0.9,
|
|
367
|
+
evidence: [`${file.relativePath} package contains ${component.file}`],
|
|
368
|
+
strength: "strong",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const feature = featureNameFromPath(component.file);
|
|
373
|
+
if (feature) {
|
|
374
|
+
addRelationship(bucket, {
|
|
375
|
+
source: pkgLabel,
|
|
376
|
+
target: feature,
|
|
377
|
+
relationship: "OWNS",
|
|
378
|
+
confidence: 0.88,
|
|
379
|
+
evidence: [`${file.relativePath} owns path ${component.file}`],
|
|
380
|
+
strength: "strong",
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// ENTERS: entrypoint → feature
|
|
387
|
+
for (const entry of entrypoints.entrypoints) {
|
|
388
|
+
const feature = featureNameFromPath(entry.file);
|
|
389
|
+
const entryName = components.get(entry.file)?.name ??
|
|
390
|
+
pascalCaseName(stripExtension(entry.file.split("/").pop() ?? "Entrypoint"));
|
|
391
|
+
if (feature) {
|
|
392
|
+
addRelationship(bucket, {
|
|
393
|
+
source: entryName,
|
|
394
|
+
target: feature,
|
|
395
|
+
relationship: "ENTERS",
|
|
396
|
+
confidence: scoreRelationship({
|
|
397
|
+
base: Math.min(entry.confidence, 0.92),
|
|
398
|
+
strength: "medium",
|
|
399
|
+
evidenceCount: entry.evidence.length,
|
|
400
|
+
}),
|
|
401
|
+
evidence: [
|
|
402
|
+
`entrypoint ${entry.file}`,
|
|
403
|
+
...entry.evidence.slice(0, 2).map((e) => `entry evidence: ${e}`),
|
|
404
|
+
],
|
|
405
|
+
strength: "medium",
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Feature PROVIDES Domain (path tokens ∩ domain lexicon)
|
|
410
|
+
for (const component of components.values()) {
|
|
411
|
+
const feature = featureNameFromPath(component.file);
|
|
412
|
+
if (!feature) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const tokens = tokenizeRelativePath(component.file);
|
|
416
|
+
for (const token of tokens) {
|
|
417
|
+
const domain = domainForToken(token);
|
|
418
|
+
if (!domain) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const domainHit = domains.domains.find((d) => d.name === domain);
|
|
422
|
+
if (!domainHit) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
addRelationship(bucket, {
|
|
426
|
+
source: feature,
|
|
427
|
+
target: domain,
|
|
428
|
+
relationship: "PROVIDES",
|
|
429
|
+
confidence: scoreRelationship({
|
|
430
|
+
base: Math.min(domainHit.confidence, 0.9),
|
|
431
|
+
strength: "medium",
|
|
432
|
+
evidenceCount: 1,
|
|
433
|
+
}),
|
|
434
|
+
evidence: [
|
|
435
|
+
`${component.file} token "${token}" maps to domain ${domain}`,
|
|
436
|
+
`domain evidence: ${domainHit.evidence[0] ?? component.file}`,
|
|
437
|
+
],
|
|
438
|
+
strength: "medium",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// CONFIGURES: module files importing configuration
|
|
443
|
+
for (const [relativePath, content] of contentByFile) {
|
|
444
|
+
const source = components.get(relativePath);
|
|
445
|
+
if (!source || source.role !== "Module") {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const refs = extractReferences(relativePath, content);
|
|
449
|
+
for (const ref of refs) {
|
|
450
|
+
const targetPath = resolveSpecifier(relativePath, ref.specifier, resolveMap, fileSet);
|
|
451
|
+
if (!targetPath) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const target = components.get(targetPath);
|
|
455
|
+
if (!target || target.role !== "Configuration") {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
addRelationship(bucket, {
|
|
459
|
+
source: source.name,
|
|
460
|
+
target: target.name,
|
|
461
|
+
relationship: "CONFIGURES",
|
|
462
|
+
confidence: 0.92,
|
|
463
|
+
evidence: [`imports ${path.posix.basename(targetPath)}`, "module configuration wiring"],
|
|
464
|
+
strength: "strong",
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Mark bidirectional pairs
|
|
469
|
+
for (const edge of bucket.values()) {
|
|
470
|
+
const reverseKey = `${edge.target}\0${edge.source}\0${edge.relationship}`;
|
|
471
|
+
const reverseSame = bucket.get(reverseKey);
|
|
472
|
+
if (reverseSame) {
|
|
473
|
+
edge.bidirectional = true;
|
|
474
|
+
reverseSame.bidirectional = true;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
// USES ↔ CALLS / DEPENDS_ON considered bidirectional semantic pair
|
|
478
|
+
for (const kind of [
|
|
479
|
+
"USES",
|
|
480
|
+
"CALLS",
|
|
481
|
+
"DEPENDS_ON",
|
|
482
|
+
"PROVIDES",
|
|
483
|
+
"CONSUMES",
|
|
484
|
+
]) {
|
|
485
|
+
const alt = bucket.get(`${edge.target}\0${edge.source}\0${kind}`);
|
|
486
|
+
if (alt) {
|
|
487
|
+
edge.bidirectional = true;
|
|
488
|
+
alt.bidirectional = true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const relationships = [...bucket.values()]
|
|
493
|
+
.filter((r) => r.confidence >= minConfidence && r.evidence.length > 0)
|
|
494
|
+
.sort((a, b) => {
|
|
495
|
+
if (b.confidence !== a.confidence) {
|
|
496
|
+
return b.confidence - a.confidence;
|
|
497
|
+
}
|
|
498
|
+
const rel = a.relationship.localeCompare(b.relationship);
|
|
499
|
+
if (rel !== 0) {
|
|
500
|
+
return rel;
|
|
501
|
+
}
|
|
502
|
+
const src = a.source.localeCompare(b.source);
|
|
503
|
+
if (src !== 0) {
|
|
504
|
+
return src;
|
|
505
|
+
}
|
|
506
|
+
return a.target.localeCompare(b.target);
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
relationships,
|
|
510
|
+
timingMs: Math.round(performance.now() - started),
|
|
511
|
+
filesConsidered: discovery.files.length,
|
|
512
|
+
filesInspected,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
export function scoreRelationshipConfidence(options) {
|
|
516
|
+
return scoreRelationship(options);
|
|
517
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClassifiedComponent, ComponentRole, SemanticSignal } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Convert file stem to PascalCase component name.
|
|
4
|
+
* checkout.controller → CheckoutController; payment_service → PaymentService
|
|
5
|
+
*/
|
|
6
|
+
export declare function pascalCaseName(stem: string): string;
|
|
7
|
+
export declare function classifyRoleFromPath(relativePath: string): ComponentRole | null;
|
|
8
|
+
export declare function extractPrimaryClassName(content: string): string | null;
|
|
9
|
+
export declare function classifyComponent(relativePath: string, content: string): ClassifiedComponent | null;
|
|
10
|
+
/**
|
|
11
|
+
* Extract semantic relationship signals from source text (deterministic regex).
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractSemanticSignals(content: string): SemanticSignal[];
|
|
14
|
+
export declare function featureNameFromPath(relativePath: string): string | null;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { ROLE_PATH_PATTERNS } from "./models.js";
|
|
2
|
+
function basename(relativePath) {
|
|
3
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
4
|
+
return parts[parts.length - 1] ?? relativePath;
|
|
5
|
+
}
|
|
6
|
+
function stripExtension(name) {
|
|
7
|
+
return name.replace(/\.[a-z0-9]+$/i, "");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert file stem to PascalCase component name.
|
|
11
|
+
* checkout.controller → CheckoutController; payment_service → PaymentService
|
|
12
|
+
*/
|
|
13
|
+
export function pascalCaseName(stem) {
|
|
14
|
+
const parts = stem.split(/[._\-/]+/).filter(Boolean);
|
|
15
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
16
|
+
}
|
|
17
|
+
export function classifyRoleFromPath(relativePath) {
|
|
18
|
+
const path = relativePath.replace(/\\/g, "/");
|
|
19
|
+
for (const entry of ROLE_PATH_PATTERNS) {
|
|
20
|
+
if (entry.pattern.test(path)) {
|
|
21
|
+
return entry.role;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
export function extractPrimaryClassName(content) {
|
|
27
|
+
const patterns = [
|
|
28
|
+
/\bexport\s+(?:default\s+)?class\s+([A-Z][A-Za-z0-9_]*)/,
|
|
29
|
+
/\bclass\s+([A-Z][A-Za-z0-9_]*)\b/,
|
|
30
|
+
/\bexport\s+(?:default\s+)?(?:function|const)\s+([A-Z][A-Za-z0-9_]*)/,
|
|
31
|
+
];
|
|
32
|
+
for (const pattern of patterns) {
|
|
33
|
+
const match = pattern.exec(content);
|
|
34
|
+
if (match?.[1]) {
|
|
35
|
+
return match[1];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function classifyComponent(relativePath, content) {
|
|
41
|
+
const role = classifyRoleFromPath(relativePath);
|
|
42
|
+
if (!role) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const className = extractPrimaryClassName(content);
|
|
46
|
+
const stem = stripExtension(basename(relativePath));
|
|
47
|
+
const name = className ?? pascalCaseName(stem);
|
|
48
|
+
return { name, role, file: relativePath.replace(/\\/g, "/") };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract semantic relationship signals from source text (deterministic regex).
|
|
52
|
+
*/
|
|
53
|
+
export function extractSemanticSignals(content) {
|
|
54
|
+
const signals = [];
|
|
55
|
+
const ctorInject = /constructor\s*\(\s*(?:private|public|protected|readonly|\s)*\s*(?:readonly\s+)?([A-Za-z_][\w]*)\s*:\s*([A-Z][A-Za-z0-9_]*)/g;
|
|
56
|
+
for (const match of content.matchAll(ctorInject)) {
|
|
57
|
+
const relatedName = match[2];
|
|
58
|
+
if (!relatedName) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
signals.push({
|
|
62
|
+
kind: "constructor-injection",
|
|
63
|
+
label: "constructor injection",
|
|
64
|
+
relatedName,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const nestInject = /@Inject(?:Repository)?\s*\(\s*([A-Z][A-Za-z0-9_]*)/g;
|
|
68
|
+
for (const match of content.matchAll(nestInject)) {
|
|
69
|
+
const relatedName = match[1];
|
|
70
|
+
if (!relatedName) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
signals.push({
|
|
74
|
+
kind: "constructor-injection",
|
|
75
|
+
label: "constructor injection",
|
|
76
|
+
relatedName,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const paramInject = /constructor\s*\([^)]*\b([A-Z][A-Za-z0-9_]*(?:Service|Repository|Bloc|Controller))\b/g;
|
|
80
|
+
for (const match of content.matchAll(paramInject)) {
|
|
81
|
+
const relatedName = match[1];
|
|
82
|
+
if (!relatedName) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
signals.push({
|
|
86
|
+
kind: "constructor-injection",
|
|
87
|
+
label: "constructor injection",
|
|
88
|
+
relatedName,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const implementsRe = /\bimplements\s+([A-Z][A-Za-z0-9_]*(?:\s*,\s*[A-Z][A-Za-z0-9_]*)*)/g;
|
|
92
|
+
for (const match of content.matchAll(implementsRe)) {
|
|
93
|
+
for (const name of (match[1] ?? "").split(",")) {
|
|
94
|
+
const trimmed = name.trim();
|
|
95
|
+
if (trimmed) {
|
|
96
|
+
signals.push({ kind: "implements", label: `implements ${trimmed}`, relatedName: trimmed });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const extendsRe = /\bextends\s+([A-Z][A-Za-z0-9_]*)/g;
|
|
101
|
+
for (const match of content.matchAll(extendsRe)) {
|
|
102
|
+
const relatedName = match[1];
|
|
103
|
+
if (!relatedName) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
signals.push({
|
|
107
|
+
kind: "extends",
|
|
108
|
+
label: `extends ${relatedName}`,
|
|
109
|
+
relatedName,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (/\bBlocProvider\b|\bCubit\b|\bBlocBuilder\b/.test(content)) {
|
|
113
|
+
signals.push({ kind: "bloc-provider", label: "BlocProvider/Cubit usage" });
|
|
114
|
+
}
|
|
115
|
+
if (/@Entity\b|\bSchema\.(?:Table|create)|CREATE TABLE/i.test(content)) {
|
|
116
|
+
signals.push({ kind: "entity", label: "database entity/schema" });
|
|
117
|
+
}
|
|
118
|
+
const callRe = /\b([A-Z][A-Za-z0-9_]*(?:Service|Repository|Controller|Bloc))\s*\.\s*[a-zA-Z_]/g;
|
|
119
|
+
for (const match of content.matchAll(callRe)) {
|
|
120
|
+
const relatedName = match[1];
|
|
121
|
+
if (!relatedName) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
signals.push({
|
|
125
|
+
kind: "call",
|
|
126
|
+
label: `calls ${relatedName}`,
|
|
127
|
+
relatedName,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return signals;
|
|
131
|
+
}
|
|
132
|
+
export function featureNameFromPath(relativePath) {
|
|
133
|
+
const path = relativePath.replace(/\\/g, "/");
|
|
134
|
+
const parts = path.split("/");
|
|
135
|
+
const stop = new Set([
|
|
136
|
+
"src",
|
|
137
|
+
"lib",
|
|
138
|
+
"app",
|
|
139
|
+
"apps",
|
|
140
|
+
"packages",
|
|
141
|
+
"services",
|
|
142
|
+
"modules",
|
|
143
|
+
"controllers",
|
|
144
|
+
"controller",
|
|
145
|
+
"services",
|
|
146
|
+
"service",
|
|
147
|
+
"repositories",
|
|
148
|
+
"repository",
|
|
149
|
+
"routes",
|
|
150
|
+
"config",
|
|
151
|
+
"main",
|
|
152
|
+
"java",
|
|
153
|
+
"kotlin",
|
|
154
|
+
]);
|
|
155
|
+
for (const part of parts.slice(0, -1)) {
|
|
156
|
+
const lower = part.toLowerCase();
|
|
157
|
+
if (stop.has(lower) || part.includes(".")) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (/^[a-zA-Z][\w-]*$/.test(part) && part.length > 1) {
|
|
161
|
+
return pascalCaseName(part);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|