@mrciphersmith/keryx 0.2.49 → 0.2.51
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/README.md +13 -0
- package/dist/cli.js +2495 -451
- package/package.json +1 -1
- package/src/gdgraph/enrich.ts +24 -7
- package/src/gdgraph/treesitter/adapter.test.ts +145 -2
- package/src/gdgraph/treesitter/adapter.ts +85 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.51",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
package/src/gdgraph/enrich.ts
CHANGED
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import {
|
|
15
|
-
resolveCapability,
|
|
16
15
|
runCapabilityOrFallback,
|
|
17
16
|
type CapabilityAdapter,
|
|
18
17
|
type CapabilitySpec,
|
|
19
18
|
} from "../capability/seam";
|
|
20
19
|
import { loadGdgraphConfig } from "./config";
|
|
21
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createTreesitterSpec,
|
|
22
|
+
resolveTreesitterCapability,
|
|
23
|
+
type BuildInput,
|
|
24
|
+
type FileRecord,
|
|
25
|
+
} from "./treesitter/adapter";
|
|
22
26
|
import type { SymbolLayer } from "./types";
|
|
23
27
|
|
|
24
28
|
export interface EnrichResult {
|
|
@@ -37,18 +41,31 @@ export type CapabilityResolver = (
|
|
|
37
41
|
export async function enrichBuildWithSymbols(
|
|
38
42
|
cwd: string,
|
|
39
43
|
files: FileRecord[],
|
|
40
|
-
|
|
44
|
+
// `undefined` (the default) selects the T6 literal-import fast path via
|
|
45
|
+
// `resolveTreesitterCapability`; tests pass an explicit `CapabilityResolver`
|
|
46
|
+
// (e.g. a mock adapter, or the raw seam) to bypass it.
|
|
47
|
+
resolve?: CapabilityResolver,
|
|
41
48
|
): Promise<EnrichResult> {
|
|
42
49
|
const config = await loadGdgraphConfig(cwd);
|
|
43
|
-
const
|
|
50
|
+
const treesitterConfig = {
|
|
44
51
|
languages: config.treesitter.languages,
|
|
45
52
|
grammarsPath: config.treesitter.grammarsPath,
|
|
46
|
-
}
|
|
53
|
+
};
|
|
47
54
|
|
|
48
55
|
// Gate 1 (manifest-enabled) + dep + grammar + isAvailable. `null` ⇒ degrade
|
|
49
56
|
// with NO symbol files written; the seam emits the single warn-once on an
|
|
50
|
-
// enabled-but-unavailable ceiling.
|
|
51
|
-
|
|
57
|
+
// enabled-but-unavailable ceiling. The default resolver additionally tries a
|
|
58
|
+
// literal `await import("web-tree-sitter")` fast path first (T6) so a
|
|
59
|
+
// `bun build --compile` binary can genuinely parse instead of always
|
|
60
|
+
// degrading — see `resolveTreesitterCapability`.
|
|
61
|
+
//
|
|
62
|
+
// `createTreesitterSpec` is only built here when an injected `resolve` is
|
|
63
|
+
// supplied (tests): `resolveTreesitterCapability` builds its own equivalent
|
|
64
|
+
// spec internally from `treesitterConfig`, so building one unconditionally
|
|
65
|
+
// on the default path would be dead work on every real `gdgraph build` call.
|
|
66
|
+
const adapter = resolve
|
|
67
|
+
? await resolve(cwd, createTreesitterSpec(cwd, treesitterConfig))
|
|
68
|
+
: await resolveTreesitterCapability(cwd, treesitterConfig);
|
|
52
69
|
if (!adapter) {
|
|
53
70
|
return { enriched: false, symbols: 0, calls: 0 };
|
|
54
71
|
}
|
|
@@ -2,12 +2,40 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { expect, test } from "bun:test";
|
|
6
|
-
import { createTreesitterSpec, type BuildInput } from "./adapter";
|
|
5
|
+
import { beforeEach, expect, mock, test } from "bun:test";
|
|
6
|
+
import { createTreesitterSpec, resolveTreesitterCapability, type BuildInput } from "./adapter";
|
|
7
7
|
import type { TsNode } from "./extract";
|
|
8
8
|
import { enrichBuildWithSymbols } from "../enrich";
|
|
9
|
+
import { hasWarned, resetWarnOnce } from "../../capability/warn-once";
|
|
10
|
+
import { setTreesitterEnabled } from "../symbols-capability";
|
|
9
11
|
import type { CallEdge, SymbolLayer, SymbolNode } from "../types";
|
|
10
12
|
|
|
13
|
+
// Registered at MODULE TOP LEVEL, before any `test()` body runs: Bun loads
|
|
14
|
+
// every test file's static imports/module body in a resolution pass before
|
|
15
|
+
// running any test's body, so a `mock.module` call placed here reliably wins
|
|
16
|
+
// the specifier UNTIL some test anywhere in the same `bun test` invocation
|
|
17
|
+
// performs a real `await import("web-tree-sitter")` first (e.g.
|
|
18
|
+
// `fallback.test.ts`'s AC4.3 test does exactly that with the capability
|
|
19
|
+
// enabled) — once that happens, Bun's module cache is warmed with the real
|
|
20
|
+
// module and this mock's factory is no longer invoked for later imports in
|
|
21
|
+
// the same process. That makes `webTreeSitterImportAttempts` a reliable
|
|
22
|
+
// "definitely zero attempts" signal (nothing increments it unless OUR mock's
|
|
23
|
+
// factory ran) but NOT a reliable "definitely attempted" signal when run
|
|
24
|
+
// alongside the rest of the suite — the enabled-path test below therefore
|
|
25
|
+
// proves "gate 1 passed and we reached the real isAvailable() probe" via the
|
|
26
|
+
// process-scoped warn-once tracker instead, which is correct regardless of
|
|
27
|
+
// which physical module served the import.
|
|
28
|
+
let webTreeSitterImportAttempts = 0;
|
|
29
|
+
mock.module("web-tree-sitter", () => {
|
|
30
|
+
webTreeSitterImportAttempts += 1;
|
|
31
|
+
function MockParser(this: unknown): void {}
|
|
32
|
+
(MockParser as unknown as { init: () => Promise<void> }).init = async () => {};
|
|
33
|
+
(MockParser as unknown as { Language: { load: (p: string) => Promise<unknown> } }).Language = {
|
|
34
|
+
load: async () => ({}),
|
|
35
|
+
};
|
|
36
|
+
return { default: MockParser };
|
|
37
|
+
});
|
|
38
|
+
|
|
11
39
|
// --- tiny structural mock tree: one top-level function `boot` that calls `tick` ---
|
|
12
40
|
function mk(o: {
|
|
13
41
|
type: string;
|
|
@@ -247,3 +275,118 @@ test("Python grammar resolution — grammarForFile selects python for .py files"
|
|
|
247
275
|
await rm(root, { recursive: true, force: true });
|
|
248
276
|
}
|
|
249
277
|
});
|
|
278
|
+
|
|
279
|
+
// --- Real default (no-injected-resolver) production path (T6 review fix) ---
|
|
280
|
+
//
|
|
281
|
+
// Every test above injects an explicit `CapabilityResolver`/`dep`, bypassing
|
|
282
|
+
// `resolveTreesitterCapability` entirely. The REAL call site
|
|
283
|
+
// (`build.ts` → `enrichBuildWithSymbols(projectRoot, fileRecords)`, no
|
|
284
|
+
// resolver argument) goes through `resolveTreesitterCapability`'s literal
|
|
285
|
+
// `await import("web-tree-sitter")` fast path with NO injected resolver at
|
|
286
|
+
// all. These tests exercise that exact default path and, via the
|
|
287
|
+
// module-top-level `mock.module` above, directly observe whether the literal
|
|
288
|
+
// import was attempted — proving gate 1 (manifest-enabled) runs BEFORE the
|
|
289
|
+
// import, not after (the bug this ordering fixes).
|
|
290
|
+
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
webTreeSitterImportAttempts = 0;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
async function makeDisabledOrAbsentWorkspace(enabled: "absent" | false): Promise<string> {
|
|
296
|
+
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-default-"));
|
|
297
|
+
if (enabled === false) {
|
|
298
|
+
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
299
|
+
const manifest = setTreesitterEnabled({}, false);
|
|
300
|
+
await writeFile(
|
|
301
|
+
path.join(root, ".metaproject", "metaproject.json"),
|
|
302
|
+
JSON.stringify(manifest),
|
|
303
|
+
"utf8",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
// enabled === "absent" ⇒ no .metaproject/metaproject.json at all (missing
|
|
307
|
+
// manifest = off, per seam.ts's own contract).
|
|
308
|
+
return root;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
test("default path, capability DISABLED (missing manifest) — resolveTreesitterCapability never attempts the web-tree-sitter import", async () => {
|
|
312
|
+
const root = await makeDisabledOrAbsentWorkspace("absent");
|
|
313
|
+
try {
|
|
314
|
+
const adapter = await resolveTreesitterCapability(root, {
|
|
315
|
+
languages: ["typescript"],
|
|
316
|
+
grammarsPath: null,
|
|
317
|
+
});
|
|
318
|
+
expect(adapter).toBeNull();
|
|
319
|
+
// The core assertion (Fix 1): a disabled/missing-manifest ceiling must
|
|
320
|
+
// resolve to null WITHOUT ever attempting the literal dep import.
|
|
321
|
+
expect(webTreeSitterImportAttempts).toBe(0);
|
|
322
|
+
} finally {
|
|
323
|
+
await rm(root, { recursive: true, force: true });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("default path, capability DISABLED (enabled: false) — resolveTreesitterCapability never attempts the web-tree-sitter import", async () => {
|
|
328
|
+
const root = await makeDisabledOrAbsentWorkspace(false);
|
|
329
|
+
try {
|
|
330
|
+
const adapter = await resolveTreesitterCapability(root, {
|
|
331
|
+
languages: ["typescript"],
|
|
332
|
+
grammarsPath: null,
|
|
333
|
+
});
|
|
334
|
+
expect(adapter).toBeNull();
|
|
335
|
+
expect(webTreeSitterImportAttempts).toBe(0);
|
|
336
|
+
} finally {
|
|
337
|
+
await rm(root, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("default path, capability DISABLED — enrichBuildWithSymbols (no injected resolver) degrades cleanly with no writes and no dep-load attempt", async () => {
|
|
342
|
+
const root = await makeDisabledOrAbsentWorkspace("absent");
|
|
343
|
+
try {
|
|
344
|
+
// No third argument ⇒ the real production default path (build.ts calls
|
|
345
|
+
// enrichBuildWithSymbols with exactly two arguments).
|
|
346
|
+
const result = await enrichBuildWithSymbols(root, [{ path: "src/x.ts", content: "" }]);
|
|
347
|
+
expect(result).toEqual({ enriched: false, symbols: 0, calls: 0 });
|
|
348
|
+
expect(webTreeSitterImportAttempts).toBe(0);
|
|
349
|
+
|
|
350
|
+
const storage = path.join(root, ".metaproject", "data", "gdgraph", "storage");
|
|
351
|
+
await expect(readFile(path.join(storage, "symbols.jsonl"), "utf8")).rejects.toThrow();
|
|
352
|
+
} finally {
|
|
353
|
+
await rm(root, { recursive: true, force: true });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("default path, capability ENABLED — resolveTreesitterCapability gets past gate 1 and reaches the real isAvailable() probe", async () => {
|
|
358
|
+
resetWarnOnce();
|
|
359
|
+
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-default-enabled-"));
|
|
360
|
+
try {
|
|
361
|
+
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
362
|
+
const manifest = setTreesitterEnabled({}, true);
|
|
363
|
+
await writeFile(
|
|
364
|
+
path.join(root, ".metaproject", "metaproject.json"),
|
|
365
|
+
JSON.stringify(manifest),
|
|
366
|
+
"utf8",
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// No grammarsPath and no lockfile entries ⇒ resolveGrammars deterministically
|
|
370
|
+
// resolves zero grammars ⇒ isAvailable() is false, so this proves the
|
|
371
|
+
// manifest gate short-circuits correctly on the enabled side too, without
|
|
372
|
+
// requiring a real compiled binary or a real WASM grammar asset.
|
|
373
|
+
const adapter = await resolveTreesitterCapability(root, {
|
|
374
|
+
languages: ["typescript"],
|
|
375
|
+
grammarsPath: null,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// No grammar resolves ⇒ isAvailable() is false ⇒ degrades to null, same
|
|
379
|
+
// AC0-8 catch-and-degrade contract as the seam.
|
|
380
|
+
expect(adapter).toBeNull();
|
|
381
|
+
// Gate 1 passed (unlike the disabled cases above) and execution reached
|
|
382
|
+
// the real `isAvailable()` probe, which reported unavailable and warned
|
|
383
|
+
// once — the process-scoped warn-once tracker is a reliable,
|
|
384
|
+
// ordering-independent witness of this (unlike counting the mock
|
|
385
|
+
// factory's own invocations, which only fires when THIS file's mock wins
|
|
386
|
+
// the module-cache race against any real `web-tree-sitter` import
|
|
387
|
+
// elsewhere in a full-suite run — see the top-of-file comment).
|
|
388
|
+
expect(hasWarned("gdgraph.treesitter")).toBe(true);
|
|
389
|
+
} finally {
|
|
390
|
+
await rm(root, { recursive: true, force: true });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// It NEVER throws out (C0-11): every parse error is caught and the file is
|
|
11
11
|
// skipped, so an opt-in ceiling can never break the deterministic seam.
|
|
12
12
|
|
|
13
|
-
import type
|
|
13
|
+
import { isCapabilityEnabled, resolveCapability, type CapabilityAdapter, type CapabilitySpec } from "../../capability/seam";
|
|
14
|
+
import { warnCapabilityDegraded } from "../../capability/warn-once";
|
|
14
15
|
import type { CallEdge, SymbolLayer, SymbolNode } from "../types";
|
|
15
16
|
import { extractSymbolLayer, resolveCrossFileCalls, type TsNode } from "./extract";
|
|
16
17
|
import {
|
|
@@ -65,6 +66,89 @@ export function createTreesitterSpec(
|
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Compiled-binary fast path (T6, keryx-native-distribution).
|
|
70
|
+
//
|
|
71
|
+
// `bun build --compile` cannot statically trace `src/capability/seam.ts`'s
|
|
72
|
+
// generic `await import(spec.optionalDependency)` (a runtime string variable —
|
|
73
|
+
// oven-sh/bun#11732), so in a compiled binary gate 2 of `resolveCapability`
|
|
74
|
+
// always throws for THIS capability even when `web-tree-sitter` is genuinely
|
|
75
|
+
// bundled in. A LITERAL `await import("web-tree-sitter")` DOES bundle and work
|
|
76
|
+
// in a compiled binary (verified empirically this flow).
|
|
77
|
+
//
|
|
78
|
+
// This function tries the literal import first. When it succeeds, it drives
|
|
79
|
+
// the SAME gates the seam applies (1: manifest-enabled, 4: adapter build +
|
|
80
|
+
// isAvailable, with the identical warn-once-on-degrade contract) — it only
|
|
81
|
+
// replaces the ONE line that loads the dependency itself. When the literal
|
|
82
|
+
// import throws (dependency genuinely not installed, e.g. a minimal npm
|
|
83
|
+
// install without optional deps, or `bun run` dev mode where the package is
|
|
84
|
+
// simply missing from node_modules), it falls through UNCHANGED to
|
|
85
|
+
// `resolveCapability(cwd, spec)` — today's exact dev-mode behavior, including
|
|
86
|
+
// the seam's own variable-based `await import(spec.optionalDependency)`
|
|
87
|
+
// attempt and its warn-once messaging. `seam.ts` itself is never modified.
|
|
88
|
+
async function loadTreesitterDepLiteral(): Promise<unknown | undefined> {
|
|
89
|
+
try {
|
|
90
|
+
return await import("web-tree-sitter");
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function resolveTreesitterCapability(
|
|
97
|
+
cwd: string,
|
|
98
|
+
config: TreesitterAdapterConfig,
|
|
99
|
+
resolve: (
|
|
100
|
+
cwd: string,
|
|
101
|
+
spec: CapabilitySpec<BuildInput, SymbolLayer>,
|
|
102
|
+
) => Promise<CapabilityAdapter<BuildInput, SymbolLayer> | null> = resolveCapability,
|
|
103
|
+
): Promise<CapabilityAdapter<BuildInput, SymbolLayer> | null> {
|
|
104
|
+
const spec = createTreesitterSpec(cwd, config);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Gate 1 FIRST — identical check the seam performs, no dep load, no
|
|
108
|
+
// warning when disabled (the normal default path per `seam.ts`'s own
|
|
109
|
+
// contract: "Disabled ⇒ null with NO dep load, NO asset touch, and NO
|
|
110
|
+
// warning"). This MUST run before the literal `import("web-tree-sitter")`
|
|
111
|
+
// below: with the check after, every `gdgraph build` call paid the
|
|
112
|
+
// literal-import cost even with the capability disabled — the bug this
|
|
113
|
+
// ordering fixes.
|
|
114
|
+
if (!(await isCapabilityEnabled(cwd, spec.id))) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const literalDep = await loadTreesitterDepLiteral();
|
|
119
|
+
if (literalDep === undefined) {
|
|
120
|
+
// Literal import failed (dependency not actually installed) — defer to
|
|
121
|
+
// the seam's normal variable-based gate 2, unchanged. The seam
|
|
122
|
+
// re-checks gate 1, which is cheap and idempotent.
|
|
123
|
+
return await resolve(cwd, spec);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Gate 2 replaced: the dependency is already loaded via the literal
|
|
127
|
+
// import above, so it is supplied directly instead of re-resolving
|
|
128
|
+
// `spec.optionalDependency` through the seam.
|
|
129
|
+
const adapter = spec.load({ dep: literalDep, asset: null });
|
|
130
|
+
|
|
131
|
+
// Gate 4: `isAvailable()` — same catch-and-degrade contract as the seam
|
|
132
|
+
// (AC0-8): a probe that throws degrades to the deterministic fallback.
|
|
133
|
+
let available: boolean;
|
|
134
|
+
try {
|
|
135
|
+
available = await adapter.isAvailable();
|
|
136
|
+
} catch {
|
|
137
|
+
warnCapabilityDegraded(spec.id, "adapter availability check threw");
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (!available) {
|
|
141
|
+
warnCapabilityDegraded(spec.id, "adapter reported unavailable");
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return adapter;
|
|
146
|
+
} catch {
|
|
147
|
+
// Absolute backstop, mirrors the seam's own contract: never throw out.
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
68
152
|
class TreesitterAdapter implements CapabilityAdapter<BuildInput, SymbolLayer> {
|
|
69
153
|
readonly id = "gdgraph.treesitter";
|
|
70
154
|
private grammars: ResolvedGrammar[] = [];
|