@monoes/monograph 1.5.1 → 1.5.3

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 (61) hide show
  1. package/.monodesign/hook.cache.json +1 -0
  2. package/__tests__/integration/build-atomicity.test.ts +61 -0
  3. package/dist/src/analysis/cycles.d.ts.map +1 -1
  4. package/dist/src/analysis/cycles.js +5 -3
  5. package/dist/src/analysis/cycles.js.map +1 -1
  6. package/dist/src/analysis/trace.d.ts.map +1 -1
  7. package/dist/src/analysis/trace.js +1 -19
  8. package/dist/src/analysis/trace.js.map +1 -1
  9. package/dist/src/analysis/worker-pool.d.ts.map +1 -1
  10. package/dist/src/analysis/worker-pool.js +9 -7
  11. package/dist/src/analysis/worker-pool.js.map +1 -1
  12. package/dist/src/cli/skill-gen.d.ts.map +1 -1
  13. package/dist/src/cli/skill-gen.js +16 -25
  14. package/dist/src/cli/skill-gen.js.map +1 -1
  15. package/dist/src/export/html.d.ts.map +1 -1
  16. package/dist/src/export/html.js +3 -2
  17. package/dist/src/export/html.js.map +1 -1
  18. package/dist/src/graph/regex-search.d.ts.map +1 -1
  19. package/dist/src/graph/regex-search.js +9 -3
  20. package/dist/src/graph/regex-search.js.map +1 -1
  21. package/dist/src/pipeline/orchestrator.js +22 -0
  22. package/dist/src/pipeline/orchestrator.js.map +1 -1
  23. package/dist/src/pipeline/phases/call-site-extractors.d.ts +17 -0
  24. package/dist/src/pipeline/phases/call-site-extractors.d.ts.map +1 -0
  25. package/dist/src/pipeline/phases/call-site-extractors.js +132 -0
  26. package/dist/src/pipeline/phases/call-site-extractors.js.map +1 -0
  27. package/dist/src/pipeline/phases/module-resolution.d.ts +10 -0
  28. package/dist/src/pipeline/phases/module-resolution.d.ts.map +1 -0
  29. package/dist/src/pipeline/phases/module-resolution.js +301 -0
  30. package/dist/src/pipeline/phases/module-resolution.js.map +1 -0
  31. package/dist/src/pipeline/phases/orm.js +1 -2
  32. package/dist/src/pipeline/phases/orm.js.map +1 -1
  33. package/dist/src/pipeline/phases/scope-resolution.d.ts +2 -25
  34. package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
  35. package/dist/src/pipeline/phases/scope-resolution.js +18 -580
  36. package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
  37. package/dist/src/pipeline/phases/wildcard-phase.d.ts.map +1 -1
  38. package/dist/src/pipeline/phases/wildcard-phase.js +6 -1
  39. package/dist/src/pipeline/phases/wildcard-phase.js.map +1 -1
  40. package/dist/src/search/hybrid-query.js +1 -1
  41. package/dist/src/search/hybrid-query.js.map +1 -1
  42. package/dist/src/web/api.d.ts.map +1 -1
  43. package/dist/src/web/api.js +17 -14
  44. package/dist/src/web/api.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +1 -1
  47. package/src/__tests__/pipeline/phases/wildcard-phase.test.ts +39 -4
  48. package/src/analysis/cycles.ts +7 -3
  49. package/src/analysis/trace.ts +1 -21
  50. package/src/analysis/worker-pool.ts +8 -7
  51. package/src/cli/skill-gen.ts +6 -14
  52. package/src/export/html.ts +3 -2
  53. package/src/graph/regex-search.ts +9 -5
  54. package/src/pipeline/orchestrator.ts +18 -0
  55. package/src/pipeline/phases/call-site-extractors.ts +169 -0
  56. package/src/pipeline/phases/module-resolution.ts +294 -0
  57. package/src/pipeline/phases/orm.ts +1 -2
  58. package/src/pipeline/phases/scope-resolution.ts +25 -618
  59. package/src/pipeline/phases/wildcard-phase.ts +5 -1
  60. package/src/search/hybrid-query.ts +1 -1
  61. package/src/web/api.ts +17 -13
@@ -0,0 +1,294 @@
1
+ // ── Module resolution for import specifiers ──────────────────────────────────
2
+
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { join, extname, dirname, resolve as resolvePath } from 'path';
5
+ import type { MonographNode } from '../../types.js';
6
+ import { TS_JS_EXTS, CJS_MJS_EXTS } from './call-site-extractors.js';
7
+
8
+ // ── Import regexes ───────────────────────────────────────────────────────────
9
+
10
+ export const IMPORT_RE = /import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+)))?\s+from\s+['"]([^'"]+)['"]/g;
11
+ export const REQUIRE_RE = /(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
12
+ const PY_FROM_IMPORT_RE = /from\s+([\w.]+)\s+import\s+(.+)/g;
13
+ const PY_IMPORT_RE = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm;
14
+ const GO_IMPORT_RE = /import\s+(?:"([^"]+)"|(?:\w+\s+)?"([^"]+)"|\(\s*([\s\S]*?)\s*\))/g;
15
+ const JAVA_IMPORT_RE = /import\s+(?:static\s+)?([\w.]+)\s*;/g;
16
+ const RUST_USE_RE = /use\s+((?:crate|super|self)(?:::\w+)+)(?:::\{([^}]+)\})?;/g;
17
+ export const REEXPORT_RE = /export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
18
+
19
+ const RESOLVE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
20
+
21
+ const PY_EXTS = new Set(['.py']);
22
+ const GO_EXTS = new Set(['.go']);
23
+ const JAVA_EXTS = new Set(['.java']);
24
+ const RUST_EXTS = new Set(['.rs']);
25
+
26
+ // ── Workspace package map ────────────────────────────────────────────────────
27
+
28
+ const workspacePackageMapCache = new Map<string, Map<string, string>>();
29
+
30
+ export function clearWorkspacePackageMapCache(repoPath: string): void {
31
+ workspacePackageMapCache.delete(repoPath);
32
+ }
33
+
34
+ export function buildWorkspacePackageMap(repoPath: string): Map<string, string> {
35
+ const cached = workspacePackageMapCache.get(repoPath);
36
+ if (cached) return cached;
37
+ const result = new Map<string, string>();
38
+ const packagesDir = join(repoPath, 'packages');
39
+ try {
40
+ const scanDirs = (base: string, depth: number) => {
41
+ if (depth > 2) return;
42
+ let entries: string[];
43
+ try { entries = require('fs').readdirSync(base); } catch { return; }
44
+ for (const e of entries) {
45
+ const full = join(base, e);
46
+ const pkgJson = join(full, 'package.json');
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'));
49
+ if (pkg.name) {
50
+ const relDir = full.slice(repoPath.length + 1);
51
+ result.set(pkg.name, relDir);
52
+ }
53
+ } catch {
54
+ if (e.startsWith('@')) scanDirs(full, depth + 1);
55
+ }
56
+ }
57
+ };
58
+ scanDirs(packagesDir, 0);
59
+ } catch { /* no packages dir */ }
60
+ workspacePackageMapCache.set(repoPath, result);
61
+ return result;
62
+ }
63
+
64
+ // ── Specifier resolvers ──────────────────────────────────────────────────────
65
+
66
+ export function resolveModuleSpecifier(
67
+ importerPath: string,
68
+ specifier: string,
69
+ repoPath: string,
70
+ knownFiles: Set<string>,
71
+ workspaceMap: Map<string, string>,
72
+ ): string | null {
73
+ if (specifier.startsWith('.')) {
74
+ const dir = dirname(importerPath);
75
+ const raw = resolvePath('/', dir, specifier).slice(1);
76
+ const base = raw.replace(/\.(js|jsx)$/, '');
77
+
78
+ for (const candidate of [
79
+ raw,
80
+ base,
81
+ ...RESOLVE_EXTS.map(e => base + e),
82
+ ...RESOLVE_EXTS.map(e => base + '/index' + e),
83
+ ]) {
84
+ if (knownFiles.has(candidate)) return candidate;
85
+ }
86
+
87
+ for (const ext of RESOLVE_EXTS) {
88
+ if (existsSync(join(repoPath, base + ext))) return base + ext;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ for (const [pkgName, pkgDir] of workspaceMap) {
94
+ if (specifier === pkgName) {
95
+ for (const entry of RESOLVE_EXTS.map(e => pkgDir + '/src/index' + e)) {
96
+ if (knownFiles.has(entry)) return entry;
97
+ }
98
+ return null;
99
+ }
100
+ if (specifier.startsWith(pkgName + '/')) {
101
+ const subpath = specifier.slice(pkgName.length + 1);
102
+ const base = pkgDir + '/' + subpath;
103
+ for (const candidate of [
104
+ base,
105
+ ...RESOLVE_EXTS.map(e => base + e),
106
+ ...RESOLVE_EXTS.map(e => base + '/index' + e),
107
+ ]) {
108
+ if (knownFiles.has(candidate)) return candidate;
109
+ }
110
+ return null;
111
+ }
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ function resolvePythonModule(importerPath: string, modulePath: string, knownFiles: Set<string>): string | null {
118
+ const dir = dirname(importerPath);
119
+ for (const base of [dir + '/' + modulePath, modulePath]) {
120
+ for (const candidate of [base + '.py', base + '/__init__.py']) {
121
+ if (knownFiles.has(candidate)) return candidate;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ function resolveGoPackage(_importerPath: string, goPath: string, knownFiles: Set<string>): string | null {
128
+ for (const f of knownFiles) {
129
+ if (f.startsWith(goPath + '/') && f.endsWith('.go')) return f;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function resolveJavaImport(qualifiedName: string, knownFiles: Set<string>): string | null {
135
+ const pathPart = qualifiedName.replace(/\./g, '/');
136
+ for (const candidate of [pathPart + '.java', 'src/main/java/' + pathPart + '.java', 'src/' + pathPart + '.java']) {
137
+ if (knownFiles.has(candidate)) return candidate;
138
+ }
139
+ if (pathPart.endsWith('/*')) {
140
+ const dir = pathPart.slice(0, -2);
141
+ for (const f of knownFiles) {
142
+ if (f.endsWith('.java') && (f.startsWith(dir + '/') || f.includes('/' + dir + '/'))) return f;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function resolveRustUse(usePath: string, importerPath: string, knownFiles: Set<string>): string | null {
149
+ const parts = usePath.split('::');
150
+ if (parts[0] === 'crate') parts[0] = 'src';
151
+ else if (parts[0] === 'super') {
152
+ const dir = dirname(importerPath);
153
+ parts[0] = dirname(dir);
154
+ } else if (parts[0] === 'self') {
155
+ parts[0] = dirname(importerPath);
156
+ }
157
+ for (let len = parts.length; len >= 2; len--) {
158
+ const base = parts.slice(0, len).join('/');
159
+ for (const candidate of [base + '.rs', base + '/mod.rs']) {
160
+ if (knownFiles.has(candidate)) return candidate;
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+
166
+ // ── Import name extraction ───────────────────────────────────────────────────
167
+
168
+ export function extractImportNames(clause: string): string[] {
169
+ return clause
170
+ .split(',')
171
+ .map(s => s.trim().split(/\s+as\s+/).pop()!.trim())
172
+ .filter(Boolean);
173
+ }
174
+
175
+ // ── Build import maps from source ────────────────────────────────────────────
176
+
177
+ export function buildAllImportMapsFromSource(
178
+ repoPath: string,
179
+ fileNodesByPath: Map<string, MonographNode>,
180
+ fileContents?: Map<string, string>,
181
+ ): Map<string, Map<string, string>> {
182
+ const result = new Map<string, Map<string, string>>();
183
+ const knownFiles = new Set(fileNodesByPath.keys());
184
+ const workspaceMap = buildWorkspacePackageMap(repoPath);
185
+
186
+ for (const [filePath, fileNode] of fileNodesByPath) {
187
+ const ext = extname(filePath).toLowerCase();
188
+ const importMap = new Map<string, string>();
189
+
190
+ let source: string | undefined = fileContents?.get(filePath);
191
+ if (!source) {
192
+ try {
193
+ source = readFileSync(join(repoPath, filePath), 'utf-8');
194
+ } catch {
195
+ continue;
196
+ }
197
+ }
198
+
199
+ if (TS_JS_EXTS.has(ext) || ext === '.cjs' || ext === '.mjs') {
200
+ IMPORT_RE.lastIndex = 0;
201
+ let m: RegExpExecArray | null;
202
+ while ((m = IMPORT_RE.exec(source)) !== null) {
203
+ const specifier = m[7];
204
+ const resolved = resolveModuleSpecifier(filePath, specifier, repoPath, knownFiles, workspaceMap);
205
+ if (!resolved) continue;
206
+ if (m[1]) for (const n of extractImportNames(m[1])) importMap.set(n, resolved);
207
+ if (m[4]) for (const n of extractImportNames(m[4])) importMap.set(n, resolved);
208
+ if (m[2]) importMap.set(m[2], resolved);
209
+ if (m[5]) importMap.set(m[5], resolved);
210
+ if (m[3]) importMap.set(m[3], resolved);
211
+ if (m[6]) importMap.set(m[6], resolved);
212
+ const baseName = resolved.split('/').pop()!.replace(/\.\w+$/, '');
213
+ importMap.set(baseName, resolved);
214
+ }
215
+
216
+ REQUIRE_RE.lastIndex = 0;
217
+ while ((m = REQUIRE_RE.exec(source)) !== null) {
218
+ const specifier = m[3];
219
+ const resolved = resolveModuleSpecifier(filePath, specifier, repoPath, knownFiles, workspaceMap);
220
+ if (!resolved) continue;
221
+ if (m[1]) for (const n of extractImportNames(m[1])) importMap.set(n, resolved);
222
+ if (m[2]) importMap.set(m[2], resolved);
223
+ const baseName = resolved.split('/').pop()!.replace(/\.\w+$/, '');
224
+ importMap.set(baseName, resolved);
225
+ }
226
+ } else if (PY_EXTS.has(ext)) {
227
+ PY_FROM_IMPORT_RE.lastIndex = 0;
228
+ let m: RegExpExecArray | null;
229
+ while ((m = PY_FROM_IMPORT_RE.exec(source)) !== null) {
230
+ const modulePath = m[1].replace(/\./g, '/');
231
+ const resolved = resolvePythonModule(filePath, modulePath, knownFiles);
232
+ if (!resolved) continue;
233
+ for (const name of m[2].split(',').map(s => s.trim().split(/\s+as\s+/).pop()!.trim()).filter(Boolean)) {
234
+ importMap.set(name, resolved);
235
+ }
236
+ const baseName = resolved.split('/').pop()!.replace(/\.\w+$/, '');
237
+ importMap.set(baseName, resolved);
238
+ }
239
+ PY_IMPORT_RE.lastIndex = 0;
240
+ while ((m = PY_IMPORT_RE.exec(source)) !== null) {
241
+ const modulePath = m[1].replace(/\./g, '/');
242
+ const resolved = resolvePythonModule(filePath, modulePath, knownFiles);
243
+ if (!resolved) continue;
244
+ const alias = m[2] ?? m[1].split('.').pop()!;
245
+ importMap.set(alias, resolved);
246
+ }
247
+ } else if (GO_EXTS.has(ext)) {
248
+ GO_IMPORT_RE.lastIndex = 0;
249
+ let m: RegExpExecArray | null;
250
+ while ((m = GO_IMPORT_RE.exec(source)) !== null) {
251
+ const paths = m[3]
252
+ ? m[3].match(/"([^"]+)"/g)?.map(s => s.slice(1, -1)) ?? []
253
+ : [m[1] ?? m[2]].filter(Boolean);
254
+ for (const goPath of paths) {
255
+ const resolved = resolveGoPackage(filePath, goPath, knownFiles);
256
+ if (!resolved) continue;
257
+ const pkgName = goPath.split('/').pop()!;
258
+ importMap.set(pkgName, resolved);
259
+ }
260
+ }
261
+ } else if (JAVA_EXTS.has(ext)) {
262
+ JAVA_IMPORT_RE.lastIndex = 0;
263
+ let m: RegExpExecArray | null;
264
+ while ((m = JAVA_IMPORT_RE.exec(source)) !== null) {
265
+ const resolved = resolveJavaImport(m[1], knownFiles);
266
+ if (!resolved) continue;
267
+ const className = m[1].split('.').pop()!;
268
+ importMap.set(className, resolved);
269
+ }
270
+ } else if (RUST_EXTS.has(ext)) {
271
+ RUST_USE_RE.lastIndex = 0;
272
+ let m: RegExpExecArray | null;
273
+ while ((m = RUST_USE_RE.exec(source)) !== null) {
274
+ if (m[2]) {
275
+ for (const name of m[2].split(',').map(s => s.trim()).filter(Boolean)) {
276
+ const resolved = resolveRustUse(m[1] + '::' + name, filePath, knownFiles);
277
+ if (resolved) importMap.set(name.split('::').pop()!, resolved);
278
+ }
279
+ } else {
280
+ const resolved = resolveRustUse(m[1], filePath, knownFiles);
281
+ if (!resolved) continue;
282
+ const itemName = m[1].split('::').pop()!;
283
+ importMap.set(itemName, resolved);
284
+ }
285
+ }
286
+ }
287
+
288
+ if (importMap.size > 0) {
289
+ result.set(fileNode.id, importMap);
290
+ }
291
+ }
292
+
293
+ return result;
294
+ }
@@ -134,6 +134,7 @@ function detectTypeOrmEntities(
134
134
  edges: MonographEdge[],
135
135
  ): void {
136
136
  const entityRe = new RegExp(TYPEORM_ENTITY_RE.source, 'g');
137
+ const lines = source.split('\n');
137
138
  let m: RegExpExecArray | null;
138
139
 
139
140
  while ((m = entityRe.exec(source)) !== null) {
@@ -154,9 +155,7 @@ function detectTypeOrmEntities(
154
155
  entityNodes.push(entityNode);
155
156
 
156
157
  const fields: FieldDef[] = [];
157
- // Use a per-call local regex to avoid shared lastIndex state on the module-level global
158
158
  const colDecRe = new RegExp(TYPEORM_COLUMN_DECORATOR_RE.source, 'g');
159
- const lines = source.split('\n');
160
159
  for (let i = 0; i < lines.length; i++) {
161
160
  const line = lines[i];
162
161
  if (colDecRe.test(line)) {