@mrciphersmith/keryx 0.2.82 → 0.2.84

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 (48) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +28857 -19477
  3. package/dist/core.js +25751 -0
  4. package/package.json +15 -2
  5. package/src/gdgraph/dangling.ts +204 -0
  6. package/src/gdgraph/find.ts +529 -37
  7. package/src/gdgraph/repomap.ts +140 -12
  8. package/src/gdgraph/staleness.ts +22 -9
  9. package/src/gdgraph/symbol.ts +45 -6
  10. package/src/gdgraph/treesitter/extract.ts +153 -5
  11. package/src/gdgraph/wiki-layer.ts +32 -1
  12. package/src/gdskills/bundled/skills/core/reviewer-skill-creator/SKILL.md +29 -0
  13. package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.codex.md +1 -1
  14. package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.cursor.md +1 -1
  15. package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.md +1 -1
  16. package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.opencode.md +1 -1
  17. package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.zed.md +1 -1
  18. package/src/gdskills/bundled/skills/orchestration/flow-orchestrator/SKILL.md +34 -6
  19. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.codex.md +1 -1
  20. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.cursor.md +1 -1
  21. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +1 -1
  22. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.opencode.md +1 -1
  23. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.zed.md +1 -1
  24. package/src/gdskills/bundled/skills/quality/security-audit/SKILL.codex.md +52 -5
  25. package/src/gdskills/bundled/skills/quality/security-audit/SKILL.cursor.md +52 -5
  26. package/src/gdskills/bundled/skills/quality/security-audit/SKILL.md +52 -5
  27. package/src/gdgraph/affected.test.ts +0 -133
  28. package/src/gdgraph/build-integrity.test.ts +0 -193
  29. package/src/gdgraph/build-lang.test.ts +0 -406
  30. package/src/gdgraph/build.test.ts +0 -120
  31. package/src/gdgraph/config.test.ts +0 -47
  32. package/src/gdgraph/core-sources.test.ts +0 -99
  33. package/src/gdgraph/fallback.test.ts +0 -153
  34. package/src/gdgraph/find.test.ts +0 -78
  35. package/src/gdgraph/import-kind.test.ts +0 -525
  36. package/src/gdgraph/path.test.ts +0 -56
  37. package/src/gdgraph/repomap.test.ts +0 -110
  38. package/src/gdgraph/service.test.ts +0 -89
  39. package/src/gdgraph/staleness.test.ts +0 -208
  40. package/src/gdgraph/symbol.test.ts +0 -89
  41. package/src/gdgraph/symbols-capability.test.ts +0 -138
  42. package/src/gdgraph/treesitter/adapter.test.ts +0 -496
  43. package/src/gdgraph/treesitter/extract.test.ts +0 -278
  44. package/src/gdgraph/treesitter/no-treesitter-import.test.ts +0 -51
  45. package/src/gdgraph/treesitter/real-grammar-fixture.test.ts +0 -71
  46. package/src/gdgraph/treesitter/resolve-calls.test.ts +0 -38
  47. package/src/gdgraph/wiki-layer-no-git.test.ts +0 -73
  48. package/src/gdgraph/wiki-layer.test.ts +0 -211
@@ -1,496 +0,0 @@
1
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
- import { createHash } from "node:crypto";
3
- import { tmpdir } from "node:os";
4
- import path from "node:path";
5
- import { beforeEach, expect, mock, test } from "bun:test";
6
- import { createTreesitterSpec, resolveTreesitterCapability, type BuildInput } from "./adapter";
7
- import type { TsNode } from "./extract";
8
- import { enrichBuildWithSymbols } from "../enrich";
9
- import { hasWarned, resetWarnOnce } from "../../capability/warn-once";
10
- import { setTreesitterEnabled } from "../symbols-capability";
11
- import type { CallEdge, SymbolLayer, SymbolNode } from "../types";
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
-
39
- // --- tiny structural mock tree: one top-level function `boot` that calls `tick` ---
40
- function mk(o: {
41
- type: string;
42
- line?: number;
43
- endLine?: number;
44
- text?: string;
45
- fields?: Record<string, TsNode | null>;
46
- namedChildren?: TsNode[];
47
- }): TsNode {
48
- const line = o.line ?? 1;
49
- const named = o.namedChildren ?? [];
50
- return {
51
- type: o.type,
52
- text: o.text ?? "",
53
- startPosition: { row: line - 1, column: 0 },
54
- endPosition: { row: (o.endLine ?? line) - 1, column: 0 },
55
- childForFieldName: (field: string) => o.fields?.[field] ?? null,
56
- namedChildren: named,
57
- children: named,
58
- };
59
- }
60
-
61
- function bootTree(): TsNode {
62
- const id = (t: string) => mk({ type: "identifier", text: t });
63
- const body = mk({
64
- type: "statement_block",
65
- line: 1,
66
- namedChildren: [mk({ type: "call_expression", line: 1, fields: { function: id("tick") }, text: "tick()" })],
67
- });
68
- const boot = mk({
69
- type: "function_declaration",
70
- line: 1,
71
- endLine: 2,
72
- fields: { name: id("boot"), parameters: mk({ type: "formal_parameters", text: "()" }) },
73
- namedChildren: [body],
74
- });
75
- const tick = mk({
76
- type: "function_declaration",
77
- line: 3,
78
- endLine: 3,
79
- fields: { name: id("tick"), parameters: mk({ type: "formal_parameters", text: "()" }) },
80
- });
81
- return mk({ type: "program", line: 1, endLine: 3, namedChildren: [boot, tick] });
82
- }
83
-
84
- // A structural mock of the `web-tree-sitter` module (a Parser constructor with
85
- // static `init` + `Language.load`). Parse always returns the bootTree.
86
- function mockParserModule(): unknown {
87
- function MockParser(this: unknown) {}
88
- (MockParser as unknown as { init: () => Promise<void> }).init = async () => {};
89
- (MockParser as unknown as { Language: { load: (p: string) => Promise<unknown> } }).Language = {
90
- load: async () => ({}),
91
- };
92
- MockParser.prototype.setLanguage = function setLanguage(): void {};
93
- MockParser.prototype.parse = function parse(): { rootNode: TsNode } {
94
- return { rootNode: bootTree() };
95
- };
96
- return MockParser;
97
- }
98
-
99
- // Create a temp workspace whose lockfile pins a real on-disk grammar file so the
100
- // Asset Resolver verifies it (availability-true), with a config grammarsPath (T1).
101
- async function makeWorkspaceWithGrammar(): Promise<{ root: string; grammarsDir: string }> {
102
- const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-"));
103
- const grammarsDir = path.join(root, "grammars");
104
- await mkdir(path.join(root, ".metaproject"), { recursive: true });
105
- await mkdir(grammarsDir, { recursive: true });
106
- const wasmPath = path.join(grammarsDir, "tree-sitter-typescript.wasm");
107
- const bytes = Buffer.from("fake-grammar-bytes");
108
- await writeFile(wasmPath, bytes);
109
- const sha = createHash("sha256").update(bytes).digest("hex");
110
- await writeFile(
111
- path.join(root, ".metaproject", "assets.lock.json"),
112
- JSON.stringify({
113
- schemaVersion: 1,
114
- assets: {
115
- "tree-sitter-typescript": {
116
- version: "0.22.0",
117
- url: "https://example.dev/tree-sitter-typescript.wasm",
118
- sha256: sha,
119
- size: bytes.length,
120
- },
121
- },
122
- }),
123
- );
124
- return { root, grammarsDir };
125
- }
126
-
127
- test("AC5.2 availability-true — adapter isAvailable + run() yields the symbol layer", async () => {
128
- const { root, grammarsDir } = await makeWorkspaceWithGrammar();
129
- try {
130
- const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: grammarsDir });
131
- const adapter = spec.load({ dep: mockParserModule(), asset: null });
132
-
133
- expect(await adapter.isAvailable()).toBe(true);
134
-
135
- const layer = await adapter.run({ files: [{ path: "src/boot.ts", content: "ignored-by-mock" }] });
136
- const symbolIds = layer.symbols.map((symbol: SymbolNode) => symbol.id).sort();
137
- expect(symbolIds).toEqual(["src/boot.ts#boot", "src/boot.ts#tick"]);
138
- const callKinds = layer.calls.map((call: CallEdge) => `${call.kind}:${call.from}=>${call.to}`);
139
- expect(callKinds).toContain("calls:src/boot.ts#boot=>src/boot.ts#tick");
140
- } finally {
141
- await rm(root, { recursive: true, force: true });
142
- }
143
- });
144
-
145
- test("AC5.2 availability-false — no grammar ⇒ isAvailable false", async () => {
146
- const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-none-"));
147
- try {
148
- await mkdir(path.join(root, ".metaproject"), { recursive: true });
149
- const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: null });
150
- const adapter = spec.load({ dep: mockParserModule(), asset: null });
151
- expect(await adapter.isAvailable()).toBe(false);
152
- } finally {
153
- await rm(root, { recursive: true, force: true });
154
- }
155
- });
156
-
157
- test("availability-false — missing dep ⇒ isAvailable false", async () => {
158
- const { root, grammarsDir } = await makeWorkspaceWithGrammar();
159
- try {
160
- const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: grammarsDir });
161
- const adapter = spec.load({ dep: undefined, asset: null });
162
- expect(await adapter.isAvailable()).toBe(false);
163
- } finally {
164
- await rm(root, { recursive: true, force: true });
165
- }
166
- });
167
-
168
- // --- AFC-13 / AC5 requirement 3: "the grammar is not installed" and "the
169
- // grammar is installed but its ABI version does not match this runtime" must
170
- // be distinguishable outcomes, not one collapsed "unavailable". Neither
171
- // failure mode can be produced on a real machine without either uninstalling
172
- // `web-tree-sitter` (not available as a seam) or installing an ABI-mismatched
173
- // grammar build (forbidden by this task). Both are exercised instead through
174
- // the module's own seam: `spec.load({ dep, asset })` accepts an injected
175
- // `dep` exactly like the existing tests above, and a `Language.load` that
176
- // rejects is a faithful simulation of a real ABI/version-mismatch error from
177
- // `web-tree-sitter` (the real runtime rejects `Language.load` the same way
178
- // for a grammar built against an incompatible ABI).
179
- function brokenLanguageParserModule(rejection: Error): unknown {
180
- function MockParser(this: unknown) {}
181
- (MockParser as unknown as { init: () => Promise<void> }).init = async () => {};
182
- (MockParser as unknown as { Language: { load: (p: string) => Promise<unknown> } }).Language = {
183
- load: async () => {
184
- throw rejection;
185
- },
186
- };
187
- MockParser.prototype.setLanguage = function setLanguage(): void {};
188
- MockParser.prototype.parse = function parse(): { rootNode: TsNode } {
189
- return { rootNode: bootTree() };
190
- };
191
- return MockParser;
192
- }
193
-
194
- test("AC5.req3 — grammar asset never resolved ⇒ diagnosis status 'missing', not 'incompatible'", async () => {
195
- const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-diag-missing-"));
196
- try {
197
- await mkdir(path.join(root, ".metaproject"), { recursive: true });
198
- // No assets.lock.json entry at all ⇒ resolveGrammar can never find the asset.
199
- const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: null });
200
- const adapter = spec.load({ dep: mockParserModule(), asset: null }) as unknown as {
201
- isAvailable(): Promise<boolean>;
202
- getDiagnoses(): { language: string; status: string; reason: string }[];
203
- };
204
-
205
- expect(await adapter.isAvailable()).toBe(false);
206
- const diagnoses = adapter.getDiagnoses();
207
- expect(diagnoses).toHaveLength(1);
208
- expect(diagnoses[0]?.status).toBe("missing");
209
- expect(diagnoses[0]?.reason).not.toContain("ABI");
210
- expect(diagnoses[0]?.reason).toContain("not resolved");
211
- } finally {
212
- await rm(root, { recursive: true, force: true });
213
- }
214
- });
215
-
216
- test("AC5.req3 — grammar asset resolves and verifies, but the runtime refuses to load it ⇒ diagnosis status 'incompatible', not 'missing'", async () => {
217
- const { root, grammarsDir } = await makeWorkspaceWithGrammar();
218
- try {
219
- const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: grammarsDir });
220
- const abiError = new Error("Incompatible language version 13. Expected minimum 14, maximum 15");
221
- const adapter = spec.load({ dep: brokenLanguageParserModule(abiError), asset: null }) as unknown as {
222
- isAvailable(): Promise<boolean>;
223
- getDiagnoses(): { language: string; status: string; reason: string }[];
224
- };
225
-
226
- // The grammar resolved (it is on disk with a verified checksum) — this is
227
- // the crucial difference from the "missing" case above — but the runtime
228
- // could not load it, so overall availability is still false.
229
- expect(await adapter.isAvailable()).toBe(false);
230
- const diagnoses = adapter.getDiagnoses();
231
- expect(diagnoses).toHaveLength(1);
232
- expect(diagnoses[0]?.status).toBe("incompatible");
233
- expect(diagnoses[0]?.reason).toContain("Incompatible language version 13");
234
- expect(diagnoses[0]?.reason).not.toContain("not resolved");
235
- } finally {
236
- await rm(root, { recursive: true, force: true });
237
- }
238
- });
239
-
240
- test("AC5.req3 — missing and incompatible are genuinely different values end to end (adapter diagnosis -> warn reason -> requireSymbols code), never the same outcome", async () => {
241
- const missingRoot = await mkdtemp(path.join(tmpdir(), "keryx-ts-diag-a-"));
242
- const incompatibleWorkspace = await makeWorkspaceWithGrammar();
243
- try {
244
- await mkdir(path.join(missingRoot, ".metaproject"), { recursive: true });
245
- const missingSpec = createTreesitterSpec(missingRoot, { languages: ["typescript"], grammarsPath: null });
246
- const missingAdapter = missingSpec.load({ dep: mockParserModule(), asset: null }) as unknown as {
247
- isAvailable(): Promise<boolean>;
248
- getDiagnoses(): { language: string; status: string; reason: string }[];
249
- };
250
- await missingAdapter.isAvailable();
251
-
252
- const incompatibleSpec = createTreesitterSpec(incompatibleWorkspace.root, {
253
- languages: ["typescript"],
254
- grammarsPath: incompatibleWorkspace.grammarsDir,
255
- });
256
- const incompatibleAdapter = incompatibleSpec.load({
257
- dep: brokenLanguageParserModule(new Error("bad wasm magic number")),
258
- asset: null,
259
- }) as unknown as {
260
- isAvailable(): Promise<boolean>;
261
- getDiagnoses(): { language: string; status: string; reason: string }[];
262
- };
263
- await incompatibleAdapter.isAvailable();
264
-
265
- expect(missingAdapter.getDiagnoses()[0]?.status).not.toBe(incompatibleAdapter.getDiagnoses()[0]?.status);
266
- } finally {
267
- await rm(missingRoot, { recursive: true, force: true });
268
- await rm(incompatibleWorkspace.root, { recursive: true, force: true });
269
- }
270
- });
271
-
272
- test("AC1.1 additive write path — enrich writes symbols.jsonl + calls.jsonl via a mock adapter", async () => {
273
- const root = await mkdtemp(path.join(tmpdir(), "keryx-enrich-"));
274
- try {
275
- await mkdir(path.join(root, ".metaproject", "data", "gdgraph", "storage"), { recursive: true });
276
- const expectedLayer: SymbolLayer = {
277
- symbols: [
278
- {
279
- id: "src/x.ts#run",
280
- kind: "function",
281
- path: "src/x.ts",
282
- name: "run",
283
- container: null,
284
- startLine: 1,
285
- endLine: 1,
286
- language: "typescript",
287
- signature: "run()",
288
- },
289
- ],
290
- calls: [{ id: "defines:src/x.ts=>src/x.ts#run", from: "src/x.ts", to: "src/x.ts#run", kind: "defines", resolved: true }],
291
- };
292
-
293
- const injected = async () => ({
294
- id: "gdgraph.treesitter",
295
- isAvailable: async () => true,
296
- run: async (_input: BuildInput) => expectedLayer,
297
- });
298
-
299
- const result = await enrichBuildWithSymbols(root, [{ path: "src/x.ts", content: "" }], injected);
300
- expect(result.enriched).toBe(true);
301
- expect(result.symbols).toBe(1);
302
-
303
- const storage = path.join(root, ".metaproject", "data", "gdgraph", "storage");
304
- const symbolsFile = await readFile(path.join(storage, "symbols.jsonl"), "utf8");
305
- const callsFile = await readFile(path.join(storage, "calls.jsonl"), "utf8");
306
- expect(symbolsFile.trim()).toBe(JSON.stringify(expectedLayer.symbols[0]));
307
- expect(callsFile.trim()).toBe(JSON.stringify(expectedLayer.calls[0]));
308
- } finally {
309
- await rm(root, { recursive: true, force: true });
310
- }
311
- });
312
-
313
- test("Java grammar resolution — grammarForFile selects java for .java files", async () => {
314
- const root = await mkdtemp(path.join(tmpdir(), "keryx-java-"));
315
- try {
316
- const grammarsDir = path.join(root, "grammars");
317
- await mkdir(path.join(root, ".metaproject"), { recursive: true });
318
- await mkdir(grammarsDir, { recursive: true });
319
-
320
- const wasmPath = path.join(grammarsDir, "tree-sitter-java.wasm");
321
- const bytes = Buffer.from("fake-java-grammar");
322
- await writeFile(wasmPath, bytes);
323
- const sha = createHash("sha256").update(bytes).digest("hex");
324
-
325
- await writeFile(
326
- path.join(root, ".metaproject", "assets.lock.json"),
327
- JSON.stringify({
328
- schemaVersion: 1,
329
- assets: {
330
- "tree-sitter-java": {
331
- version: "0.1.13",
332
- url: "https://example.dev/tree-sitter-java.wasm",
333
- sha256: sha,
334
- size: bytes.length,
335
- },
336
- },
337
- }),
338
- );
339
-
340
- const spec = createTreesitterSpec(root, { languages: ["java"], grammarsPath: grammarsDir });
341
- const adapter = spec.load({ dep: mockParserModule(), asset: null });
342
- expect(await adapter.isAvailable()).toBe(true);
343
- } finally {
344
- await rm(root, { recursive: true, force: true });
345
- }
346
- });
347
-
348
- test("Python grammar resolution — grammarForFile selects python for .py files", async () => {
349
- const root = await mkdtemp(path.join(tmpdir(), "keryx-python-"));
350
- try {
351
- const grammarsDir = path.join(root, "grammars");
352
- await mkdir(path.join(root, ".metaproject"), { recursive: true });
353
- await mkdir(grammarsDir, { recursive: true });
354
-
355
- const wasmPath = path.join(grammarsDir, "tree-sitter-python.wasm");
356
- const bytes = Buffer.from("fake-python-grammar");
357
- await writeFile(wasmPath, bytes);
358
- const sha = createHash("sha256").update(bytes).digest("hex");
359
-
360
- await writeFile(
361
- path.join(root, ".metaproject", "assets.lock.json"),
362
- JSON.stringify({
363
- schemaVersion: 1,
364
- assets: {
365
- "tree-sitter-python": {
366
- version: "0.1.13",
367
- url: "https://example.dev/tree-sitter-python.wasm",
368
- sha256: sha,
369
- size: bytes.length,
370
- },
371
- },
372
- }),
373
- );
374
-
375
- const spec = createTreesitterSpec(root, { languages: ["python"], grammarsPath: grammarsDir });
376
- const adapter = spec.load({ dep: mockParserModule(), asset: null });
377
- expect(await adapter.isAvailable()).toBe(true);
378
- } finally {
379
- await rm(root, { recursive: true, force: true });
380
- }
381
- });
382
-
383
- // --- Real default (no-injected-resolver) production path (T6 review fix) ---
384
- //
385
- // Every test above injects an explicit `CapabilityResolver`/`dep`, bypassing
386
- // `resolveTreesitterCapability` entirely. The REAL call site
387
- // (`build.ts` → `enrichBuildWithSymbols(projectRoot, fileRecords)`, no
388
- // resolver argument) goes through `resolveTreesitterCapability`'s literal
389
- // `await import("web-tree-sitter")` fast path with NO injected resolver at
390
- // all. These tests exercise that exact default path and, via the
391
- // module-top-level `mock.module` above, directly observe whether the literal
392
- // import was attempted — proving gate 1 (manifest-enabled) runs BEFORE the
393
- // import, not after (the bug this ordering fixes).
394
-
395
- beforeEach(() => {
396
- webTreeSitterImportAttempts = 0;
397
- });
398
-
399
- async function makeDisabledOrAbsentWorkspace(enabled: "absent" | false): Promise<string> {
400
- const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-default-"));
401
- if (enabled === false) {
402
- await mkdir(path.join(root, ".metaproject"), { recursive: true });
403
- const manifest = setTreesitterEnabled({}, false);
404
- await writeFile(
405
- path.join(root, ".metaproject", "metaproject.json"),
406
- JSON.stringify(manifest),
407
- "utf8",
408
- );
409
- }
410
- // enabled === "absent" ⇒ no .metaproject/metaproject.json at all (missing
411
- // manifest = off, per seam.ts's own contract).
412
- return root;
413
- }
414
-
415
- test("default path, capability DISABLED (missing manifest) — resolveTreesitterCapability never attempts the web-tree-sitter import", async () => {
416
- const root = await makeDisabledOrAbsentWorkspace("absent");
417
- try {
418
- const adapter = await resolveTreesitterCapability(root, {
419
- languages: ["typescript"],
420
- grammarsPath: null,
421
- });
422
- expect(adapter).toBeNull();
423
- // The core assertion (Fix 1): a disabled/missing-manifest ceiling must
424
- // resolve to null WITHOUT ever attempting the literal dep import.
425
- expect(webTreeSitterImportAttempts).toBe(0);
426
- } finally {
427
- await rm(root, { recursive: true, force: true });
428
- }
429
- });
430
-
431
- test("default path, capability DISABLED (enabled: false) — resolveTreesitterCapability never attempts the web-tree-sitter import", async () => {
432
- const root = await makeDisabledOrAbsentWorkspace(false);
433
- try {
434
- const adapter = await resolveTreesitterCapability(root, {
435
- languages: ["typescript"],
436
- grammarsPath: null,
437
- });
438
- expect(adapter).toBeNull();
439
- expect(webTreeSitterImportAttempts).toBe(0);
440
- } finally {
441
- await rm(root, { recursive: true, force: true });
442
- }
443
- });
444
-
445
- test("default path, capability DISABLED — enrichBuildWithSymbols (no injected resolver) degrades cleanly with no writes and no dep-load attempt", async () => {
446
- const root = await makeDisabledOrAbsentWorkspace("absent");
447
- try {
448
- // No third argument ⇒ the real production default path (build.ts calls
449
- // enrichBuildWithSymbols with exactly two arguments).
450
- const result = await enrichBuildWithSymbols(root, [{ path: "src/x.ts", content: "" }]);
451
- expect(result).toEqual({ enriched: false, symbols: 0, calls: 0 });
452
- expect(webTreeSitterImportAttempts).toBe(0);
453
-
454
- const storage = path.join(root, ".metaproject", "data", "gdgraph", "storage");
455
- await expect(readFile(path.join(storage, "symbols.jsonl"), "utf8")).rejects.toThrow();
456
- } finally {
457
- await rm(root, { recursive: true, force: true });
458
- }
459
- });
460
-
461
- test("default path, capability ENABLED — resolveTreesitterCapability gets past gate 1 and reaches the real isAvailable() probe", async () => {
462
- resetWarnOnce();
463
- const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-default-enabled-"));
464
- try {
465
- await mkdir(path.join(root, ".metaproject"), { recursive: true });
466
- const manifest = setTreesitterEnabled({}, true);
467
- await writeFile(
468
- path.join(root, ".metaproject", "metaproject.json"),
469
- JSON.stringify(manifest),
470
- "utf8",
471
- );
472
-
473
- // No grammarsPath and no lockfile entries ⇒ resolveGrammars deterministically
474
- // resolves zero grammars ⇒ isAvailable() is false, so this proves the
475
- // manifest gate short-circuits correctly on the enabled side too, without
476
- // requiring a real compiled binary or a real WASM grammar asset.
477
- const adapter = await resolveTreesitterCapability(root, {
478
- languages: ["typescript"],
479
- grammarsPath: null,
480
- });
481
-
482
- // No grammar resolves ⇒ isAvailable() is false ⇒ degrades to null, same
483
- // AC0-8 catch-and-degrade contract as the seam.
484
- expect(adapter).toBeNull();
485
- // Gate 1 passed (unlike the disabled cases above) and execution reached
486
- // the real `isAvailable()` probe, which reported unavailable and warned
487
- // once — the process-scoped warn-once tracker is a reliable,
488
- // ordering-independent witness of this (unlike counting the mock
489
- // factory's own invocations, which only fires when THIS file's mock wins
490
- // the module-cache race against any real `web-tree-sitter` import
491
- // elsewhere in a full-suite run — see the top-of-file comment).
492
- expect(hasWarned("gdgraph.treesitter")).toBe(true);
493
- } finally {
494
- await rm(root, { recursive: true, force: true });
495
- }
496
- });