@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,135 @@
1
+ import { EdgeKind } from "../schema/edge-kind.js";
2
+ /**
3
+ * Find every import cycle in the file-import directed graph. Returns
4
+ * one entry per SCC of size ≥ 2. Iterative Tarjan SCC over the
5
+ * `imports-file` subgraph; O(V+E) and stack-safe.
6
+ *
7
+ * `pathById` is optional — when supplied, the returned `paths` array
8
+ * is populated so callers don't have to re-resolve file ids.
9
+ */
10
+ export function findFileCycles(nodes, edges, pathById) {
11
+ const adj = buildFileAdjacency(nodes, edges);
12
+ const sccs = stronglyConnectedComponentsIterative(adj);
13
+ const out = [];
14
+ for (const scc of sccs) {
15
+ if (scc.length < 2)
16
+ continue;
17
+ const cycle = {
18
+ nodeIds: [...scc],
19
+ size: scc.length,
20
+ };
21
+ if (pathById) {
22
+ cycle.paths = scc.map((id) => pathById.get(id) ?? id.replace(/^file:/, ''));
23
+ }
24
+ out.push(cycle);
25
+ }
26
+ // Stable order: by size DESC, then by first id ASC. Makes "show me
27
+ // the worst cycle first" deterministic across runs.
28
+ out.sort((a, b) => {
29
+ if (b.size !== a.size)
30
+ return b.size - a.size;
31
+ return (a.nodeIds[0] ?? '').localeCompare(b.nodeIds[0] ?? '');
32
+ });
33
+ return out;
34
+ }
35
+ /**
36
+ * Roll-up over `findFileCycles` results. Kept for downstream callers
37
+ * (the indexer, doctor) that only care about counts.
38
+ */
39
+ export function summarizeCycles(nodes, edges) {
40
+ const cycles = findFileCycles(nodes, edges);
41
+ let largestCycleSize = 0;
42
+ let filesInCycles = 0;
43
+ for (const c of cycles) {
44
+ if (c.size > largestCycleSize)
45
+ largestCycleSize = c.size;
46
+ filesInCycles += c.size;
47
+ }
48
+ return {
49
+ cycleCount: cycles.length,
50
+ largestCycleSize,
51
+ filesInCycles,
52
+ };
53
+ }
54
+ function buildFileAdjacency(nodes, edges) {
55
+ const fileIds = new Set();
56
+ for (const n of nodes) {
57
+ if (n.id.startsWith('file:'))
58
+ fileIds.add(n.id);
59
+ }
60
+ const adj = new Map();
61
+ for (const id of fileIds)
62
+ adj.set(id, []);
63
+ for (const e of edges) {
64
+ if (e.kind !== EdgeKind.ImportsFile)
65
+ continue;
66
+ if (!fileIds.has(e.from) || !fileIds.has(e.to))
67
+ continue;
68
+ adj.get(e.from).push(e.to);
69
+ }
70
+ return adj;
71
+ }
72
+ /**
73
+ * Iterative Tarjan's strongly connected components. Returns one array
74
+ * per SCC; singletons are included so callers that care about all of
75
+ * them (e.g. `IGraphQueryApi.cycles()` once that ships) can filter
76
+ * themselves. Tarjan is single-pass O(V+E), and unlike the recursive
77
+ * form this version doesn't blow the stack on long import chains.
78
+ */
79
+ function stronglyConnectedComponentsIterative(adj) {
80
+ const nodeIds = [...adj.keys()];
81
+ const indices = new Map();
82
+ const lowlinks = new Map();
83
+ const onStack = new Set();
84
+ const stack = [];
85
+ const result = [];
86
+ let index = 0;
87
+ for (const start of nodeIds) {
88
+ if (indices.has(start))
89
+ continue;
90
+ const callStack = [{ v: start, iter: 0 }];
91
+ indices.set(start, index);
92
+ lowlinks.set(start, index);
93
+ index += 1;
94
+ stack.push(start);
95
+ onStack.add(start);
96
+ while (callStack.length > 0) {
97
+ const top = callStack[callStack.length - 1];
98
+ const successors = adj.get(top.v) ?? [];
99
+ if (top.iter < successors.length) {
100
+ const w = successors[top.iter];
101
+ top.iter += 1;
102
+ if (!indices.has(w)) {
103
+ indices.set(w, index);
104
+ lowlinks.set(w, index);
105
+ index += 1;
106
+ stack.push(w);
107
+ onStack.add(w);
108
+ callStack.push({ v: w, iter: 0 });
109
+ }
110
+ else if (onStack.has(w)) {
111
+ lowlinks.set(top.v, Math.min(lowlinks.get(top.v), indices.get(w)));
112
+ }
113
+ }
114
+ else {
115
+ if (lowlinks.get(top.v) === indices.get(top.v)) {
116
+ const scc = [];
117
+ while (true) {
118
+ const w = stack.pop();
119
+ onStack.delete(w);
120
+ scc.push(w);
121
+ if (w === top.v)
122
+ break;
123
+ }
124
+ result.push(scc);
125
+ }
126
+ callStack.pop();
127
+ if (callStack.length > 0) {
128
+ const parent = callStack[callStack.length - 1];
129
+ lowlinks.set(parent.v, Math.min(lowlinks.get(parent.v), lowlinks.get(top.v)));
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return result;
135
+ }
@@ -0,0 +1,87 @@
1
+ import type { IEdge } from '../schema/edge.js';
2
+ import type { IGraphSnapshot } from '../schema/graph-snapshot.js';
3
+ import type { INode } from '../schema/node.js';
4
+ import { type IFileCycle } from './cycle-detection.js';
5
+ export interface IGraphStatus {
6
+ exists: boolean;
7
+ state: 'fresh' | 'stale' | 'missing' | 'corrupt';
8
+ fileCount: number;
9
+ nodeCount: number;
10
+ edgeCount: number;
11
+ lastIndexedAt?: string;
12
+ digestOk?: boolean;
13
+ }
14
+ export interface IGraphNeighbours {
15
+ node: INode;
16
+ /** Outgoing edges (this → other). */
17
+ out: readonly {
18
+ edge: IEdge;
19
+ target: INode | {
20
+ id: string;
21
+ resolved: false;
22
+ };
23
+ }[];
24
+ /** Incoming edges (other → this). */
25
+ in: readonly {
26
+ edge: IEdge;
27
+ source: INode | {
28
+ id: string;
29
+ resolved: false;
30
+ };
31
+ }[];
32
+ }
33
+ export interface IFindSymbolOptions {
34
+ /** Filter by symbol package. */
35
+ package?: string;
36
+ /** Hard cap on returned matches. Default 50. */
37
+ limit?: number;
38
+ /** Exact match required (no case-insensitive / prefix matching). */
39
+ exact?: boolean;
40
+ }
41
+ /**
42
+ * Read-only query layer over an in-memory graph snapshot.
43
+ *
44
+ * The CLI/MCP layer constructs one instance per request from a freshly
45
+ * loaded snapshot. The query API never writes; only the indexer does.
46
+ */
47
+ export declare class GraphQueryApi {
48
+ private readonly snap;
49
+ private readonly fileByPath;
50
+ private readonly symbolByName;
51
+ private readonly outByFrom;
52
+ private readonly inByTo;
53
+ constructor(snap: IGraphSnapshot);
54
+ /** Load a snapshot from disk and construct the query API. */
55
+ static fromStore(projectRoot: string): GraphQueryApi;
56
+ status(): IGraphStatus;
57
+ findFile(path: string): INode | undefined;
58
+ /** Iterate every file node in the snapshot. Cheap; in-memory walk. */
59
+ allFiles(): IterableIterator<INode>;
60
+ /** Iterate every package node in the snapshot. */
61
+ allPackages(): IterableIterator<INode>;
62
+ findSymbol(name: string, opts?: IFindSymbolOptions): readonly INode[];
63
+ /** Files that import `nodeId` (directly). */
64
+ importersOf(nodeId: string): readonly INode[];
65
+ /** Files imported by `nodeId` (directly). Resolves only File-kind targets. */
66
+ importsFrom(nodeId: string): readonly INode[];
67
+ /** Symbols declared by a given file. */
68
+ symbolsIn(fileNodeId: string): readonly INode[];
69
+ /** Files that *call* the given symbol (Wave 3 — file-level precision). */
70
+ callersOf(symbolNodeId: string): readonly INode[];
71
+ /** Files that *reference* the given symbol (any use, including calls). */
72
+ referencesOf(symbolNodeId: string): readonly INode[];
73
+ /** Packages that this package depends on (PackageDependsOn). */
74
+ packageDeps(packageName: string): readonly INode[];
75
+ /**
76
+ * Every import cycle in the snapshot (SCC ≥ 2). Recomputes from the
77
+ * in-memory snapshot — file paths are filled in from the snapshot's
78
+ * file nodes. Sorted by size descending then id ascending so callers
79
+ * get a stable "worst first" ordering. Roadmap §3.1 long-promised
80
+ * this method on the query API; backed by `findFileCycles` so the
81
+ * indexer's manifest counts stay consistent with what callers see.
82
+ */
83
+ cycles(): readonly IFileCycle[];
84
+ /** 1-hop neighbours of a node, both in + out. */
85
+ neighbours(nodeId: string): IGraphNeighbours | undefined;
86
+ }
87
+ //# sourceMappingURL=query-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-api.d.ts","sourceRoot":"","sources":["../../src/query/query-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,232 @@
1
+ import { EdgeKind } from "../schema/edge-kind.js";
2
+ import { NodeKind } from "../schema/node-kind.js";
3
+ import { GraphStore } from "../store/graph-store.js";
4
+ import { findFileCycles } from "./cycle-detection.js";
5
+ /**
6
+ * Read-only query layer over an in-memory graph snapshot.
7
+ *
8
+ * The CLI/MCP layer constructs one instance per request from a freshly
9
+ * loaded snapshot. The query API never writes; only the indexer does.
10
+ */
11
+ export class GraphQueryApi {
12
+ snap;
13
+ fileByPath;
14
+ symbolByName;
15
+ outByFrom;
16
+ inByTo;
17
+ constructor(snap) {
18
+ this.snap = snap;
19
+ const fileByPath = new Map();
20
+ const symbolByName = new Map();
21
+ for (const n of snap.nodes.values()) {
22
+ if (n.kind === NodeKind.File && n.path) {
23
+ fileByPath.set(n.path, n);
24
+ }
25
+ else if (n.kind === NodeKind.Symbol) {
26
+ const arr = symbolByName.get(n.label);
27
+ if (arr)
28
+ arr.push(n);
29
+ else
30
+ symbolByName.set(n.label, [n]);
31
+ }
32
+ }
33
+ const outByFrom = new Map();
34
+ const inByTo = new Map();
35
+ for (const e of snap.edges.values()) {
36
+ const ofrom = outByFrom.get(e.from);
37
+ if (ofrom)
38
+ ofrom.push(e);
39
+ else
40
+ outByFrom.set(e.from, [e]);
41
+ const ito = inByTo.get(e.to);
42
+ if (ito)
43
+ ito.push(e);
44
+ else
45
+ inByTo.set(e.to, [e]);
46
+ }
47
+ this.fileByPath = fileByPath;
48
+ this.symbolByName = symbolByName;
49
+ this.outByFrom = outByFrom;
50
+ this.inByTo = inByTo;
51
+ }
52
+ /** Load a snapshot from disk and construct the query API. */
53
+ static fromStore(projectRoot) {
54
+ const store = new GraphStore(projectRoot);
55
+ return new GraphQueryApi(store.loadSnapshot());
56
+ }
57
+ status() {
58
+ const m = this.snap.manifest;
59
+ return {
60
+ exists: true,
61
+ state: 'fresh',
62
+ fileCount: m.filesIndexed,
63
+ nodeCount: this.snap.nodes.size,
64
+ edgeCount: this.snap.edges.size,
65
+ lastIndexedAt: m.lastIndexedAt,
66
+ };
67
+ }
68
+ findFile(path) {
69
+ return this.fileByPath.get(path);
70
+ }
71
+ /** Iterate every file node in the snapshot. Cheap; in-memory walk. */
72
+ *allFiles() {
73
+ for (const n of this.fileByPath.values())
74
+ yield n;
75
+ }
76
+ /** Iterate every package node in the snapshot. */
77
+ *allPackages() {
78
+ for (const n of this.snap.nodes.values()) {
79
+ if (n.kind === NodeKind.Package)
80
+ yield n;
81
+ }
82
+ }
83
+ findSymbol(name, opts = {}) {
84
+ const limit = opts.limit ?? 50;
85
+ if (opts.exact !== false) {
86
+ const exact = this.symbolByName.get(name) ?? [];
87
+ return filterByPackage(exact, opts.package).slice(0, limit);
88
+ }
89
+ const lower = name.toLowerCase();
90
+ const out = [];
91
+ for (const [k, list] of this.symbolByName) {
92
+ if (!k.toLowerCase().includes(lower))
93
+ continue;
94
+ for (const n of list)
95
+ out.push(n);
96
+ if (out.length >= limit * 2)
97
+ break;
98
+ }
99
+ return filterByPackage(out, opts.package).slice(0, limit);
100
+ }
101
+ /** Files that import `nodeId` (directly). */
102
+ importersOf(nodeId) {
103
+ const edges = this.inByTo.get(nodeId) ?? [];
104
+ const out = [];
105
+ for (const e of edges) {
106
+ if (e.kind !== EdgeKind.ImportsFile)
107
+ continue;
108
+ const n = this.snap.nodes.get(e.from);
109
+ if (n)
110
+ out.push(n);
111
+ }
112
+ return out;
113
+ }
114
+ /** Files imported by `nodeId` (directly). Resolves only File-kind targets. */
115
+ importsFrom(nodeId) {
116
+ const edges = this.outByFrom.get(nodeId) ?? [];
117
+ const out = [];
118
+ for (const e of edges) {
119
+ if (e.kind !== EdgeKind.ImportsFile)
120
+ continue;
121
+ const n = this.snap.nodes.get(e.to);
122
+ if (n)
123
+ out.push(n);
124
+ }
125
+ return out;
126
+ }
127
+ /** Symbols declared by a given file. */
128
+ symbolsIn(fileNodeId) {
129
+ const edges = this.outByFrom.get(fileNodeId) ?? [];
130
+ const out = [];
131
+ for (const e of edges) {
132
+ if (e.kind !== EdgeKind.DeclaresSymbol)
133
+ continue;
134
+ const n = this.snap.nodes.get(e.to);
135
+ if (n)
136
+ out.push(n);
137
+ }
138
+ return out;
139
+ }
140
+ /** Files that *call* the given symbol (Wave 3 — file-level precision). */
141
+ callersOf(symbolNodeId) {
142
+ const edges = this.inByTo.get(symbolNodeId) ?? [];
143
+ const out = [];
144
+ for (const e of edges) {
145
+ if (e.kind !== EdgeKind.CallsSymbol)
146
+ continue;
147
+ const n = this.snap.nodes.get(e.from);
148
+ if (n)
149
+ out.push(n);
150
+ }
151
+ return out;
152
+ }
153
+ /** Files that *reference* the given symbol (any use, including calls). */
154
+ referencesOf(symbolNodeId) {
155
+ const edges = this.inByTo.get(symbolNodeId) ?? [];
156
+ const out = [];
157
+ const seen = new Set();
158
+ for (const e of edges) {
159
+ if (e.kind !== EdgeKind.ReferencesSymbol && e.kind !== EdgeKind.CallsSymbol)
160
+ continue;
161
+ if (seen.has(e.from))
162
+ continue;
163
+ seen.add(e.from);
164
+ const n = this.snap.nodes.get(e.from);
165
+ if (n)
166
+ out.push(n);
167
+ }
168
+ return out;
169
+ }
170
+ /** Packages that this package depends on (PackageDependsOn). */
171
+ packageDeps(packageName) {
172
+ const edges = this.outByFrom.get(`package:${packageName}`) ?? [];
173
+ const out = [];
174
+ for (const e of edges) {
175
+ if (e.kind !== EdgeKind.PackageDependsOn)
176
+ continue;
177
+ const n = this.snap.nodes.get(e.to);
178
+ if (n)
179
+ out.push(n);
180
+ }
181
+ return out;
182
+ }
183
+ /**
184
+ * Every import cycle in the snapshot (SCC ≥ 2). Recomputes from the
185
+ * in-memory snapshot — file paths are filled in from the snapshot's
186
+ * file nodes. Sorted by size descending then id ascending so callers
187
+ * get a stable "worst first" ordering. Roadmap §3.1 long-promised
188
+ * this method on the query API; backed by `findFileCycles` so the
189
+ * indexer's manifest counts stay consistent with what callers see.
190
+ */
191
+ cycles() {
192
+ const pathById = new Map();
193
+ for (const [id, n] of this.snap.nodes) {
194
+ if (n.kind === NodeKind.File && n.path)
195
+ pathById.set(id, n.path);
196
+ }
197
+ return findFileCycles([...this.snap.nodes.values()], [...this.snap.edges.values()], pathById);
198
+ }
199
+ /** 1-hop neighbours of a node, both in + out. */
200
+ neighbours(nodeId) {
201
+ const node = this.snap.nodes.get(nodeId);
202
+ if (!node)
203
+ return undefined;
204
+ const outEdges = this.outByFrom.get(nodeId) ?? [];
205
+ const inEdges = this.inByTo.get(nodeId) ?? [];
206
+ return {
207
+ node,
208
+ out: outEdges.map((edge) => {
209
+ const target = this.snap.nodes.get(edge.to);
210
+ return { edge, target: target ?? { id: edge.to, resolved: false } };
211
+ }),
212
+ in: inEdges.map((edge) => {
213
+ const source = this.snap.nodes.get(edge.from);
214
+ return { edge, source: source ?? { id: edge.from, resolved: false } };
215
+ }),
216
+ };
217
+ }
218
+ }
219
+ function filterByPackage(list, pkg) {
220
+ if (!pkg)
221
+ return [...list];
222
+ const prefix = pkg + '/';
223
+ return list.filter((n) => {
224
+ if (!n.path)
225
+ return false;
226
+ // Workspace packages live at `packages/<slug>/...`. Without a path-
227
+ // alias map at query time, the cheap proxy is "path starts with the
228
+ // package directory prefix". Caller passes the directory, not the
229
+ // package name, for now.
230
+ return n.path === pkg || n.path.startsWith(prefix);
231
+ });
232
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Edge kinds in the code graph.
3
+ *
4
+ * MVP populates the "Code structure" group. Symbol-reference edges are
5
+ * reserved for Wave 3; bridge edges are reserved for Wave 2.
6
+ */
7
+ export declare enum EdgeKind {
8
+ ImportsFile = "imports-file",
9
+ DeclaresSymbol = "declares-symbol",
10
+ ReExportsSymbol = "re-exports-symbol",
11
+ BelongsToPackage = "belongs-to-package",
12
+ PackageDependsOn = "package-depends-on",
13
+ ReferencesSymbol = "references-symbol",
14
+ CallsSymbol = "calls-symbol",
15
+ ExtendsSymbol = "extends-symbol",
16
+ ImplementsSymbol = "implements-symbol",
17
+ AppliesRule = "applies-rule",
18
+ ViolatesBoundary = "violates-boundary",
19
+ MatchesPath = "matches-path",
20
+ CoveredByTemplate = "covered-by-template",
21
+ CoveredByPipeline = "covered-by-pipeline",
22
+ ContributedByPack = "contributed-by-pack",
23
+ ContainsKnowledge = "contains-knowledge",
24
+ /** File declares a framework entity (e.g. controller/component/module). */
25
+ FrameworkDeclares = "framework-declares",
26
+ /** Controller / route handler → HTTP route. */
27
+ HandlesRoute = "handles-route",
28
+ /** Component → hook it uses. */
29
+ UsesHook = "uses-hook"
30
+ }
31
+ //# sourceMappingURL=edge-kind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-kind.d.ts","sourceRoot":"","sources":["../../src/schema/edge-kind.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,oBAAY,QAAQ;IAElB,WAAW,iBAAiB;IAC5B,cAAc,oBAAoB;IAClC,eAAe,sBAAsB;IACrC,gBAAgB,uBAAuB;IACvC,gBAAgB,uBAAuB;IAGvC,gBAAgB,sBAAsB;IACtC,WAAW,iBAAiB;IAC5B,aAAa,mBAAmB;IAChC,gBAAgB,sBAAsB;IAGtC,WAAW,iBAAiB;IAC5B,gBAAgB,sBAAsB;IACtC,WAAW,iBAAiB;IAC5B,iBAAiB,wBAAwB;IACzC,iBAAiB,wBAAwB;IACzC,iBAAiB,wBAAwB;IACzC,iBAAiB,uBAAuB;IAGxC,2EAA2E;IAC3E,iBAAiB,uBAAuB;IACxC,+CAA+C;IAC/C,YAAY,kBAAkB;IAC9B,gCAAgC;IAChC,QAAQ,cAAc;CACvB"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Edge kinds in the code graph.
3
+ *
4
+ * MVP populates the "Code structure" group. Symbol-reference edges are
5
+ * reserved for Wave 3; bridge edges are reserved for Wave 2.
6
+ */
7
+ export var EdgeKind;
8
+ (function (EdgeKind) {
9
+ // ── Code structure (MVP) ─────────────────────────────────────────────
10
+ EdgeKind["ImportsFile"] = "imports-file";
11
+ EdgeKind["DeclaresSymbol"] = "declares-symbol";
12
+ EdgeKind["ReExportsSymbol"] = "re-exports-symbol";
13
+ EdgeKind["BelongsToPackage"] = "belongs-to-package";
14
+ EdgeKind["PackageDependsOn"] = "package-depends-on";
15
+ // ── Symbol references (reserved, Wave 3) ─────────────────────────────
16
+ EdgeKind["ReferencesSymbol"] = "references-symbol";
17
+ EdgeKind["CallsSymbol"] = "calls-symbol";
18
+ EdgeKind["ExtendsSymbol"] = "extends-symbol";
19
+ EdgeKind["ImplementsSymbol"] = "implements-symbol";
20
+ // ── Bridge to assets (reserved, Wave 2) ──────────────────────────────
21
+ EdgeKind["AppliesRule"] = "applies-rule";
22
+ EdgeKind["ViolatesBoundary"] = "violates-boundary";
23
+ EdgeKind["MatchesPath"] = "matches-path";
24
+ EdgeKind["CoveredByTemplate"] = "covered-by-template";
25
+ EdgeKind["CoveredByPipeline"] = "covered-by-pipeline";
26
+ EdgeKind["ContributedByPack"] = "contributed-by-pack";
27
+ EdgeKind["ContainsKnowledge"] = "contains-knowledge";
28
+ // ── Framework-aware edges (Wave 7) ───────────────────────────────────
29
+ /** File declares a framework entity (e.g. controller/component/module). */
30
+ EdgeKind["FrameworkDeclares"] = "framework-declares";
31
+ /** Controller / route handler → HTTP route. */
32
+ EdgeKind["HandlesRoute"] = "handles-route";
33
+ /** Component → hook it uses. */
34
+ EdgeKind["UsesHook"] = "uses-hook";
35
+ })(EdgeKind || (EdgeKind = {}));
@@ -0,0 +1,22 @@
1
+ import type { EdgeKind } from './edge-kind.js';
2
+ /**
3
+ * An edge in the code graph.
4
+ *
5
+ * `id` is deterministic: sha1(from || '|' || to || '|' || kind). Two
6
+ * extractors emitting the same logical edge collapse to one row in the
7
+ * store.
8
+ *
9
+ * `source` records which extractor created the edge, with version. Used
10
+ * to invalidate edges when an extractor's behaviour changes.
11
+ */
12
+ export interface IEdge {
13
+ id: string;
14
+ from: string;
15
+ to: string;
16
+ kind: EdgeKind;
17
+ /** e.g. 'extract-ts-file@v1'. */
18
+ source: string;
19
+ /** Kind-specific structured payload. */
20
+ data?: Readonly<Record<string, unknown>>;
21
+ }
22
+ //# sourceMappingURL=edge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge.d.ts","sourceRoot":"","sources":["../../src/schema/edge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1C"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Per-file fingerprint used for incremental indexing.
3
+ *
4
+ * MVP uses mtime + sha1(content). Falling back to sha1-only is fine when
5
+ * mtime is unreliable (e.g., docker bind mounts on Linux). The store keeps
6
+ * both so the comparison is cheap.
7
+ */
8
+ export interface IFileFingerprint {
9
+ /** Project-relative path. */
10
+ path: string;
11
+ /** ms since epoch. */
12
+ mtime: number;
13
+ /** SHA-1 of file contents, hex. */
14
+ sha1: string;
15
+ /** File size in bytes. */
16
+ sizeBytes: number;
17
+ /** Resolved language tag, e.g. 'typescript' | 'javascript'. */
18
+ language: string;
19
+ /** Node id this file owns in the graph. */
20
+ nodeId: string;
21
+ }
22
+ //# sourceMappingURL=file-fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-fingerprint.d.ts","sourceRoot":"","sources":["../../src/schema/file-fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { IEdge } from './edge.js';
2
+ import type { IFileFingerprint } from './file-fingerprint.js';
3
+ import type { IGraphManifest } from './manifest.js';
4
+ import type { INode } from './node.js';
5
+ /**
6
+ * In-memory snapshot of the graph — the unit the query API operates on.
7
+ *
8
+ * `nodes` and `edges` are flat maps keyed by id. The store loads JSONL
9
+ * into this shape; the indexer builds it before writing. Callers should
10
+ * treat it as immutable after construction.
11
+ */
12
+ export interface IGraphSnapshot {
13
+ manifest: IGraphManifest;
14
+ nodes: ReadonlyMap<string, INode>;
15
+ edges: ReadonlyMap<string, IEdge>;
16
+ files: ReadonlyMap<string, IFileFingerprint>;
17
+ }
18
+ //# sourceMappingURL=graph-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-snapshot.d.ts","sourceRoot":"","sources":["../../src/schema/graph-snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAClC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAClC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC9C"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import type { GraphSchemaVersion } from './schema-version.js';
2
+ /**
3
+ * `.sharkcraft/graph/meta.json` shape.
4
+ *
5
+ * `digest` is SHA-256 over the concatenated JSONL files (alphabetical),
6
+ * computed at write time. A stale digest signals a tampered or partial
7
+ * store and must trigger a rebuild rather than silent use.
8
+ */
9
+ export interface IGraphManifest {
10
+ schema: GraphSchemaVersion;
11
+ projectRoot: string;
12
+ lastIndexedAt: string;
13
+ /** Wall-clock duration of the last full index, in ms. */
14
+ lastIndexDurationMs: number;
15
+ filesIndexed: number;
16
+ nodesByKind: Readonly<Record<string, number>>;
17
+ edgesByKind: Readonly<Record<string, number>>;
18
+ /** SHA-256 of all JSONL files. */
19
+ digest: string;
20
+ /** Workspace packages discovered at index time. */
21
+ workspacePackages: readonly string[];
22
+ /**
23
+ * Count of strongly-connected components of size ≥ 2 in the
24
+ * `imports-file` subgraph, computed at index time. Optional for
25
+ * forward-compat with manifests written before this field existed
26
+ * (added 2026-05). Roadmap §3.1 promised cycle-aware queries; this
27
+ * is the persisted counter the doctor + dashboard read from.
28
+ */
29
+ cycleCount?: number;
30
+ /** Size of the largest SCC of size ≥ 2 (0 when no cycles). */
31
+ largestCycleSize?: number;
32
+ /** Total file nodes participating in any cycle. */
33
+ filesInCycles?: number;
34
+ /**
35
+ * Number of `imports-file` edges that resolved to the
36
+ * `unresolved:<spec>` sentinel (relative / alias / workspace path
37
+ * the resolver could not match against an on-disk file). Optional
38
+ * for forward-compat. The first ten distinct specifiers are kept
39
+ * in `unresolvedImportSamples` for doctor + dashboard.
40
+ */
41
+ unresolvedImportCount?: number;
42
+ /** Distinct files with at least one unresolved import. */
43
+ filesWithUnresolvedImports?: number;
44
+ /** First N distinct unresolved specifiers (sample for human/agent). */
45
+ unresolvedImportSamples?: readonly string[];
46
+ }
47
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/schema/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,0DAA0D;IAC1D,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,uEAAuE;IACvE,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C"}
@@ -0,0 +1 @@
1
+ export {};