@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.
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/indexer/detect-workspace.d.ts +18 -0
- package/dist/indexer/detect-workspace.d.ts.map +1 -0
- package/dist/indexer/detect-workspace.js +80 -0
- package/dist/indexer/extract-csharp-file.d.ts +27 -0
- package/dist/indexer/extract-csharp-file.d.ts.map +1 -0
- package/dist/indexer/extract-csharp-file.js +163 -0
- package/dist/indexer/extract-dart-file.d.ts +28 -0
- package/dist/indexer/extract-dart-file.d.ts.map +1 -0
- package/dist/indexer/extract-dart-file.js +167 -0
- package/dist/indexer/extract-elixir-file.d.ts +27 -0
- package/dist/indexer/extract-elixir-file.d.ts.map +1 -0
- package/dist/indexer/extract-elixir-file.js +164 -0
- package/dist/indexer/extract-go-file.d.ts +28 -0
- package/dist/indexer/extract-go-file.d.ts.map +1 -0
- package/dist/indexer/extract-go-file.js +156 -0
- package/dist/indexer/extract-java-file.d.ts +25 -0
- package/dist/indexer/extract-java-file.d.ts.map +1 -0
- package/dist/indexer/extract-java-file.js +140 -0
- package/dist/indexer/extract-kotlin-file.d.ts +20 -0
- package/dist/indexer/extract-kotlin-file.d.ts.map +1 -0
- package/dist/indexer/extract-kotlin-file.js +158 -0
- package/dist/indexer/extract-php-file.d.ts +26 -0
- package/dist/indexer/extract-php-file.d.ts.map +1 -0
- package/dist/indexer/extract-php-file.js +161 -0
- package/dist/indexer/extract-python-file.d.ts +30 -0
- package/dist/indexer/extract-python-file.d.ts.map +1 -0
- package/dist/indexer/extract-python-file.js +196 -0
- package/dist/indexer/extract-ruby-file.d.ts +29 -0
- package/dist/indexer/extract-ruby-file.d.ts.map +1 -0
- package/dist/indexer/extract-ruby-file.js +151 -0
- package/dist/indexer/extract-rust-file.d.ts +27 -0
- package/dist/indexer/extract-rust-file.d.ts.map +1 -0
- package/dist/indexer/extract-rust-file.js +186 -0
- package/dist/indexer/extract-swift-file.d.ts +27 -0
- package/dist/indexer/extract-swift-file.d.ts.map +1 -0
- package/dist/indexer/extract-swift-file.js +168 -0
- package/dist/indexer/extract-ts-file.d.ts +79 -0
- package/dist/indexer/extract-ts-file.d.ts.map +1 -0
- package/dist/indexer/extract-ts-file.js +403 -0
- package/dist/indexer/incremental-updater.d.ts +41 -0
- package/dist/indexer/incremental-updater.d.ts.map +1 -0
- package/dist/indexer/incremental-updater.js +395 -0
- package/dist/indexer/index-builder.d.ts +23 -0
- package/dist/indexer/index-builder.d.ts.map +1 -0
- package/dist/indexer/index-builder.js +289 -0
- package/dist/indexer/resolve-imports.d.ts +36 -0
- package/dist/indexer/resolve-imports.d.ts.map +1 -0
- package/dist/indexer/resolve-imports.js +144 -0
- package/dist/indexer/unresolved-imports.d.ts +20 -0
- package/dist/indexer/unresolved-imports.d.ts.map +1 -0
- package/dist/indexer/unresolved-imports.js +32 -0
- package/dist/query/cycle-detection.d.ts +40 -0
- package/dist/query/cycle-detection.d.ts.map +1 -0
- package/dist/query/cycle-detection.js +135 -0
- package/dist/query/query-api.d.ts +87 -0
- package/dist/query/query-api.d.ts.map +1 -0
- package/dist/query/query-api.js +232 -0
- package/dist/schema/edge-kind.d.ts +31 -0
- package/dist/schema/edge-kind.d.ts.map +1 -0
- package/dist/schema/edge-kind.js +35 -0
- package/dist/schema/edge.d.ts +22 -0
- package/dist/schema/edge.d.ts.map +1 -0
- package/dist/schema/edge.js +1 -0
- package/dist/schema/file-fingerprint.d.ts +22 -0
- package/dist/schema/file-fingerprint.d.ts.map +1 -0
- package/dist/schema/file-fingerprint.js +1 -0
- package/dist/schema/graph-snapshot.d.ts +18 -0
- package/dist/schema/graph-snapshot.d.ts.map +1 -0
- package/dist/schema/graph-snapshot.js +1 -0
- package/dist/schema/manifest.d.ts +47 -0
- package/dist/schema/manifest.d.ts.map +1 -0
- package/dist/schema/manifest.js +1 -0
- package/dist/schema/node-kind.d.ts +21 -0
- package/dist/schema/node-kind.d.ts.map +1 -0
- package/dist/schema/node-kind.js +27 -0
- package/dist/schema/node.d.ts +26 -0
- package/dist/schema/node.d.ts.map +1 -0
- package/dist/schema/node.js +1 -0
- package/dist/schema/schema-version.d.ts +10 -0
- package/dist/schema/schema-version.d.ts.map +1 -0
- package/dist/schema/schema-version.js +8 -0
- package/dist/store/file-fingerprint.d.ts +8 -0
- package/dist/store/file-fingerprint.d.ts.map +1 -0
- package/dist/store/file-fingerprint.js +64 -0
- package/dist/store/graph-store.d.ts +48 -0
- package/dist/store/graph-store.d.ts.map +1 -0
- package/dist/store/graph-store.js +194 -0
- package/package.json +54 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node kinds in the code graph.
|
|
3
|
+
*
|
|
4
|
+
* MVP ships File / Symbol / Package. Bridge kinds are reserved but emitted
|
|
5
|
+
* by the rule-graph package (Wave 2), not by graph itself.
|
|
6
|
+
*/
|
|
7
|
+
export declare enum NodeKind {
|
|
8
|
+
File = "file",
|
|
9
|
+
Symbol = "symbol",
|
|
10
|
+
Package = "package",
|
|
11
|
+
Rule = "rule",
|
|
12
|
+
Path = "path",
|
|
13
|
+
Template = "template",
|
|
14
|
+
Pipeline = "pipeline",
|
|
15
|
+
Preset = "preset",
|
|
16
|
+
Pack = "pack",
|
|
17
|
+
Boundary = "boundary",
|
|
18
|
+
Knowledge = "knowledge",
|
|
19
|
+
FrameworkEntity = "framework-entity"
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=node-kind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-kind.d.ts","sourceRoot":"","sources":["../../src/schema/node-kind.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,oBAAY,QAAQ;IAClB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,OAAO,YAAY;IAInB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,QAAQ,aAAa;IACrB,QAAQ,aAAa;IACrB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,QAAQ,aAAa;IACrB,SAAS,cAAc;IAMvB,eAAe,qBAAqB;CACrC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node kinds in the code graph.
|
|
3
|
+
*
|
|
4
|
+
* MVP ships File / Symbol / Package. Bridge kinds are reserved but emitted
|
|
5
|
+
* by the rule-graph package (Wave 2), not by graph itself.
|
|
6
|
+
*/
|
|
7
|
+
export var NodeKind;
|
|
8
|
+
(function (NodeKind) {
|
|
9
|
+
NodeKind["File"] = "file";
|
|
10
|
+
NodeKind["Symbol"] = "symbol";
|
|
11
|
+
NodeKind["Package"] = "package";
|
|
12
|
+
// Reserved for @shrkcrft/rule-graph (Wave 2). Listed here so node ids stay
|
|
13
|
+
// globally namespaced and the union surface is stable.
|
|
14
|
+
NodeKind["Rule"] = "rule";
|
|
15
|
+
NodeKind["Path"] = "path";
|
|
16
|
+
NodeKind["Template"] = "template";
|
|
17
|
+
NodeKind["Pipeline"] = "pipeline";
|
|
18
|
+
NodeKind["Preset"] = "preset";
|
|
19
|
+
NodeKind["Pack"] = "pack";
|
|
20
|
+
NodeKind["Boundary"] = "boundary";
|
|
21
|
+
NodeKind["Knowledge"] = "knowledge";
|
|
22
|
+
// Framework-aware entities (Wave 7). The `data.framework` and
|
|
23
|
+
// `data.subtype` fields distinguish nestjs:controller vs
|
|
24
|
+
// react:component vs express:route. Single kind keeps the NodeKind
|
|
25
|
+
// enum tight; consumers filter on data.
|
|
26
|
+
NodeKind["FrameworkEntity"] = "framework-entity";
|
|
27
|
+
})(NodeKind || (NodeKind = {}));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NodeKind } from './node-kind.js';
|
|
2
|
+
/**
|
|
3
|
+
* A node in the code graph.
|
|
4
|
+
*
|
|
5
|
+
* `id` is namespaced by kind: `file:packages/foo/src/bar.ts`,
|
|
6
|
+
* `symbol:packages/foo/src/bar.ts#fooFn`, `package:@shrkcrft/foo`.
|
|
7
|
+
*
|
|
8
|
+
* `data` holds kind-specific structured payload — typed at write via
|
|
9
|
+
* the helper builders in `indexer/` rather than enforced via a
|
|
10
|
+
* discriminated union here (the union would balloon and gain little).
|
|
11
|
+
*/
|
|
12
|
+
export interface INode {
|
|
13
|
+
id: string;
|
|
14
|
+
kind: NodeKind;
|
|
15
|
+
/** Short, deterministic display label. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Project-relative path for File / Symbol; undefined otherwise. */
|
|
18
|
+
path?: string;
|
|
19
|
+
/** 1-based line number for Symbol nodes; undefined otherwise. */
|
|
20
|
+
line?: number;
|
|
21
|
+
/** Free-form, sorted, deduped tags. */
|
|
22
|
+
tags?: readonly string[];
|
|
23
|
+
/** Kind-specific structured payload. */
|
|
24
|
+
data?: Readonly<Record<string, unknown>>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/schema/node.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,QAAQ,CAAC;IACf,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,wCAAwC;IACxC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema version for the on-disk code graph.
|
|
3
|
+
*
|
|
4
|
+
* Every payload written to `.sharkcraft/graph/` and every JSON response
|
|
5
|
+
* carries this constant. A change here is a breaking change and requires
|
|
6
|
+
* a migration in the store.
|
|
7
|
+
*/
|
|
8
|
+
export declare const GRAPH_SCHEMA: "sharkcraft.graph/v1";
|
|
9
|
+
export type GraphSchemaVersion = typeof GRAPH_SCHEMA;
|
|
10
|
+
//# sourceMappingURL=schema-version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-version.d.ts","sourceRoot":"","sources":["../../src/schema/schema-version.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAG,qBAA8B,CAAC;AAE3D,MAAM,MAAM,kBAAkB,GAAG,OAAO,YAAY,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema version for the on-disk code graph.
|
|
3
|
+
*
|
|
4
|
+
* Every payload written to `.sharkcraft/graph/` and every JSON response
|
|
5
|
+
* carries this constant. A change here is a breaking change and requires
|
|
6
|
+
* a migration in the store.
|
|
7
|
+
*/
|
|
8
|
+
export const GRAPH_SCHEMA = 'sharkcraft.graph/v1';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IFileFingerprint } from '../schema/file-fingerprint.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compute a fingerprint for a file. Reads the file once; callers that
|
|
4
|
+
* already have the contents should use `fingerprintFromContent` instead.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fingerprintFile(absPath: string, projectRoot: string): IFileFingerprint;
|
|
7
|
+
export declare function fingerprintFromBuffer(buf: Buffer, absPath: string, projectRoot: string, mtimeMs: number, sizeBytes: number): IFileFingerprint;
|
|
8
|
+
//# sourceMappingURL=file-fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-fingerprint.d.ts","sourceRoot":"","sources":["../../src/store/file-fingerprint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA2BtE;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,gBAAgB,CAIlB;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,gBAAgB,CAWlB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import * as nodePath from 'node:path';
|
|
4
|
+
const TS_EXTS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
|
5
|
+
const JS_EXTS = new Set(['.js', '.jsx', '.mjs', '.cjs']);
|
|
6
|
+
function languageOf(absPath) {
|
|
7
|
+
const ext = nodePath.extname(absPath).toLowerCase();
|
|
8
|
+
if (TS_EXTS.has(ext))
|
|
9
|
+
return 'typescript';
|
|
10
|
+
if (JS_EXTS.has(ext))
|
|
11
|
+
return 'javascript';
|
|
12
|
+
if (ext === '.vue')
|
|
13
|
+
return 'vue';
|
|
14
|
+
if (ext === '.svelte')
|
|
15
|
+
return 'svelte';
|
|
16
|
+
if (ext === '.astro')
|
|
17
|
+
return 'astro';
|
|
18
|
+
if (ext === '.py')
|
|
19
|
+
return 'python';
|
|
20
|
+
if (ext === '.go')
|
|
21
|
+
return 'go';
|
|
22
|
+
if (ext === '.java')
|
|
23
|
+
return 'java';
|
|
24
|
+
if (ext === '.rs')
|
|
25
|
+
return 'rust';
|
|
26
|
+
if (ext === '.kt' || ext === '.kts')
|
|
27
|
+
return 'kotlin';
|
|
28
|
+
if (ext === '.rb')
|
|
29
|
+
return 'ruby';
|
|
30
|
+
if (ext === '.cs' || ext === '.csx')
|
|
31
|
+
return 'csharp';
|
|
32
|
+
if (ext === '.ex' || ext === '.exs')
|
|
33
|
+
return 'elixir';
|
|
34
|
+
if (ext === '.php')
|
|
35
|
+
return 'php';
|
|
36
|
+
if (ext === '.dart')
|
|
37
|
+
return 'dart';
|
|
38
|
+
if (ext === '.swift')
|
|
39
|
+
return 'swift';
|
|
40
|
+
if (ext === '.graphql' || ext === '.gql')
|
|
41
|
+
return 'graphql';
|
|
42
|
+
return 'unknown';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute a fingerprint for a file. Reads the file once; callers that
|
|
46
|
+
* already have the contents should use `fingerprintFromContent` instead.
|
|
47
|
+
*/
|
|
48
|
+
export function fingerprintFile(absPath, projectRoot) {
|
|
49
|
+
const st = statSync(absPath);
|
|
50
|
+
const buf = readFileSync(absPath);
|
|
51
|
+
return fingerprintFromBuffer(buf, absPath, projectRoot, st.mtimeMs, st.size);
|
|
52
|
+
}
|
|
53
|
+
export function fingerprintFromBuffer(buf, absPath, projectRoot, mtimeMs, sizeBytes) {
|
|
54
|
+
const sha1 = createHash('sha1').update(buf).digest('hex');
|
|
55
|
+
const rel = nodePath.relative(projectRoot, absPath);
|
|
56
|
+
return {
|
|
57
|
+
path: rel.split(nodePath.sep).join('/'),
|
|
58
|
+
mtime: Math.floor(mtimeMs),
|
|
59
|
+
sha1,
|
|
60
|
+
sizeBytes,
|
|
61
|
+
language: languageOf(absPath),
|
|
62
|
+
nodeId: `file:${rel.split(nodePath.sep).join('/')}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IEdge } from '../schema/edge.js';
|
|
2
|
+
import type { IFileFingerprint } from '../schema/file-fingerprint.js';
|
|
3
|
+
import type { IGraphManifest } from '../schema/manifest.js';
|
|
4
|
+
import type { INode } from '../schema/node.js';
|
|
5
|
+
import type { IGraphSnapshot } from '../schema/graph-snapshot.js';
|
|
6
|
+
/**
|
|
7
|
+
* On-disk JSONL graph store under `<root>/.sharkcraft/graph/`.
|
|
8
|
+
*
|
|
9
|
+
* Layout (MVP):
|
|
10
|
+
* meta.json manifest, schema version
|
|
11
|
+
* files.json { path: IFileFingerprint }
|
|
12
|
+
* nodes/<kind>.jsonl one row per node, sorted by id
|
|
13
|
+
* edges/<kind>.jsonl one row per edge, sorted by id
|
|
14
|
+
*
|
|
15
|
+
* The store is the only writer; callers go through `writeSnapshot` for a
|
|
16
|
+
* full rewrite or `appendNodes` / `appendEdges` for surgical updates (R64).
|
|
17
|
+
*
|
|
18
|
+
* `loadSnapshot` reads everything into memory. For SharkCraft-sized
|
|
19
|
+
* repos that's a few MB at most; SQLite swap is not on the MVP path
|
|
20
|
+
* (see code-intelligence.md §6.3 upgrade triggers).
|
|
21
|
+
*/
|
|
22
|
+
export declare class GraphStore {
|
|
23
|
+
private readonly projectRoot;
|
|
24
|
+
readonly storeDir: string;
|
|
25
|
+
constructor(projectRoot: string);
|
|
26
|
+
exists(): boolean;
|
|
27
|
+
clear(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Write a full snapshot. Overwrites any existing store. Manifest digest
|
|
30
|
+
* is computed from the resulting JSONL files.
|
|
31
|
+
*/
|
|
32
|
+
writeSnapshot(nodes: readonly INode[], edges: readonly IEdge[], files: readonly IFileFingerprint[], partial: Omit<IGraphManifest, 'schema' | 'digest'>): IGraphManifest;
|
|
33
|
+
/**
|
|
34
|
+
* Load the full store into memory. Throws if the store is missing or
|
|
35
|
+
* malformed — callers should check `exists()` first or wrap in a try.
|
|
36
|
+
*/
|
|
37
|
+
loadSnapshot(): IGraphSnapshot;
|
|
38
|
+
/**
|
|
39
|
+
* Recompute the digest from disk and verify it matches the manifest.
|
|
40
|
+
* Used by `shrk graph status` and as a sanity check before queries.
|
|
41
|
+
*/
|
|
42
|
+
verifyDigest(): {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
expected: string;
|
|
45
|
+
actual: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=graph-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-store.d.ts","sourceRoot":"","sources":["../../src/store/graph-store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAOlE;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,UAAU;IAGT,OAAO,CAAC,QAAQ,CAAC,WAAW;IAFxC,SAAgB,QAAQ,EAAE,MAAM,CAAC;gBAEJ,WAAW,EAAE,MAAM;IAIhD,MAAM,IAAI,OAAO;IAIjB,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,aAAa,CACX,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAClC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,QAAQ,CAAC,GACjD,cAAc;IA6CjB;;;OAGG;IACH,YAAY,IAAI,cAAc;IAwC9B;;;OAGG;IACH,YAAY,IAAI;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;CAMlE"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import * as nodePath from 'node:path';
|
|
4
|
+
import { GRAPH_SCHEMA } from "../schema/schema-version.js";
|
|
5
|
+
const NODES_DIR = 'nodes';
|
|
6
|
+
const EDGES_DIR = 'edges';
|
|
7
|
+
const META_FILE = 'meta.json';
|
|
8
|
+
const FILES_FILE = 'files.json';
|
|
9
|
+
/**
|
|
10
|
+
* On-disk JSONL graph store under `<root>/.sharkcraft/graph/`.
|
|
11
|
+
*
|
|
12
|
+
* Layout (MVP):
|
|
13
|
+
* meta.json manifest, schema version
|
|
14
|
+
* files.json { path: IFileFingerprint }
|
|
15
|
+
* nodes/<kind>.jsonl one row per node, sorted by id
|
|
16
|
+
* edges/<kind>.jsonl one row per edge, sorted by id
|
|
17
|
+
*
|
|
18
|
+
* The store is the only writer; callers go through `writeSnapshot` for a
|
|
19
|
+
* full rewrite or `appendNodes` / `appendEdges` for surgical updates (R64).
|
|
20
|
+
*
|
|
21
|
+
* `loadSnapshot` reads everything into memory. For SharkCraft-sized
|
|
22
|
+
* repos that's a few MB at most; SQLite swap is not on the MVP path
|
|
23
|
+
* (see code-intelligence.md §6.3 upgrade triggers).
|
|
24
|
+
*/
|
|
25
|
+
export class GraphStore {
|
|
26
|
+
projectRoot;
|
|
27
|
+
storeDir;
|
|
28
|
+
constructor(projectRoot) {
|
|
29
|
+
this.projectRoot = projectRoot;
|
|
30
|
+
this.storeDir = nodePath.join(projectRoot, '.sharkcraft', 'graph');
|
|
31
|
+
}
|
|
32
|
+
exists() {
|
|
33
|
+
return existsSync(nodePath.join(this.storeDir, META_FILE));
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
if (existsSync(this.storeDir)) {
|
|
37
|
+
rmSync(this.storeDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Write a full snapshot. Overwrites any existing store. Manifest digest
|
|
42
|
+
* is computed from the resulting JSONL files.
|
|
43
|
+
*/
|
|
44
|
+
writeSnapshot(nodes, edges, files, partial) {
|
|
45
|
+
// Clear stale per-kind JSONL files so kinds that became empty
|
|
46
|
+
// (e.g. PackageDependsOn after a delete) don't survive across
|
|
47
|
+
// writes. This is the simplest correctness guarantee — the store
|
|
48
|
+
// is small enough that the cost is negligible.
|
|
49
|
+
const nodesDir = nodePath.join(this.storeDir, NODES_DIR);
|
|
50
|
+
const edgesDir = nodePath.join(this.storeDir, EDGES_DIR);
|
|
51
|
+
if (existsSync(nodesDir))
|
|
52
|
+
rmSync(nodesDir, { recursive: true, force: true });
|
|
53
|
+
if (existsSync(edgesDir))
|
|
54
|
+
rmSync(edgesDir, { recursive: true, force: true });
|
|
55
|
+
mkdirSync(nodesDir, { recursive: true });
|
|
56
|
+
mkdirSync(edgesDir, { recursive: true });
|
|
57
|
+
const nodesByKind = bucketBy(nodes, (n) => n.kind);
|
|
58
|
+
const edgesByKind = bucketBy(edges, (e) => e.kind);
|
|
59
|
+
const nodeCounts = {};
|
|
60
|
+
const edgeCounts = {};
|
|
61
|
+
for (const [kind, list] of Object.entries(nodesByKind)) {
|
|
62
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
63
|
+
writeJsonl(nodePath.join(this.storeDir, NODES_DIR, `${kind}.jsonl`), list);
|
|
64
|
+
nodeCounts[kind] = list.length;
|
|
65
|
+
}
|
|
66
|
+
for (const [kind, list] of Object.entries(edgesByKind)) {
|
|
67
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
68
|
+
writeJsonl(nodePath.join(this.storeDir, EDGES_DIR, `${kind}.jsonl`), list);
|
|
69
|
+
edgeCounts[kind] = list.length;
|
|
70
|
+
}
|
|
71
|
+
writeJson(nodePath.join(this.storeDir, FILES_FILE), Object.fromEntries([...files].sort((a, b) => a.path.localeCompare(b.path)).map((f) => [f.path, f])));
|
|
72
|
+
const digest = computeStoreDigest(this.storeDir);
|
|
73
|
+
const manifest = {
|
|
74
|
+
schema: GRAPH_SCHEMA,
|
|
75
|
+
digest,
|
|
76
|
+
...partial,
|
|
77
|
+
nodesByKind: nodeCounts,
|
|
78
|
+
edgesByKind: edgeCounts,
|
|
79
|
+
};
|
|
80
|
+
writeJson(nodePath.join(this.storeDir, META_FILE), manifest);
|
|
81
|
+
return manifest;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Load the full store into memory. Throws if the store is missing or
|
|
85
|
+
* malformed — callers should check `exists()` first or wrap in a try.
|
|
86
|
+
*/
|
|
87
|
+
loadSnapshot() {
|
|
88
|
+
if (!this.exists()) {
|
|
89
|
+
throw new Error(`code-graph store not found under ${this.storeDir}. Run 'shrk graph index' to build it.`);
|
|
90
|
+
}
|
|
91
|
+
const manifest = readJson(nodePath.join(this.storeDir, META_FILE));
|
|
92
|
+
if (manifest.schema !== GRAPH_SCHEMA) {
|
|
93
|
+
throw new Error(`code-graph schema mismatch: store=${manifest.schema}, expected=${GRAPH_SCHEMA}. Rebuild with 'shrk graph index'.`);
|
|
94
|
+
}
|
|
95
|
+
const filesRaw = readJson(nodePath.join(this.storeDir, FILES_FILE));
|
|
96
|
+
const files = new Map(Object.entries(filesRaw));
|
|
97
|
+
const nodes = new Map();
|
|
98
|
+
const nodesDir = nodePath.join(this.storeDir, NODES_DIR);
|
|
99
|
+
if (existsSync(nodesDir)) {
|
|
100
|
+
for (const fname of readdirSync(nodesDir)) {
|
|
101
|
+
if (!fname.endsWith('.jsonl'))
|
|
102
|
+
continue;
|
|
103
|
+
for (const row of readJsonl(nodePath.join(nodesDir, fname))) {
|
|
104
|
+
nodes.set(row.id, row);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const edges = new Map();
|
|
109
|
+
const edgesDir = nodePath.join(this.storeDir, EDGES_DIR);
|
|
110
|
+
if (existsSync(edgesDir)) {
|
|
111
|
+
for (const fname of readdirSync(edgesDir)) {
|
|
112
|
+
if (!fname.endsWith('.jsonl'))
|
|
113
|
+
continue;
|
|
114
|
+
for (const row of readJsonl(nodePath.join(edgesDir, fname))) {
|
|
115
|
+
edges.set(row.id, row);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { manifest, nodes, edges, files };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Recompute the digest from disk and verify it matches the manifest.
|
|
123
|
+
* Used by `shrk graph status` and as a sanity check before queries.
|
|
124
|
+
*/
|
|
125
|
+
verifyDigest() {
|
|
126
|
+
if (!this.exists())
|
|
127
|
+
return { ok: false, expected: '', actual: '' };
|
|
128
|
+
const manifest = readJson(nodePath.join(this.storeDir, META_FILE));
|
|
129
|
+
const actual = computeStoreDigest(this.storeDir);
|
|
130
|
+
return { ok: actual === manifest.digest, expected: manifest.digest, actual };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function bucketBy(list, key) {
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const item of list) {
|
|
136
|
+
const k = key(item);
|
|
137
|
+
if (!out[k])
|
|
138
|
+
out[k] = [];
|
|
139
|
+
out[k].push(item);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function writeJsonl(path, rows) {
|
|
144
|
+
const body = rows.map((r) => JSON.stringify(r)).join('\n');
|
|
145
|
+
writeFileSync(path, body.length > 0 ? body + '\n' : '');
|
|
146
|
+
}
|
|
147
|
+
function readJsonl(path) {
|
|
148
|
+
const raw = readFileSync(path, 'utf8');
|
|
149
|
+
if (!raw)
|
|
150
|
+
return [];
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const line of raw.split('\n')) {
|
|
153
|
+
const trimmed = line.trim();
|
|
154
|
+
if (!trimmed)
|
|
155
|
+
continue;
|
|
156
|
+
out.push(JSON.parse(trimmed));
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function writeJson(path, value) {
|
|
161
|
+
writeFileSync(path, JSON.stringify(value, null, 2));
|
|
162
|
+
}
|
|
163
|
+
function readJson(path) {
|
|
164
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* SHA-256 over the concatenation of all JSONL files (alphabetical) plus
|
|
168
|
+
* files.json. meta.json itself is excluded — it holds the digest.
|
|
169
|
+
*/
|
|
170
|
+
function computeStoreDigest(storeDir) {
|
|
171
|
+
const hash = createHash('sha256');
|
|
172
|
+
const targets = [];
|
|
173
|
+
const filesJson = nodePath.join(storeDir, FILES_FILE);
|
|
174
|
+
if (existsSync(filesJson))
|
|
175
|
+
targets.push(filesJson);
|
|
176
|
+
for (const sub of [NODES_DIR, EDGES_DIR]) {
|
|
177
|
+
const dir = nodePath.join(storeDir, sub);
|
|
178
|
+
if (!existsSync(dir))
|
|
179
|
+
continue;
|
|
180
|
+
for (const fname of readdirSync(dir).sort()) {
|
|
181
|
+
if (!fname.endsWith('.jsonl'))
|
|
182
|
+
continue;
|
|
183
|
+
targets.push(nodePath.join(dir, fname));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
targets.sort();
|
|
187
|
+
for (const t of targets) {
|
|
188
|
+
hash.update(nodePath.relative(storeDir, t));
|
|
189
|
+
hash.update('\0');
|
|
190
|
+
hash.update(readFileSync(t));
|
|
191
|
+
hash.update('\0');
|
|
192
|
+
}
|
|
193
|
+
return hash.digest('hex');
|
|
194
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shrkcrft/graph",
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
4
|
+
"description": "SharkCraft code intelligence graph: files, symbols, packages, imports and exports. Persistent JSONL store; on-disk index queried by every other code-intelligence package.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SharkCraft contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/sharkcraft/sharkcraft.git",
|
|
25
|
+
"directory": "packages/graph"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/sharkcraft/sharkcraft",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/sharkcraft/sharkcraft/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sharkcraft",
|
|
33
|
+
"graph",
|
|
34
|
+
"code-intelligence",
|
|
35
|
+
"import-graph",
|
|
36
|
+
"symbol-graph",
|
|
37
|
+
"monorepo"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"bun": ">=1.1.0",
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@shrkcrft/core": "^0.1.0-alpha.10",
|
|
48
|
+
"@shrkcrft/boundaries": "^0.1.0-alpha.10",
|
|
49
|
+
"@shrkcrft/inspector": "^0.1.0-alpha.10"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|