@shrkcrft/graph 0.1.0-alpha.10

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 (91) hide show
  1. package/dist/index.d.ts +30 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +32 -0
  4. package/dist/indexer/detect-workspace.d.ts +18 -0
  5. package/dist/indexer/detect-workspace.d.ts.map +1 -0
  6. package/dist/indexer/detect-workspace.js +80 -0
  7. package/dist/indexer/extract-csharp-file.d.ts +27 -0
  8. package/dist/indexer/extract-csharp-file.d.ts.map +1 -0
  9. package/dist/indexer/extract-csharp-file.js +163 -0
  10. package/dist/indexer/extract-dart-file.d.ts +28 -0
  11. package/dist/indexer/extract-dart-file.d.ts.map +1 -0
  12. package/dist/indexer/extract-dart-file.js +167 -0
  13. package/dist/indexer/extract-elixir-file.d.ts +27 -0
  14. package/dist/indexer/extract-elixir-file.d.ts.map +1 -0
  15. package/dist/indexer/extract-elixir-file.js +164 -0
  16. package/dist/indexer/extract-go-file.d.ts +28 -0
  17. package/dist/indexer/extract-go-file.d.ts.map +1 -0
  18. package/dist/indexer/extract-go-file.js +156 -0
  19. package/dist/indexer/extract-java-file.d.ts +25 -0
  20. package/dist/indexer/extract-java-file.d.ts.map +1 -0
  21. package/dist/indexer/extract-java-file.js +140 -0
  22. package/dist/indexer/extract-kotlin-file.d.ts +20 -0
  23. package/dist/indexer/extract-kotlin-file.d.ts.map +1 -0
  24. package/dist/indexer/extract-kotlin-file.js +158 -0
  25. package/dist/indexer/extract-php-file.d.ts +26 -0
  26. package/dist/indexer/extract-php-file.d.ts.map +1 -0
  27. package/dist/indexer/extract-php-file.js +161 -0
  28. package/dist/indexer/extract-python-file.d.ts +30 -0
  29. package/dist/indexer/extract-python-file.d.ts.map +1 -0
  30. package/dist/indexer/extract-python-file.js +196 -0
  31. package/dist/indexer/extract-ruby-file.d.ts +29 -0
  32. package/dist/indexer/extract-ruby-file.d.ts.map +1 -0
  33. package/dist/indexer/extract-ruby-file.js +151 -0
  34. package/dist/indexer/extract-rust-file.d.ts +27 -0
  35. package/dist/indexer/extract-rust-file.d.ts.map +1 -0
  36. package/dist/indexer/extract-rust-file.js +186 -0
  37. package/dist/indexer/extract-swift-file.d.ts +27 -0
  38. package/dist/indexer/extract-swift-file.d.ts.map +1 -0
  39. package/dist/indexer/extract-swift-file.js +168 -0
  40. package/dist/indexer/extract-ts-file.d.ts +79 -0
  41. package/dist/indexer/extract-ts-file.d.ts.map +1 -0
  42. package/dist/indexer/extract-ts-file.js +403 -0
  43. package/dist/indexer/incremental-updater.d.ts +41 -0
  44. package/dist/indexer/incremental-updater.d.ts.map +1 -0
  45. package/dist/indexer/incremental-updater.js +395 -0
  46. package/dist/indexer/index-builder.d.ts +23 -0
  47. package/dist/indexer/index-builder.d.ts.map +1 -0
  48. package/dist/indexer/index-builder.js +289 -0
  49. package/dist/indexer/resolve-imports.d.ts +36 -0
  50. package/dist/indexer/resolve-imports.d.ts.map +1 -0
  51. package/dist/indexer/resolve-imports.js +144 -0
  52. package/dist/indexer/unresolved-imports.d.ts +20 -0
  53. package/dist/indexer/unresolved-imports.d.ts.map +1 -0
  54. package/dist/indexer/unresolved-imports.js +32 -0
  55. package/dist/query/cycle-detection.d.ts +40 -0
  56. package/dist/query/cycle-detection.d.ts.map +1 -0
  57. package/dist/query/cycle-detection.js +135 -0
  58. package/dist/query/query-api.d.ts +87 -0
  59. package/dist/query/query-api.d.ts.map +1 -0
  60. package/dist/query/query-api.js +232 -0
  61. package/dist/schema/edge-kind.d.ts +31 -0
  62. package/dist/schema/edge-kind.d.ts.map +1 -0
  63. package/dist/schema/edge-kind.js +35 -0
  64. package/dist/schema/edge.d.ts +22 -0
  65. package/dist/schema/edge.d.ts.map +1 -0
  66. package/dist/schema/edge.js +1 -0
  67. package/dist/schema/file-fingerprint.d.ts +22 -0
  68. package/dist/schema/file-fingerprint.d.ts.map +1 -0
  69. package/dist/schema/file-fingerprint.js +1 -0
  70. package/dist/schema/graph-snapshot.d.ts +18 -0
  71. package/dist/schema/graph-snapshot.d.ts.map +1 -0
  72. package/dist/schema/graph-snapshot.js +1 -0
  73. package/dist/schema/manifest.d.ts +47 -0
  74. package/dist/schema/manifest.d.ts.map +1 -0
  75. package/dist/schema/manifest.js +1 -0
  76. package/dist/schema/node-kind.d.ts +21 -0
  77. package/dist/schema/node-kind.d.ts.map +1 -0
  78. package/dist/schema/node-kind.js +27 -0
  79. package/dist/schema/node.d.ts +26 -0
  80. package/dist/schema/node.d.ts.map +1 -0
  81. package/dist/schema/node.js +1 -0
  82. package/dist/schema/schema-version.d.ts +10 -0
  83. package/dist/schema/schema-version.d.ts.map +1 -0
  84. package/dist/schema/schema-version.js +8 -0
  85. package/dist/store/file-fingerprint.d.ts +8 -0
  86. package/dist/store/file-fingerprint.d.ts.map +1 -0
  87. package/dist/store/file-fingerprint.js +64 -0
  88. package/dist/store/graph-store.d.ts +48 -0
  89. package/dist/store/graph-store.d.ts.map +1 -0
  90. package/dist/store/graph-store.js +194 -0
  91. package/package.json +54 -0
@@ -0,0 +1,289 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdirSync, statSync } from 'node:fs';
3
+ import * as nodePath from 'node:path';
4
+ import { EdgeKind } from "../schema/edge-kind.js";
5
+ import { NodeKind } from "../schema/node-kind.js";
6
+ import { fingerprintFile } from "../store/file-fingerprint.js";
7
+ import { GraphStore } from "../store/graph-store.js";
8
+ import { summarizeCycles } from "../query/cycle-detection.js";
9
+ import { summarizeUnresolvedImports } from "./unresolved-imports.js";
10
+ import { detectWorkspacePackages, } from "./detect-workspace.js";
11
+ import { EXTRACT_TS_FILE_SOURCE, extractTsFile, stitchPerFileReferences, } from "./extract-ts-file.js";
12
+ import { extractPythonFile } from "./extract-python-file.js";
13
+ import { extractGoFile } from "./extract-go-file.js";
14
+ import { extractJavaFile } from "./extract-java-file.js";
15
+ import { extractRustFile } from "./extract-rust-file.js";
16
+ import { extractKotlinFile } from "./extract-kotlin-file.js";
17
+ import { extractRubyFile } from "./extract-ruby-file.js";
18
+ import { extractCsharpFile } from "./extract-csharp-file.js";
19
+ import { extractElixirFile } from "./extract-elixir-file.js";
20
+ import { extractPhpFile } from "./extract-php-file.js";
21
+ import { extractDartFile } from "./extract-dart-file.js";
22
+ import { extractSwiftFile } from "./extract-swift-file.js";
23
+ import { createImportResolverContext, ImportResolution, resolveImport, } from "./resolve-imports.js";
24
+ const SOURCE_EXTS = new Set([
25
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
26
+ // Web component formats — parsed by framework-scanners; the TS-AST
27
+ // extractor short-circuits on these.
28
+ '.vue', '.svelte', '.astro',
29
+ // Non-TS languages — handled by the per-language dispatcher.
30
+ '.py', '.go', '.java', '.rs', '.kt', '.kts', '.rb', '.cs', '.csx', '.ex', '.exs', '.php',
31
+ '.dart', '.swift',
32
+ // Schema-definition formats — File nodes only; framework-scanners
33
+ // does the SDL parsing.
34
+ '.graphql', '.gql',
35
+ ]);
36
+ const SKIP_DIRS = new Set([
37
+ 'node_modules',
38
+ 'dist',
39
+ 'build',
40
+ 'coverage',
41
+ '.git',
42
+ '.sharkcraft',
43
+ '.next',
44
+ '.cache',
45
+ '.tmp-pack',
46
+ 'out',
47
+ 'target',
48
+ ]);
49
+ /**
50
+ * Build a full graph index and write it to disk. Overwrites any
51
+ * pre-existing store under `<root>/.sharkcraft/graph/`.
52
+ *
53
+ * Single-process, no worker pool yet. The compiler-API per-file extractor
54
+ * is fast enough at SharkCraft's size; worker-pool parallelism is a
55
+ * later optimisation tied to measured budgets (see code-intelligence.md
56
+ * §7).
57
+ */
58
+ export function buildFullIndex(options) {
59
+ const start = Date.now();
60
+ const { projectRoot } = options;
61
+ const ignore = new Set([...SKIP_DIRS, ...(options.extraIgnore ?? [])]);
62
+ const sourceFiles = walkSources(projectRoot, ignore, options.maxFiles ?? 0);
63
+ const workspaces = detectWorkspacePackages(projectRoot);
64
+ const resolverCtx = createImportResolverContext(projectRoot, workspaces);
65
+ const nodes = [];
66
+ const edges = [];
67
+ const fingerprints = [];
68
+ const packageNodes = buildPackageNodes(workspaces);
69
+ for (const n of packageNodes)
70
+ nodes.push(n);
71
+ const fileIdByPath = new Map();
72
+ const packageDirIndex = buildPackageDirIndex(workspaces);
73
+ // Track per-file extracted + resolved imports so the stitcher pass
74
+ // can emit references-symbol / calls-symbol edges with cross-file
75
+ // targets resolved.
76
+ const extractedByPath = new Map();
77
+ const resolvedSpecByPath = new Map();
78
+ const defaultExportNameByPath = new Map();
79
+ for (const abs of sourceFiles) {
80
+ const fp = fingerprintFile(abs, projectRoot);
81
+ fingerprints.push(fp);
82
+ fileIdByPath.set(fp.path, fp.nodeId);
83
+ const extracted = fp.language === 'python' ? extractPythonFile(fp, abs)
84
+ : fp.language === 'go' ? extractGoFile(fp, abs)
85
+ : fp.language === 'java' ? extractJavaFile(fp, abs)
86
+ : fp.language === 'rust' ? extractRustFile(fp, abs)
87
+ : fp.language === 'kotlin' ? extractKotlinFile(fp, abs)
88
+ : fp.language === 'ruby' ? extractRubyFile(fp, abs)
89
+ : fp.language === 'csharp' ? extractCsharpFile(fp, abs)
90
+ : fp.language === 'elixir' ? extractElixirFile(fp, abs)
91
+ : fp.language === 'php' ? extractPhpFile(fp, abs)
92
+ : fp.language === 'dart' ? extractDartFile(fp, abs)
93
+ : fp.language === 'swift' ? extractSwiftFile(fp, abs)
94
+ : extractTsFile(fp, abs);
95
+ nodes.push(extracted.fileNode);
96
+ for (const sym of extracted.symbolNodes)
97
+ nodes.push(sym);
98
+ for (const e of extracted.edges)
99
+ edges.push(e);
100
+ extractedByPath.set(fp.path, extracted);
101
+ defaultExportNameByPath.set(fp.path, extracted.fileNode.data?.['defaultExportName'] ?? undefined);
102
+ // BelongsToPackage edge for files inside a known package dir.
103
+ const pkg = findOwningPackage(fp.path, packageDirIndex);
104
+ if (pkg) {
105
+ edges.push(buildEdge(fp.nodeId, `package:${pkg.name}`, EdgeKind.BelongsToPackage, EXTRACT_TS_FILE_SOURCE));
106
+ }
107
+ // ImportsFile edges (resolved where possible).
108
+ const resolvedSpec = new Map();
109
+ for (const raw of extracted.rawImportSpecifiers) {
110
+ const r = resolveImport(raw.specifier, abs, resolverCtx);
111
+ resolvedSpec.set(raw.specifier, r.targetPath);
112
+ const data = {
113
+ specifier: r.specifier,
114
+ line: raw.line,
115
+ importKind: raw.kind,
116
+ resolutionKind: r.kind,
117
+ };
118
+ if (r.targetPath) {
119
+ const targetId = `file:${r.targetPath}`;
120
+ edges.push(buildEdge(fp.nodeId, targetId, EdgeKind.ImportsFile, EXTRACT_TS_FILE_SOURCE, data));
121
+ }
122
+ else {
123
+ const externalId = r.kind === ImportResolution.External
124
+ ? `external:${r.specifier}`
125
+ : `unresolved:${r.specifier}`;
126
+ edges.push(buildEdge(fp.nodeId, externalId, EdgeKind.ImportsFile, EXTRACT_TS_FILE_SOURCE, data));
127
+ }
128
+ }
129
+ resolvedSpecByPath.set(fp.path, resolvedSpec);
130
+ }
131
+ // Stitch references / calls edges now that bindings + targets are all
132
+ // collected. Loops over the same files; cheap.
133
+ for (const [path, extracted] of extractedByPath) {
134
+ const localNames = new Map();
135
+ for (const sym of extracted.symbolNodes)
136
+ localNames.set(sym.label, sym.id);
137
+ const refEdges = stitchPerFileReferences({
138
+ fileNodeId: extracted.fileNode.id,
139
+ extracted,
140
+ resolvedSpec: resolvedSpecByPath.get(path) ?? new Map(),
141
+ defaultExportNameByPath,
142
+ localSymbolNamesInThisFile: localNames,
143
+ });
144
+ for (const e of refEdges)
145
+ edges.push(e);
146
+ }
147
+ // PackageDependsOn aggregates: collapse internal ImportsFile edges to
148
+ // their owning package on both sides.
149
+ collectPackageDependsOn(edges, packageDirIndex);
150
+ // Drop duplicate edges (extractor may emit identical edges for `export
151
+ // { foo } from './foo'` and an `import` re-using the same line — same
152
+ // hashed id, last write wins). Edge dedupe by id.
153
+ const seen = new Set();
154
+ const dedupedEdges = [];
155
+ for (const e of edges) {
156
+ if (seen.has(e.id))
157
+ continue;
158
+ seen.add(e.id);
159
+ dedupedEdges.push(e);
160
+ }
161
+ const store = new GraphStore(projectRoot);
162
+ const cycles = summarizeCycles(nodes, dedupedEdges);
163
+ const unresolved = summarizeUnresolvedImports(dedupedEdges);
164
+ const manifest = store.writeSnapshot(nodes, dedupedEdges, fingerprints, {
165
+ projectRoot,
166
+ lastIndexedAt: new Date().toISOString(),
167
+ lastIndexDurationMs: Date.now() - start,
168
+ filesIndexed: fingerprints.length,
169
+ workspacePackages: workspaces.map((w) => w.name),
170
+ // nodesByKind / edgesByKind are filled in by the store.
171
+ nodesByKind: {},
172
+ edgesByKind: {},
173
+ cycleCount: cycles.cycleCount,
174
+ largestCycleSize: cycles.largestCycleSize,
175
+ filesInCycles: cycles.filesInCycles,
176
+ unresolvedImportCount: unresolved.unresolvedImportCount,
177
+ filesWithUnresolvedImports: unresolved.filesWithUnresolvedImports,
178
+ unresolvedImportSamples: unresolved.unresolvedImportSamples,
179
+ });
180
+ return { manifest, durationMs: Date.now() - start };
181
+ }
182
+ function walkSources(projectRoot, ignore, maxFiles) {
183
+ const out = [];
184
+ const stack = [projectRoot];
185
+ while (stack.length > 0) {
186
+ if (maxFiles > 0 && out.length >= maxFiles)
187
+ break;
188
+ const dir = stack.pop();
189
+ let entries;
190
+ try {
191
+ entries = readdirSync(dir);
192
+ }
193
+ catch {
194
+ continue;
195
+ }
196
+ for (const name of entries) {
197
+ if (ignore.has(name))
198
+ continue;
199
+ if (name.startsWith('.') && name !== '.')
200
+ continue;
201
+ const full = nodePath.join(dir, name);
202
+ let st;
203
+ try {
204
+ st = statSync(full);
205
+ }
206
+ catch {
207
+ continue;
208
+ }
209
+ if (st.isDirectory()) {
210
+ stack.push(full);
211
+ continue;
212
+ }
213
+ if (!st.isFile())
214
+ continue;
215
+ if (!SOURCE_EXTS.has(nodePath.extname(full).toLowerCase()))
216
+ continue;
217
+ out.push(full);
218
+ if (maxFiles > 0 && out.length >= maxFiles)
219
+ break;
220
+ }
221
+ }
222
+ return out.sort();
223
+ }
224
+ function buildPackageNodes(packages) {
225
+ return packages.map((p) => ({
226
+ id: `package:${p.name}`,
227
+ kind: NodeKind.Package,
228
+ label: p.name,
229
+ path: p.dir,
230
+ data: {
231
+ ...(p.entry ? { entry: p.entry } : {}),
232
+ },
233
+ }));
234
+ }
235
+ function buildPackageDirIndex(packages) {
236
+ // Sorted by length desc so the most-specific match wins (e.g.
237
+ // `packages/foo/sub` resolves before `packages/foo`).
238
+ const entries = [...packages]
239
+ .map((p) => ({ dir: p.dir.replace(/\/+$/, ''), name: p.name }))
240
+ .sort((a, b) => b.dir.length - a.dir.length);
241
+ return { entries };
242
+ }
243
+ function findOwningPackage(filePath, index) {
244
+ for (const e of index.entries) {
245
+ if (filePath === e.dir || filePath.startsWith(e.dir + '/')) {
246
+ return e;
247
+ }
248
+ }
249
+ return undefined;
250
+ }
251
+ function collectPackageDependsOn(edges, index) {
252
+ const pairs = new Map();
253
+ for (const e of edges) {
254
+ if (e.kind !== EdgeKind.ImportsFile)
255
+ continue;
256
+ const fromFile = stripPrefix(e.from, 'file:');
257
+ const toFile = stripPrefix(e.to, 'file:');
258
+ if (!fromFile || !toFile)
259
+ continue; // skip external / unresolved targets
260
+ const fromPkg = findOwningPackage(fromFile, index);
261
+ const toPkg = findOwningPackage(toFile, index);
262
+ if (!fromPkg || !toPkg)
263
+ continue;
264
+ if (fromPkg.name === toPkg.name)
265
+ continue;
266
+ const k = `${fromPkg.name}|${toPkg.name}`;
267
+ const cur = pairs.get(k);
268
+ if (cur)
269
+ cur.count += 1;
270
+ else
271
+ pairs.set(k, { from: fromPkg.name, to: toPkg.name, count: 1 });
272
+ }
273
+ for (const { from, to, count } of pairs.values()) {
274
+ edges.push(buildEdge(`package:${from}`, `package:${to}`, EdgeKind.PackageDependsOn, 'index-builder@v1', { count }));
275
+ }
276
+ }
277
+ function buildEdge(from, to, kind, source, data) {
278
+ return {
279
+ id: createHash('sha1').update(`${from}|${to}|${kind}`).digest('hex'),
280
+ from,
281
+ to,
282
+ kind,
283
+ source,
284
+ ...(data ? { data } : {}),
285
+ };
286
+ }
287
+ function stripPrefix(id, prefix) {
288
+ return id.startsWith(prefix) ? id.slice(prefix.length) : undefined;
289
+ }
@@ -0,0 +1,36 @@
1
+ import { type ITsconfigPathsMap } from '@shrkcrft/boundaries';
2
+ import type { IWorkspacePackage } from './detect-workspace.js';
3
+ export declare enum ImportResolution {
4
+ Relative = "relative",
5
+ Alias = "alias",
6
+ Workspace = "workspace",
7
+ External = "external",
8
+ Unresolved = "unresolved"
9
+ }
10
+ export interface IResolvedImport {
11
+ /** Project-relative path to the target file, POSIX, when resolved. */
12
+ targetPath?: string;
13
+ kind: ImportResolution;
14
+ /** Specifier as it appeared in source. */
15
+ specifier: string;
16
+ }
17
+ export interface IImportResolverContext {
18
+ projectRoot: string;
19
+ workspacePackages: readonly IWorkspacePackage[];
20
+ tsconfigPaths: ITsconfigPathsMap;
21
+ }
22
+ export declare function createImportResolverContext(projectRoot: string, workspacePackages: readonly IWorkspacePackage[]): IImportResolverContext;
23
+ /**
24
+ * Resolve an import specifier to a project-relative file path.
25
+ *
26
+ * Resolution order (cheapest first):
27
+ * 1. Relative (`./` or `../`) against the source file's directory.
28
+ * 2. tsconfig path aliases.
29
+ * 3. Workspace package name (exact match or `<pkg>/<sub>`).
30
+ * 4. External — left unresolved with `ImportResolution.External`.
31
+ *
32
+ * `fromAbsPath` is the absolute path of the importing file; needed for
33
+ * relative resolution.
34
+ */
35
+ export declare function resolveImport(specifier: string, fromAbsPath: string, ctx: IImportResolverContext): IResolvedImport;
36
+ //# sourceMappingURL=resolve-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-imports.d.ts","sourceRoot":"","sources":["../../src/indexer/resolve-imports.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAI/D,oBAAY,gBAAgB;IAC1B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,QAAQ,aAAa;IACrB,UAAU,eAAe;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAED,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,GAC9C,sBAAsB,CAMxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,sBAAsB,GAC1B,eAAe,CAwCjB"}
@@ -0,0 +1,144 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { loadTsconfigPaths, resolveAliasCandidates, } from '@shrkcrft/boundaries';
4
+ const PROBE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
5
+ export var ImportResolution;
6
+ (function (ImportResolution) {
7
+ ImportResolution["Relative"] = "relative";
8
+ ImportResolution["Alias"] = "alias";
9
+ ImportResolution["Workspace"] = "workspace";
10
+ ImportResolution["External"] = "external";
11
+ ImportResolution["Unresolved"] = "unresolved";
12
+ })(ImportResolution || (ImportResolution = {}));
13
+ export function createImportResolverContext(projectRoot, workspacePackages) {
14
+ return {
15
+ projectRoot,
16
+ workspacePackages,
17
+ tsconfigPaths: loadTsconfigPaths(projectRoot),
18
+ };
19
+ }
20
+ /**
21
+ * Resolve an import specifier to a project-relative file path.
22
+ *
23
+ * Resolution order (cheapest first):
24
+ * 1. Relative (`./` or `../`) against the source file's directory.
25
+ * 2. tsconfig path aliases.
26
+ * 3. Workspace package name (exact match or `<pkg>/<sub>`).
27
+ * 4. External — left unresolved with `ImportResolution.External`.
28
+ *
29
+ * `fromAbsPath` is the absolute path of the importing file; needed for
30
+ * relative resolution.
31
+ */
32
+ export function resolveImport(specifier, fromAbsPath, ctx) {
33
+ if (specifier.startsWith('.')) {
34
+ const dir = nodePath.dirname(fromAbsPath);
35
+ const probe = probeCandidate(nodePath.resolve(dir, specifier));
36
+ if (probe) {
37
+ return {
38
+ specifier,
39
+ targetPath: toProjectRel(ctx.projectRoot, probe),
40
+ kind: ImportResolution.Relative,
41
+ };
42
+ }
43
+ return { specifier, kind: ImportResolution.Unresolved };
44
+ }
45
+ const aliasCandidates = resolveAliasCandidates(specifier, ctx.tsconfigPaths);
46
+ for (const cand of aliasCandidates) {
47
+ const abs = nodePath.resolve(ctx.projectRoot, cand);
48
+ const probe = probeCandidate(abs);
49
+ if (probe) {
50
+ return {
51
+ specifier,
52
+ targetPath: toProjectRel(ctx.projectRoot, probe),
53
+ kind: ImportResolution.Alias,
54
+ };
55
+ }
56
+ }
57
+ const ws = findWorkspacePackage(specifier, ctx.workspacePackages);
58
+ if (ws) {
59
+ const probe = resolveWorkspaceTarget(specifier, ws, ctx.projectRoot);
60
+ if (probe) {
61
+ return {
62
+ specifier,
63
+ targetPath: toProjectRel(ctx.projectRoot, probe),
64
+ kind: ImportResolution.Workspace,
65
+ };
66
+ }
67
+ }
68
+ return { specifier, kind: ImportResolution.External };
69
+ }
70
+ function probeCandidate(absPathNoExt) {
71
+ // If the path already has a known extension and exists, return it.
72
+ const ext = nodePath.extname(absPathNoExt);
73
+ if (PROBE_EXTS.includes(ext) && existsSafe(absPathNoExt) && isFile(absPathNoExt)) {
74
+ return absPathNoExt;
75
+ }
76
+ // Try appending each known extension.
77
+ for (const e of PROBE_EXTS) {
78
+ const cand = absPathNoExt + e;
79
+ if (existsSafe(cand) && isFile(cand))
80
+ return cand;
81
+ }
82
+ // Try as a directory with index.<ext>.
83
+ if (existsSafe(absPathNoExt) && isDir(absPathNoExt)) {
84
+ for (const e of PROBE_EXTS) {
85
+ const cand = nodePath.join(absPathNoExt, `index${e}`);
86
+ if (existsSafe(cand) && isFile(cand))
87
+ return cand;
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+ function findWorkspacePackage(specifier, packages) {
93
+ for (const p of packages) {
94
+ if (specifier === p.name)
95
+ return p;
96
+ if (specifier.startsWith(p.name + '/'))
97
+ return p;
98
+ }
99
+ return undefined;
100
+ }
101
+ function resolveWorkspaceTarget(specifier, pkg, projectRoot) {
102
+ // Exact match → resolve via package entry, falling back to src/index.<ext>.
103
+ if (specifier === pkg.name) {
104
+ if (pkg.entry) {
105
+ const cand = nodePath.resolve(projectRoot, pkg.entry);
106
+ if (existsSafe(cand) && isFile(cand))
107
+ return cand;
108
+ // Some packages list ./dist/index.js as main; for source-time graphs
109
+ // we'd rather hit the src/. Try that next.
110
+ }
111
+ const srcIndex = nodePath.resolve(projectRoot, pkg.dir, 'src');
112
+ return probeCandidate(srcIndex);
113
+ }
114
+ // Subpath: `<pkg>/foo/bar`.
115
+ const sub = specifier.slice(pkg.name.length + 1);
116
+ return probeCandidate(nodePath.resolve(projectRoot, pkg.dir, sub));
117
+ }
118
+ function existsSafe(p) {
119
+ try {
120
+ return existsSync(p);
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
126
+ function isFile(p) {
127
+ try {
128
+ return statSync(p).isFile();
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ function isDir(p) {
135
+ try {
136
+ return statSync(p).isDirectory();
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ function toProjectRel(projectRoot, abs) {
143
+ return nodePath.relative(projectRoot, abs).split(nodePath.sep).join('/');
144
+ }
@@ -0,0 +1,20 @@
1
+ import type { IEdge } from '../schema/edge.js';
2
+ export interface IUnresolvedImportSummary {
3
+ /** Total `unresolved:<spec>` ImportsFile edges. */
4
+ unresolvedImportCount: number;
5
+ /** Distinct file ids with at least one unresolved import. */
6
+ filesWithUnresolvedImports: number;
7
+ /** Up to `sampleLimit` distinct specifier strings. */
8
+ unresolvedImportSamples: readonly string[];
9
+ }
10
+ /**
11
+ * Roll up unresolved imports from the indexer's edge list. The indexer
12
+ * already emits edges targeting `unresolved:<spec>` when the resolver
13
+ * fails (relative path doesn't exist, alias points nowhere, workspace
14
+ * package not found). This helper counts them so the manifest can
15
+ * carry a stable counter the doctor + dashboard read from.
16
+ *
17
+ * Pure function over the dedupe'd edge set; no I/O.
18
+ */
19
+ export declare function summarizeUnresolvedImports(edges: readonly IEdge[], sampleLimit?: number): IUnresolvedImportSummary;
20
+ //# sourceMappingURL=unresolved-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unresolved-imports.d.ts","sourceRoot":"","sources":["../../src/indexer/unresolved-imports.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,qBAAqB,EAAE,MAAM,CAAC;IAC9B,6DAA6D;IAC7D,0BAA0B,EAAE,MAAM,CAAC;IACnC,sDAAsD;IACtD,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C;AAID;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,WAAW,GAAE,MAA6B,GACzC,wBAAwB,CAkB1B"}
@@ -0,0 +1,32 @@
1
+ import { EdgeKind } from "../schema/edge-kind.js";
2
+ const DEFAULT_SAMPLE_LIMIT = 10;
3
+ /**
4
+ * Roll up unresolved imports from the indexer's edge list. The indexer
5
+ * already emits edges targeting `unresolved:<spec>` when the resolver
6
+ * fails (relative path doesn't exist, alias points nowhere, workspace
7
+ * package not found). This helper counts them so the manifest can
8
+ * carry a stable counter the doctor + dashboard read from.
9
+ *
10
+ * Pure function over the dedupe'd edge set; no I/O.
11
+ */
12
+ export function summarizeUnresolvedImports(edges, sampleLimit = DEFAULT_SAMPLE_LIMIT) {
13
+ let unresolvedImportCount = 0;
14
+ const fileSet = new Set();
15
+ const sampleSet = new Set();
16
+ for (const e of edges) {
17
+ if (e.kind !== EdgeKind.ImportsFile)
18
+ continue;
19
+ if (!e.to.startsWith('unresolved:'))
20
+ continue;
21
+ unresolvedImportCount += 1;
22
+ fileSet.add(e.from);
23
+ if (sampleSet.size < sampleLimit) {
24
+ sampleSet.add(e.to.slice('unresolved:'.length));
25
+ }
26
+ }
27
+ return {
28
+ unresolvedImportCount,
29
+ filesWithUnresolvedImports: fileSet.size,
30
+ unresolvedImportSamples: [...sampleSet].sort(),
31
+ };
32
+ }
@@ -0,0 +1,40 @@
1
+ import type { IEdge } from '../schema/edge.js';
2
+ import type { INode } from '../schema/node.js';
3
+ export interface ICycleSummary {
4
+ /** Number of strongly-connected components of size ≥ 2. */
5
+ cycleCount: number;
6
+ /** Size of the largest SCC of size ≥ 2 (0 if no cycles). */
7
+ largestCycleSize: number;
8
+ /** Total number of file nodes participating in some cycle. */
9
+ filesInCycles: number;
10
+ }
11
+ /**
12
+ * A single import cycle. `nodeIds` are the participating `file:<path>`
13
+ * node ids, in iteration order from Tarjan SCC (the entry node is at
14
+ * index 0; the cycle is undirected from a presentation standpoint).
15
+ * Renderers usually want to display `paths` instead — file ids are
16
+ * stable but bear the `file:` prefix.
17
+ */
18
+ export interface IFileCycle {
19
+ /** Stable file node ids that form the cycle. */
20
+ nodeIds: readonly string[];
21
+ /** Project-relative file paths (filled when callers can resolve them). */
22
+ paths?: readonly string[];
23
+ /** Cycle size (== nodeIds.length). */
24
+ size: number;
25
+ }
26
+ /**
27
+ * Find every import cycle in the file-import directed graph. Returns
28
+ * one entry per SCC of size ≥ 2. Iterative Tarjan SCC over the
29
+ * `imports-file` subgraph; O(V+E) and stack-safe.
30
+ *
31
+ * `pathById` is optional — when supplied, the returned `paths` array
32
+ * is populated so callers don't have to re-resolve file ids.
33
+ */
34
+ export declare function findFileCycles(nodes: readonly INode[], edges: readonly IEdge[], pathById?: ReadonlyMap<string, string>): readonly IFileCycle[];
35
+ /**
36
+ * Roll-up over `findFileCycles` results. Kept for downstream callers
37
+ * (the indexer, doctor) that only care about counts.
38
+ */
39
+ export declare function summarizeCycles(nodes: readonly INode[], edges: readonly IEdge[]): ICycleSummary;
40
+ //# sourceMappingURL=cycle-detection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cycle-detection.d.ts","sourceRoot":"","sources":["../../src/query/cycle-detection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,SAAS,UAAU,EAAE,CAsBvB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,KAAK,EAAE,SAAS,KAAK,EAAE,GACtB,aAAa,CAaf"}