@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,525 +0,0 @@
1
- // P1 remediation (flow 140) — `keryx gdgraph query cycles` was folding
2
- // `await import()` edges into the load-order cycle count. `Bun.Transpiler
3
- // #scanImports` already reports whether a specifier is a static
4
- // import-statement, a dynamic-import, a require-call, etc.; `build.ts` threw
5
- // that classification away one line after receiving it. These tests are
6
- // written FIRST (TDD RED) against the frozen acceptance criteria in
7
- // `.metaproject/flows/140-2026-08-07-gdgraph-dynamic-import-edges/acceptance-criteria.md`.
8
- //
9
- // Conventions mirror build.test.ts / build-lang.test.ts: uniqueTestRoot(),
10
- // reset(root), buildGraph(root), loadGraph(root).
11
-
12
- import { mkdir, rm, writeFile } from "node:fs/promises";
13
- import { tmpdir } from "node:os";
14
- import path from "node:path";
15
- import { expect, test } from "bun:test";
16
- import { buildGraph } from "./build";
17
- import { getCycles, loadGraph } from "./query";
18
- import { uniqueTestRoot } from "../lib/test-tmp";
19
-
20
- async function reset(root: string): Promise<void> {
21
- await rm(root, { recursive: true, force: true });
22
- await mkdir(root, { recursive: true });
23
- }
24
-
25
- // ---------------------------------------------------------------------------
26
- // AC1 — every edge the transpiler produced carries the kind scanImports
27
- // actually returned, and it survives to the written edge record.
28
- // ---------------------------------------------------------------------------
29
-
30
- test("AC1 — the transpiler's import kind survives onto the written edge record", async () => {
31
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-import-kind-ac1");
32
- await reset(root);
33
- await mkdir(path.join(root, "src"), { recursive: true });
34
- await writeFile(
35
- path.join(root, "src", "index.ts"),
36
- [
37
- "import { staticValue } from './static';",
38
- "const loadDynamic = async () => { const { dynamicValue } = await import('./dynamic'); return dynamicValue; };",
39
- "const required = require('./required');",
40
- "export const result = { staticValue, loadDynamic, required };",
41
- "",
42
- ].join("\n"),
43
- );
44
- await writeFile(path.join(root, "src", "static.ts"), "export const staticValue = 1;\n");
45
- await writeFile(path.join(root, "src", "dynamic.ts"), "export const dynamicValue = 2;\n");
46
- await writeFile(path.join(root, "src", "required.ts"), "export const requiredValue = 3;\n");
47
-
48
- await buildGraph(root);
49
- const graph = await loadGraph(root);
50
- const edgesFromIndex = graph.edges.filter(
51
- (edge) => edge.from === "src/index.ts" && edge.kind === "imports",
52
- );
53
- const importKindByTarget = new Map(edgesFromIndex.map((edge) => [edge.to, edge.importKind]));
54
-
55
- expect(importKindByTarget.get("src/static.ts")).toBe("import-statement");
56
- expect(importKindByTarget.get("src/dynamic.ts")).toBe("dynamic-import");
57
- expect(importKindByTarget.get("src/required.ts")).toBe("require-call");
58
- });
59
-
60
- test("a specifier imported both statically and dynamically in the same file keeps the static classification", async () => {
61
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-import-kind-mixed-specifier");
62
- await reset(root);
63
- await mkdir(path.join(root, "src"), { recursive: true });
64
- await writeFile(
65
- path.join(root, "src", "index.ts"),
66
- [
67
- "import './shared';",
68
- "const loadAgain = async () => { await import('./shared'); };",
69
- "export const result = loadAgain;",
70
- "",
71
- ].join("\n"),
72
- );
73
- await writeFile(path.join(root, "src", "shared.ts"), "export const shared = true;\n");
74
-
75
- await buildGraph(root);
76
- const graph = await loadGraph(root);
77
- const edge = graph.edges.find((item) => item.from === "src/index.ts" && item.to === "src/shared.ts");
78
-
79
- expect(edge?.importKind).toBe("import-statement");
80
- });
81
-
82
- // ---------------------------------------------------------------------------
83
- // AC2 — a fixture reproducing the target's shape: one static edge, one
84
- // dynamic-import edge back, no longer reported as a load-order cycle.
85
- // ---------------------------------------------------------------------------
86
-
87
- test("AC2 — a cycle closed only through a dynamic-import edge is no longer reported as load-order", async () => {
88
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-cycle-mixed");
89
- await reset(root);
90
- await mkdir(path.join(root, "bot", "commands"), { recursive: true });
91
- // Mirrors the target's shape: commands/menu.ts statically imports
92
- // callbacks.ts; callbacks.ts reaches back into menu.ts only via `await
93
- // import()`.
94
- await writeFile(
95
- path.join(root, "bot", "commands", "menu.ts"),
96
- "import { registerCallback } from '../callbacks';\nexport const menu = () => registerCallback();\n",
97
- );
98
- await writeFile(
99
- path.join(root, "bot", "callbacks.ts"),
100
- [
101
- "export const registerCallback = () => 1;",
102
- "export const handleMenuCallback = async () => {",
103
- " const { menu } = await import('./commands/menu');",
104
- " return menu();",
105
- "};",
106
- "",
107
- ].join("\n"),
108
- );
109
-
110
- await buildGraph(root);
111
- const graph = await loadGraph(root);
112
- const cycles = getCycles(graph);
113
-
114
- expect(
115
- cycles.some((cycle) => cycle.includes("bot/callbacks.ts") && cycle.includes("bot/commands/menu.ts")),
116
- ).toBe(false);
117
- });
118
-
119
- // ---------------------------------------------------------------------------
120
- // AC3 — classification both ways, same two-file cycle shape.
121
- // ---------------------------------------------------------------------------
122
-
123
- test("AC3 — a two-file cycle formed by static imports IS reported", async () => {
124
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-cycle-static");
125
- await reset(root);
126
- await mkdir(path.join(root, "src"), { recursive: true });
127
- await writeFile(path.join(root, "src", "a.ts"), "import { b } from './b';\nexport const a = () => b();\n");
128
- await writeFile(path.join(root, "src", "b.ts"), "import { a } from './a';\nexport const b = () => a();\n");
129
-
130
- await buildGraph(root);
131
- const graph = await loadGraph(root);
132
- const cycles = getCycles(graph);
133
-
134
- expect(cycles.some((cycle) => cycle.includes("src/a.ts") && cycle.includes("src/b.ts"))).toBe(true);
135
- });
136
-
137
- test("AC3 — the same two-file cycle formed by await import() is NOT reported as load-order", async () => {
138
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-cycle-dynamic");
139
- await reset(root);
140
- await mkdir(path.join(root, "src"), { recursive: true });
141
- await writeFile(
142
- path.join(root, "src", "a.ts"),
143
- "export const a = async () => { const { b } = await import('./b'); return b(); };\n",
144
- );
145
- await writeFile(
146
- path.join(root, "src", "b.ts"),
147
- "export const b = async () => { const { a } = await import('./a'); return a(); };\n",
148
- );
149
-
150
- await buildGraph(root);
151
- const graph = await loadGraph(root);
152
- const cycles = getCycles(graph);
153
-
154
- expect(cycles.some((cycle) => cycle.includes("src/a.ts") && cycle.includes("src/b.ts"))).toBe(false);
155
- });
156
-
157
- // ---------------------------------------------------------------------------
158
- // AC4 (flow 140) — an edge found only by the regex fallback (never seen by
159
- // scanImports) is marked with an explicit unknown/static marker, never
160
- // inferred dynamic.
161
- //
162
- // SUPERSEDED for the TS/JS type-only case by AFC-11 (flow 234, below): a
163
- // fallback-only edge whose file the transpiler DID successfully scan is now
164
- // known to be type-only, not merely "unknown" — it gets its own
165
- // `TYPE_ONLY_IMPORT_KIND` so cycle detection can exclude it while impact
166
- // analysis keeps it. The Java case is untouched: Java is routed straight to
167
- // the fallback (`extractImportRecords` never calls `scanImports` for it), so
168
- // its provenance is genuinely unknown and it keeps `unknown-static`.
169
- // ---------------------------------------------------------------------------
170
-
171
- test("AC4 (flow 140, still applies to Java) — a fallback-only Java edge is marked unknown-static, never inferred dynamic", async () => {
172
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-import-kind-java");
173
- await reset(root);
174
- const javaRoot = path.join(root, "src", "main", "java", "com", "example");
175
- await mkdir(javaRoot, { recursive: true });
176
- await writeFile(
177
- path.join(javaRoot, "Consumer.java"),
178
- "package com.example;\nimport com.example.Model;\npublic class Consumer {}\n",
179
- );
180
- await writeFile(path.join(javaRoot, "Model.java"), "package com.example;\npublic class Model {}\n");
181
-
182
- await buildGraph(root);
183
- const graph = await loadGraph(root);
184
- const edge = graph.edges.find(
185
- (item) =>
186
- item.from === "src/main/java/com/example/Consumer.java" &&
187
- item.to === "src/main/java/com/example/Model.java",
188
- );
189
-
190
- expect(edge).toBeDefined();
191
- expect(edge?.importKind).toBe("unknown-static");
192
- });
193
-
194
- // ---------------------------------------------------------------------------
195
- // AFC-11 (flow 234, phase 2) — frozen AC4:
196
- // "Type-only цикл не попадает в runtime список; mixed import с
197
- // runtime-частью попадает; consumer типа виден в impact; цикл сам по себе
198
- // не блокирует gate."
199
- //
200
- // Reproduced first (see the task report for the exact repro run), then fixed:
201
- // a type-only cycle (`import type` on both sides) built as `unknown-static`
202
- // and was reported by `getCycles` as a real runtime cycle, because
203
- // `unknown-static` was — and for Java/unparseable TS still is — treated as
204
- // load-order. The fix gives TS/JS type-only edges their own kind,
205
- // `TYPE_ONLY_IMPORT_KIND` ("type-only"), which `getCycles` (query.ts)
206
- // excludes from load-order adjacency the same way it already excludes
207
- // `dynamic-import`, while every impact-analysis consumer (`getOrphans`,
208
- // `getAffected`, `computeAffected`) is untouched because they read
209
- // `edge.kind`, not `edge.importKind`.
210
- // ---------------------------------------------------------------------------
211
-
212
- // --- requirement 1 + classification table: the four "is it type-only?"
213
- // spellings this task must distinguish. Each RED-failed against the
214
- // pre-fix code with `edge?.importKind === "unknown-static"` (see report).
215
-
216
- test("AFC-11 req1 — `import type { X } from './m'` (whole-statement) is classified type-only", async () => {
217
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-import-type-whole");
218
- await reset(root);
219
- await mkdir(path.join(root, "src"), { recursive: true });
220
- await writeFile(
221
- path.join(root, "src", "consumer.ts"),
222
- "import type { Shape } from './types';\nexport const consumer: Shape = { ok: true } as Shape;\n",
223
- );
224
- await writeFile(path.join(root, "src", "types.ts"), "export interface Shape { ok: boolean }\n");
225
-
226
- await buildGraph(root);
227
- const graph = await loadGraph(root);
228
- const edge = graph.edges.find((item) => item.from === "src/consumer.ts" && item.to === "src/types.ts");
229
-
230
- expect(edge?.importKind).toBe("type-only");
231
- });
232
-
233
- test("AFC-11 req1 — `import { type X } from './m'` (inline, all-type) is classified type-only", async () => {
234
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-import-type-inline");
235
- await reset(root);
236
- await mkdir(path.join(root, "src"), { recursive: true });
237
- await writeFile(
238
- path.join(root, "src", "consumer.ts"),
239
- "import { type Shape } from './types';\nexport const consumer: Shape = { ok: true } as Shape;\n",
240
- );
241
- await writeFile(path.join(root, "src", "types.ts"), "export interface Shape { ok: boolean }\n");
242
-
243
- await buildGraph(root);
244
- const graph = await loadGraph(root);
245
- const edge = graph.edges.find((item) => item.from === "src/consumer.ts" && item.to === "src/types.ts");
246
-
247
- expect(edge?.importKind).toBe("type-only");
248
- });
249
-
250
- test("AFC-11 req1 — `export type { X } from './m'` (whole-statement re-export) is classified type-only", async () => {
251
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-export-type-whole");
252
- await reset(root);
253
- await mkdir(path.join(root, "src"), { recursive: true });
254
- await writeFile(path.join(root, "src", "types.ts"), "export interface Shape { ok: boolean }\n");
255
- await writeFile(path.join(root, "src", "reexport.ts"), "export type { Shape } from './types';\n");
256
-
257
- await buildGraph(root);
258
- const graph = await loadGraph(root);
259
- const edge = graph.edges.find((item) => item.from === "src/reexport.ts" && item.to === "src/types.ts");
260
-
261
- expect(edge?.importKind).toBe("type-only");
262
- });
263
-
264
- test("AFC-11 req1 — `export { type X } from './m'` (inline re-export, all-type) is classified type-only", async () => {
265
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-export-type-inline");
266
- await reset(root);
267
- await mkdir(path.join(root, "src"), { recursive: true });
268
- await writeFile(path.join(root, "src", "types.ts"), "export interface Shape { ok: boolean }\n");
269
- await writeFile(path.join(root, "src", "reexport.ts"), "export { type Shape } from './types';\n");
270
-
271
- await buildGraph(root);
272
- const graph = await loadGraph(root);
273
- const edge = graph.edges.find((item) => item.from === "src/reexport.ts" && item.to === "src/types.ts");
274
-
275
- expect(edge?.importKind).toBe("type-only");
276
- });
277
-
278
- // --- requirement 1: the actual reported symptom — a two-file cycle formed
279
- // only by type-only edges must not show up in `getCycles`.
280
-
281
- test("AFC-11 req1 — a two-file cycle formed only by `import type` on both sides is NOT reported", async () => {
282
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-cycle-import-type");
283
- await reset(root);
284
- await mkdir(path.join(root, "src"), { recursive: true });
285
- await writeFile(
286
- path.join(root, "src", "a.ts"),
287
- "import type { BType } from './b';\nexport type AType = { b?: BType };\n",
288
- );
289
- await writeFile(
290
- path.join(root, "src", "b.ts"),
291
- "import type { AType } from './a';\nexport type BType = { a?: AType };\n",
292
- );
293
-
294
- await buildGraph(root);
295
- const graph = await loadGraph(root);
296
- const cycles = getCycles(graph);
297
-
298
- expect(cycles.some((cycle) => cycle.includes("src/a.ts") && cycle.includes("src/b.ts"))).toBe(false);
299
- });
300
-
301
- test("AFC-11 req1 — a two-file cycle formed only by `export type … from` on both sides is NOT reported", async () => {
302
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-cycle-export-type");
303
- await reset(root);
304
- await mkdir(path.join(root, "src"), { recursive: true });
305
- await writeFile(path.join(root, "src", "a.ts"), "export type { BType } from './b';\nexport type AType = {};\n");
306
- await writeFile(path.join(root, "src", "b.ts"), "export type { AType } from './a';\nexport type BType = {};\n");
307
-
308
- await buildGraph(root);
309
- const graph = await loadGraph(root);
310
- const cycles = getCycles(graph);
311
-
312
- expect(cycles.some((cycle) => cycle.includes("src/a.ts") && cycle.includes("src/b.ts"))).toBe(false);
313
- });
314
-
315
- // --- a fifth, non-type-erasing spelling: a plain value import whose binding
316
- // is used only in a type position at the use site. `import { x } from './m'`
317
- // carries no `type` keyword anywhere, so the transpiler (this repo's
318
- // tsconfig sets neither `importsNotUsedAsValues` nor `verbatimModuleSyntax`,
319
- // so TypeScript's/Bun's default elision applies — see the report) does NOT
320
- // erase it: erasure here is purely syntactic (was `type` written?), never a
321
- // usage analysis (is the binding read only as a type elsewhere?). This test
322
- // guards against a fix that tries to be "smarter" than the transpiler by
323
- // inferring type-only-ness from usage — it must stay a real runtime edge.
324
-
325
- test("`import { x } from './m'` used only in a type position stays import-statement (no usage-based erasure)", async () => {
326
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-value-used-as-type");
327
- await reset(root);
328
- await mkdir(path.join(root, "src"), { recursive: true });
329
- await writeFile(
330
- path.join(root, "src", "consumer.ts"),
331
- "import { value } from './lib';\nexport const y: typeof value = value;\n",
332
- );
333
- await writeFile(path.join(root, "src", "lib.ts"), "export const value = { ok: true };\n");
334
-
335
- await buildGraph(root);
336
- const graph = await loadGraph(root);
337
- const edge = graph.edges.find((item) => item.from === "src/consumer.ts" && item.to === "src/lib.ts");
338
-
339
- expect(edge?.importKind).toBe("import-statement");
340
- });
341
-
342
- // --- requirement 2 (over-correction guard) — a mixed import (one runtime
343
- // specifier alongside a type specifier) still produces a real load-order
344
- // edge, and a cycle closed through it IS reported. This is the test that
345
- // would fail if requirement 1 were implemented too bluntly (e.g. by
346
- // excluding every edge whose statement merely CONTAINS the word `type`).
347
-
348
- test("AFC-11 req2 — `import { x, type Y } from './m'` (mixed) is classified import-statement, a real runtime edge", async () => {
349
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-mixed-classification");
350
- await reset(root);
351
- await mkdir(path.join(root, "src"), { recursive: true });
352
- await writeFile(
353
- path.join(root, "src", "consumer.ts"),
354
- "import { value, type Shape } from './lib';\nexport const consumer: Shape = value;\n",
355
- );
356
- await writeFile(
357
- path.join(root, "src", "lib.ts"),
358
- "export interface Shape { ok: boolean }\nexport const value = { ok: true };\n",
359
- );
360
-
361
- await buildGraph(root);
362
- const graph = await loadGraph(root);
363
- const edge = graph.edges.find((item) => item.from === "src/consumer.ts" && item.to === "src/lib.ts");
364
-
365
- expect(edge?.importKind).toBe("import-statement");
366
- });
367
-
368
- test("AFC-11 req2 (the over-correction guard) — a two-file cycle where both directions are mixed imports IS reported", async () => {
369
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-cycle-mixed-both");
370
- await reset(root);
371
- await mkdir(path.join(root, "src"), { recursive: true });
372
- await writeFile(
373
- path.join(root, "src", "a.ts"),
374
- "import { b, type BType } from './b';\nexport type AType = { b?: BType };\nexport const a = () => b();\n",
375
- );
376
- await writeFile(
377
- path.join(root, "src", "b.ts"),
378
- "import { a, type AType } from './a';\nexport type BType = { a?: AType };\nexport const b = () => a();\n",
379
- );
380
-
381
- await buildGraph(root);
382
- const graph = await loadGraph(root);
383
- const cycles = getCycles(graph);
384
-
385
- expect(cycles.some((cycle) => cycle.includes("src/a.ts") && cycle.includes("src/b.ts"))).toBe(true);
386
- });
387
-
388
- // --- requirement 4 — a detected cycle is information only; it never throws,
389
- // never sets a failure flag, and the JSON `getCycles` returns is exactly what
390
- // a caller (CLI/MCP) renders as a plain list. There is no "gate" primitive in
391
- // this module's return type to fail: proving that is proving there is no
392
- // exception and no truthy/falsy "blocked" field mixed into the result.
393
-
394
- // ---------------------------------------------------------------------------
395
- // T19 finding 2 (flow 234 review) — `extractImportRecords` hardcoded the
396
- // `tsx` transpiler loader for every `.ts` file. Under `tsx`, valid TypeScript
397
- // syntax that collides with JSX grammar throws: a generic arrow function
398
- // (`<T>(x: T): T => x`) and an angle-bracket cast (`<string>value`) are both
399
- // ambiguous with a JSX opening tag. When the scan throws, EVERY specifier in
400
- // the file falls back to the regex extractor with `scanResult.succeeded ===
401
- // false`, so a type-only cycle between two such files was still classified
402
- // `UNKNOWN_IMPORT_KIND` — which `getCycles` treats as load-order — defeating
403
- // AC4 clause 1 on a real, constructible TypeScript input. Reproduced directly
404
- // against `src/tui/shell-chrome.ts` / `src/sac/fwk-service.test.ts` (both
405
- // throw under the `tsx` loader, both parse under `ts`) — see the task report.
406
- // Fixed by choosing the loader from the file extension (`ts` for `.ts`,
407
- // `tsx` for `.tsx`/`.jsx`), so a `.ts` file — which can never legally contain
408
- // JSX — is parsed with JSX grammar disabled.
409
- // ---------------------------------------------------------------------------
410
-
411
- test("T19 finding 2 — a generic arrow function no longer breaks the scan for a .ts file", async () => {
412
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-t19-generic-arrow");
413
- await reset(root);
414
- await mkdir(path.join(root, "src"), { recursive: true });
415
- await writeFile(
416
- path.join(root, "src", "identity.ts"),
417
- "import { helper } from './helper';\nexport const identity = <T>(x: T): T => x;\nexport const used = helper();\n",
418
- );
419
- await writeFile(path.join(root, "src", "helper.ts"), "export const helper = () => 1;\n");
420
-
421
- await buildGraph(root);
422
- const graph = await loadGraph(root);
423
- const edge = graph.edges.find((item) => item.from === "src/identity.ts" && item.to === "src/helper.ts");
424
-
425
- // Before the fix: the `tsx` scan of identity.ts throws on `<T>(x: T)`, the
426
- // whole file falls back to the regex extractor, and this edge is
427
- // classified `unknown-static`, not the real `import-statement`.
428
- expect(edge?.importKind).toBe("import-statement");
429
- });
430
-
431
- test("T19 finding 2 — an angle-bracket cast no longer breaks the scan for a .ts file", async () => {
432
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-t19-angle-cast");
433
- await reset(root);
434
- await mkdir(path.join(root, "src"), { recursive: true });
435
- await writeFile(
436
- path.join(root, "src", "cast.ts"),
437
- "import { helper } from './helper';\nexport const cast = (value: unknown) => <string>value;\nexport const used = helper();\n",
438
- );
439
- await writeFile(path.join(root, "src", "helper.ts"), "export const helper = () => 1;\n");
440
-
441
- await buildGraph(root);
442
- const graph = await loadGraph(root);
443
- const edge = graph.edges.find((item) => item.from === "src/cast.ts" && item.to === "src/helper.ts");
444
-
445
- expect(edge?.importKind).toBe("import-statement");
446
- });
447
-
448
- test("T19 finding 2 — a type-only cycle between two files that also use a generic arrow / angle-bracket cast is NOT reported as a runtime cycle", async () => {
449
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-t19-cycle-jsx-collision");
450
- await reset(root);
451
- await mkdir(path.join(root, "src"), { recursive: true });
452
- await writeFile(
453
- path.join(root, "src", "a.ts"),
454
- [
455
- "import type { BType } from './b';",
456
- "export type AType = { b?: BType };",
457
- "export const identity = <T>(x: T): T => x;",
458
- "",
459
- ].join("\n"),
460
- );
461
- await writeFile(
462
- path.join(root, "src", "b.ts"),
463
- [
464
- "import type { AType } from './a';",
465
- "export type BType = { a?: AType };",
466
- "export const cast = (value: unknown) => <string>value;",
467
- "",
468
- ].join("\n"),
469
- );
470
-
471
- await buildGraph(root);
472
- const graph = await loadGraph(root);
473
-
474
- const edgeAB = graph.edges.find((item) => item.from === "src/a.ts" && item.to === "src/b.ts");
475
- const edgeBA = graph.edges.find((item) => item.from === "src/b.ts" && item.to === "src/a.ts");
476
- // Before the fix, both scans throw (the file collides with JSX grammar
477
- // under `tsx`), so both edges fall back to `unknown-static` — which
478
- // `getCycles` treats as load-order — instead of the real `type-only`.
479
- expect(edgeAB?.importKind).toBe("type-only");
480
- expect(edgeBA?.importKind).toBe("type-only");
481
-
482
- const cycles = getCycles(graph);
483
- expect(cycles.some((cycle) => cycle.includes("src/a.ts") && cycle.includes("src/b.ts"))).toBe(false);
484
- });
485
-
486
- // A genuinely unparseable `.ts` file must still classify as unknown — the
487
- // fix must not weaken AC4 (flow 140) by treating every scan failure as
488
- // type-only. Unbalanced syntax throws under BOTH the `ts` and `tsx` loaders
489
- // (verified directly against `Bun.Transpiler` — see the task report), so this
490
- // proves the retry/extension-selection does not paper over a real parse
491
- // failure.
492
- test("T19 finding 2 (both directions) — a genuinely unparseable .ts file still classifies its edges as unknown-static", async () => {
493
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-t19-unparseable-ts");
494
- await reset(root);
495
- await mkdir(path.join(root, "src"), { recursive: true });
496
- await writeFile(
497
- path.join(root, "src", "broken.ts"),
498
- "import { helper } from './helper';\nexport const broken = {\n",
499
- );
500
- await writeFile(path.join(root, "src", "helper.ts"), "export const helper = () => 1;\n");
501
-
502
- await buildGraph(root);
503
- const graph = await loadGraph(root);
504
- const edge = graph.edges.find((item) => item.from === "src/broken.ts" && item.to === "src/helper.ts");
505
-
506
- expect(edge?.importKind).toBe("unknown-static");
507
- });
508
-
509
- test("AFC-11 req4 — a real runtime cycle is reported without throwing, as plain data (not a gate failure)", async () => {
510
- const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-afc11-gate");
511
- await reset(root);
512
- await mkdir(path.join(root, "src"), { recursive: true });
513
- await writeFile(path.join(root, "src", "a.ts"), "import { b } from './b';\nexport const a = () => b();\n");
514
- await writeFile(path.join(root, "src", "b.ts"), "import { a } from './a';\nexport const b = () => a();\n");
515
-
516
- await buildGraph(root);
517
- const graph = await loadGraph(root);
518
-
519
- expect(() => getCycles(graph)).not.toThrow();
520
- const cycles = getCycles(graph);
521
- expect(Array.isArray(cycles)).toBe(true);
522
- expect(cycles.length).toBeGreaterThan(0);
523
- // The return type is `string[][]` — there is no boolean/gate field on it to
524
- // assert "false" for; the absence of one IS the requirement.
525
- });
@@ -1,56 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import { findPath, labelNode } from "./path";
3
- import type { GraphData, SymbolNode } from "./types";
4
-
5
- function fileNode(path: string) {
6
- return { id: path, kind: "file" as const, path, language: "typescript" as const };
7
- }
8
- function importEdge(from: string, to: string) {
9
- return { id: `${from}->${to}`, from, to, kind: "imports" as const, specifier: to };
10
- }
11
- function sym(id: string, name: string, path: string, line: number): SymbolNode {
12
- return { id, kind: "function", path, name, container: null, startLine: line, endLine: line + 3, language: "typescript" };
13
- }
14
- function call(from: string, to: string) {
15
- return { id: `${from}=>${to}`, from, to, kind: "calls" as const, resolved: true };
16
- }
17
-
18
- // a.ts imports b.ts imports c.ts; symbols foo(a) -> bar(b)
19
- const GRAPH: GraphData = {
20
- nodes: [fileNode("src/a.ts"), fileNode("src/b.ts"), fileNode("src/c.ts")],
21
- edges: [importEdge("src/a.ts", "src/b.ts"), importEdge("src/b.ts", "src/c.ts")],
22
- symbols: [sym("src/a.ts#foo", "foo", "src/a.ts", 1), sym("src/b.ts#bar", "bar", "src/b.ts", 2)],
23
- calls: [call("src/a.ts#foo", "src/b.ts#bar")],
24
- };
25
-
26
- test("finds a file-to-file import path", () => {
27
- const r = findPath(GRAPH, "src/a.ts", "src/c.ts");
28
- expect(r.nodes).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
29
- });
30
-
31
- test("connects two symbols across the file+call layers", () => {
32
- const r = findPath(GRAPH, "foo", "bar");
33
- // foo -> bar via the resolved call edge (or foo -> a.ts -> b.ts -> bar)
34
- expect(r.nodes[0]).toBe("src/a.ts#foo");
35
- expect(r.nodes.at(-1)).toBe("src/b.ts#bar");
36
- expect(r.nodes.length).toBeGreaterThanOrEqual(2);
37
- });
38
-
39
- test("resolves endpoints by symbol name and by file substring", () => {
40
- const r = findPath(GRAPH, "foo", "c.ts");
41
- expect(r.fromResolved).toContain("src/a.ts#foo");
42
- expect(r.toResolved).toContain("src/c.ts");
43
- expect(r.nodes.length).toBeGreaterThan(0);
44
- });
45
-
46
- test("returns empty nodes when an endpoint cannot be resolved", () => {
47
- const r = findPath(GRAPH, "foo", "does-not-exist-xyz");
48
- expect(r.toResolved).toEqual([]);
49
- expect(r.nodes).toEqual([]);
50
- });
51
-
52
- test("labelNode renders symbols as name (path:line) and files verbatim", () => {
53
- const byId = new Map(GRAPH.symbols!.map((s) => [s.id, s]));
54
- expect(labelNode("src/a.ts#foo", byId)).toBe("foo (src/a.ts:1)");
55
- expect(labelNode("src/c.ts", byId)).toBe("src/c.ts");
56
- });