@praneeth_54/agentdoctor 1.0.0 → 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.
Files changed (208) hide show
  1. package/CHANGELOG.md +34 -5
  2. package/README.md +15 -12
  3. package/dist/cli/commands/brain-mcp.d.ts +9 -0
  4. package/dist/cli/commands/brain-mcp.js +38 -0
  5. package/dist/cli/program.js +13 -0
  6. package/dist/constants.d.ts +1 -1
  7. package/dist/constants.js +1 -1
  8. package/dist/core/understanding/architecture/index.d.ts +4 -0
  9. package/dist/core/understanding/architecture/index.js +3 -0
  10. package/dist/core/understanding/architecture/infer.d.ts +6 -0
  11. package/dist/core/understanding/architecture/infer.js +39 -0
  12. package/dist/core/understanding/architecture/models.d.ts +23 -0
  13. package/dist/core/understanding/architecture/models.js +127 -0
  14. package/dist/core/understanding/architecture/patterns/distribution.d.ts +2 -0
  15. package/dist/core/understanding/architecture/patterns/distribution.js +115 -0
  16. package/dist/core/understanding/architecture/patterns/index.d.ts +2 -0
  17. package/dist/core/understanding/architecture/patterns/index.js +8 -0
  18. package/dist/core/understanding/architecture/patterns/layered.d.ts +2 -0
  19. package/dist/core/understanding/architecture/patterns/layered.js +279 -0
  20. package/dist/core/understanding/architecture/patterns/structural.d.ts +2 -0
  21. package/dist/core/understanding/architecture/patterns/structural.js +197 -0
  22. package/dist/core/understanding/architecture/rules.d.ts +4 -0
  23. package/dist/core/understanding/architecture/rules.js +6 -0
  24. package/dist/core/understanding/architecture/types.d.ts +52 -0
  25. package/dist/core/understanding/architecture/types.js +1 -0
  26. package/dist/core/understanding/brain/build.d.ts +13 -0
  27. package/dist/core/understanding/brain/build.js +158 -0
  28. package/dist/core/understanding/brain/claims/index.d.ts +3 -0
  29. package/dist/core/understanding/brain/claims/index.js +2 -0
  30. package/dist/core/understanding/brain/claims/lifecycle.d.ts +16 -0
  31. package/dist/core/understanding/brain/claims/lifecycle.js +78 -0
  32. package/dist/core/understanding/brain/claims/types.d.ts +32 -0
  33. package/dist/core/understanding/brain/claims/types.js +25 -0
  34. package/dist/core/understanding/brain/components/index.d.ts +17 -0
  35. package/dist/core/understanding/brain/components/index.js +97 -0
  36. package/dist/core/understanding/brain/components/types.d.ts +16 -0
  37. package/dist/core/understanding/brain/components/types.js +8 -0
  38. package/dist/core/understanding/brain/confidence.d.ts +19 -0
  39. package/dist/core/understanding/brain/confidence.js +39 -0
  40. package/dist/core/understanding/brain/contract.d.ts +17 -0
  41. package/dist/core/understanding/brain/contract.js +100 -0
  42. package/dist/core/understanding/brain/contradictions/index.d.ts +12 -0
  43. package/dist/core/understanding/brain/contradictions/index.js +57 -0
  44. package/dist/core/understanding/brain/contradictions/types.d.ts +12 -0
  45. package/dist/core/understanding/brain/contradictions/types.js +8 -0
  46. package/dist/core/understanding/brain/delta.d.ts +25 -0
  47. package/dist/core/understanding/brain/delta.js +116 -0
  48. package/dist/core/understanding/brain/evidence/index.d.ts +3 -0
  49. package/dist/core/understanding/brain/evidence/index.js +2 -0
  50. package/dist/core/understanding/brain/evidence/redact.d.ts +4 -0
  51. package/dist/core/understanding/brain/evidence/redact.js +36 -0
  52. package/dist/core/understanding/brain/evidence/types.d.ts +34 -0
  53. package/dist/core/understanding/brain/evidence/types.js +35 -0
  54. package/dist/core/understanding/brain/explain.d.ts +17 -0
  55. package/dist/core/understanding/brain/explain.js +33 -0
  56. package/dist/core/understanding/brain/index.d.ts +28 -0
  57. package/dist/core/understanding/brain/index.js +16 -0
  58. package/dist/core/understanding/brain/migrate.d.ts +21 -0
  59. package/dist/core/understanding/brain/migrate.js +47 -0
  60. package/dist/core/understanding/brain/query.d.ts +60 -0
  61. package/dist/core/understanding/brain/query.js +92 -0
  62. package/dist/core/understanding/brain/security.d.ts +3 -0
  63. package/dist/core/understanding/brain/security.js +43 -0
  64. package/dist/core/understanding/brain/storage/index.d.ts +2 -0
  65. package/dist/core/understanding/brain/storage/index.js +1 -0
  66. package/dist/core/understanding/brain/storage/store.d.ts +53 -0
  67. package/dist/core/understanding/brain/storage/store.js +305 -0
  68. package/dist/core/understanding/brain/trace.d.ts +24 -0
  69. package/dist/core/understanding/brain/trace.js +207 -0
  70. package/dist/core/understanding/brain/types.d.ts +32 -0
  71. package/dist/core/understanding/brain/types.js +8 -0
  72. package/dist/core/understanding/brain/version.d.ts +13 -0
  73. package/dist/core/understanding/brain/version.js +7 -0
  74. package/dist/core/understanding/delta/compare.d.ts +26 -0
  75. package/dist/core/understanding/delta/compare.js +74 -0
  76. package/dist/core/understanding/delta/index.d.ts +2 -0
  77. package/dist/core/understanding/delta/index.js +1 -0
  78. package/dist/core/understanding/dependencies/discover.d.ts +7 -0
  79. package/dist/core/understanding/dependencies/discover.js +550 -0
  80. package/dist/core/understanding/dependencies/extract.d.ts +10 -0
  81. package/dist/core/understanding/dependencies/extract.js +189 -0
  82. package/dist/core/understanding/dependencies/index.d.ts +4 -0
  83. package/dist/core/understanding/dependencies/index.js +3 -0
  84. package/dist/core/understanding/dependencies/models.d.ts +12 -0
  85. package/dist/core/understanding/dependencies/models.js +125 -0
  86. package/dist/core/understanding/dependencies/types.d.ts +27 -0
  87. package/dist/core/understanding/dependencies/types.js +1 -0
  88. package/dist/core/understanding/domain/discover.d.ts +14 -0
  89. package/dist/core/understanding/domain/discover.js +90 -0
  90. package/dist/core/understanding/domain/index.d.ts +2 -0
  91. package/dist/core/understanding/domain/index.js +1 -0
  92. package/dist/core/understanding/entrypoints/discover.d.ts +6 -0
  93. package/dist/core/understanding/entrypoints/discover.js +62 -0
  94. package/dist/core/understanding/entrypoints/extract.d.ts +19 -0
  95. package/dist/core/understanding/entrypoints/extract.js +43 -0
  96. package/dist/core/understanding/entrypoints/index.d.ts +4 -0
  97. package/dist/core/understanding/entrypoints/index.js +3 -0
  98. package/dist/core/understanding/entrypoints/models.d.ts +26 -0
  99. package/dist/core/understanding/entrypoints/models.js +216 -0
  100. package/dist/core/understanding/entrypoints/types.d.ts +20 -0
  101. package/dist/core/understanding/entrypoints/types.js +1 -0
  102. package/dist/core/understanding/index.d.ts +32 -0
  103. package/dist/core/understanding/index.js +18 -0
  104. package/dist/core/understanding/mind/build.d.ts +16 -0
  105. package/dist/core/understanding/mind/build.js +134 -0
  106. package/dist/core/understanding/mind/index.d.ts +6 -0
  107. package/dist/core/understanding/mind/index.js +3 -0
  108. package/dist/core/understanding/mind/query.d.ts +33 -0
  109. package/dist/core/understanding/mind/query.js +39 -0
  110. package/dist/core/understanding/mind/types.d.ts +24 -0
  111. package/dist/core/understanding/mind/types.js +7 -0
  112. package/dist/core/understanding/model/builder.d.ts +9 -0
  113. package/dist/core/understanding/model/builder.js +209 -0
  114. package/dist/core/understanding/model/ids.d.ts +7 -0
  115. package/dist/core/understanding/model/ids.js +48 -0
  116. package/dist/core/understanding/model/index.d.ts +7 -0
  117. package/dist/core/understanding/model/index.js +6 -0
  118. package/dist/core/understanding/model/schema.d.ts +12 -0
  119. package/dist/core/understanding/model/schema.js +33 -0
  120. package/dist/core/understanding/model/serializer.d.ts +9 -0
  121. package/dist/core/understanding/model/serializer.js +46 -0
  122. package/dist/core/understanding/model/types.d.ts +110 -0
  123. package/dist/core/understanding/model/types.js +1 -0
  124. package/dist/core/understanding/model/validator.d.ts +5 -0
  125. package/dist/core/understanding/model/validator.js +126 -0
  126. package/dist/core/understanding/model/version.d.ts +9 -0
  127. package/dist/core/understanding/model/version.js +7 -0
  128. package/dist/core/understanding/ownership/discover.d.ts +12 -0
  129. package/dist/core/understanding/ownership/discover.js +242 -0
  130. package/dist/core/understanding/ownership/index.d.ts +2 -0
  131. package/dist/core/understanding/ownership/index.js +1 -0
  132. package/dist/core/understanding/ownership/types.d.ts +23 -0
  133. package/dist/core/understanding/ownership/types.js +1 -0
  134. package/dist/core/understanding/query/engine.d.ts +24 -0
  135. package/dist/core/understanding/query/engine.js +35 -0
  136. package/dist/core/understanding/query/errors.d.ts +13 -0
  137. package/dist/core/understanding/query/errors.js +26 -0
  138. package/dist/core/understanding/query/executor.d.ts +8 -0
  139. package/dist/core/understanding/query/executor.js +34 -0
  140. package/dist/core/understanding/query/index.d.ts +7 -0
  141. package/dist/core/understanding/query/index.js +5 -0
  142. package/dist/core/understanding/query/models.d.ts +102 -0
  143. package/dist/core/understanding/query/models.js +1 -0
  144. package/dist/core/understanding/query/query.d.ts +68 -0
  145. package/dist/core/understanding/query/query.js +145 -0
  146. package/dist/core/understanding/query/registry.d.ts +8 -0
  147. package/dist/core/understanding/query/registry.js +340 -0
  148. package/dist/core/understanding/query/types.d.ts +63 -0
  149. package/dist/core/understanding/query/types.js +1 -0
  150. package/dist/core/understanding/relationships/discover.d.ts +11 -0
  151. package/dist/core/understanding/relationships/discover.js +517 -0
  152. package/dist/core/understanding/relationships/extract.d.ts +14 -0
  153. package/dist/core/understanding/relationships/extract.js +165 -0
  154. package/dist/core/understanding/relationships/index.d.ts +4 -0
  155. package/dist/core/understanding/relationships/index.js +3 -0
  156. package/dist/core/understanding/relationships/models.d.ts +21 -0
  157. package/dist/core/understanding/relationships/models.js +185 -0
  158. package/dist/core/understanding/relationships/types.d.ts +42 -0
  159. package/dist/core/understanding/relationships/types.js +1 -0
  160. package/dist/core/understanding/risks/discover.d.ts +8 -0
  161. package/dist/core/understanding/risks/discover.js +155 -0
  162. package/dist/core/understanding/risks/index.d.ts +2 -0
  163. package/dist/core/understanding/risks/index.js +1 -0
  164. package/dist/core/understanding/risks/types.d.ts +26 -0
  165. package/dist/core/understanding/risks/types.js +1 -0
  166. package/dist/core/understanding/shared/domain-lexicon.d.ts +9 -0
  167. package/dist/core/understanding/shared/domain-lexicon.js +107 -0
  168. package/dist/core/understanding/shared/index.d.ts +2 -0
  169. package/dist/core/understanding/shared/index.js +2 -0
  170. package/dist/core/understanding/shared/tokens.d.ts +7 -0
  171. package/dist/core/understanding/shared/tokens.js +141 -0
  172. package/dist/core/understanding/snapshot/identity.d.ts +24 -0
  173. package/dist/core/understanding/snapshot/identity.js +79 -0
  174. package/dist/core/understanding/snapshot/index.d.ts +2 -0
  175. package/dist/core/understanding/snapshot/index.js +1 -0
  176. package/dist/core/understanding/types/index.d.ts +22 -0
  177. package/dist/core/understanding/types/index.js +1 -0
  178. package/dist/core/understanding/understand/formatter.d.ts +10 -0
  179. package/dist/core/understanding/understand/formatter.js +115 -0
  180. package/dist/core/understanding/understand/index.d.ts +4 -0
  181. package/dist/core/understanding/understand/index.js +3 -0
  182. package/dist/core/understanding/understand/service.d.ts +17 -0
  183. package/dist/core/understanding/understand/service.js +76 -0
  184. package/dist/core/understanding/understand/summary.d.ts +14 -0
  185. package/dist/core/understanding/understand/summary.js +39 -0
  186. package/dist/core/understanding/understand/types.d.ts +31 -0
  187. package/dist/core/understanding/understand/types.js +1 -0
  188. package/dist/mcp/brain/compile.d.ts +9 -0
  189. package/dist/mcp/brain/compile.js +42 -0
  190. package/dist/mcp/brain/errors.d.ts +12 -0
  191. package/dist/mcp/brain/errors.js +23 -0
  192. package/dist/mcp/brain/index.d.ts +11 -0
  193. package/dist/mcp/brain/index.js +8 -0
  194. package/dist/mcp/brain/provenance.d.ts +29 -0
  195. package/dist/mcp/brain/provenance.js +59 -0
  196. package/dist/mcp/brain/schemas.d.ts +13 -0
  197. package/dist/mcp/brain/schemas.js +105 -0
  198. package/dist/mcp/brain/security/root.d.ts +6 -0
  199. package/dist/mcp/brain/security/root.js +66 -0
  200. package/dist/mcp/brain/server.d.ts +12 -0
  201. package/dist/mcp/brain/server.js +46 -0
  202. package/dist/mcp/brain/session.d.ts +33 -0
  203. package/dist/mcp/brain/session.js +117 -0
  204. package/dist/mcp/brain/tools/handlers.d.ts +13 -0
  205. package/dist/mcp/brain/tools/handlers.js +483 -0
  206. package/dist/mcp/brain/tools/registry.d.ts +9 -0
  207. package/dist/mcp/brain/tools/registry.js +191 -0
  208. package/package.json +5 -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
+ }