@mrciphersmith/keryx 0.2.83 → 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.
- package/dist/cli.js +28463 -19413
- package/dist/core.js +25751 -0
- package/package.json +15 -2
- package/src/gdgraph/dangling.ts +204 -0
- package/src/gdgraph/find.ts +529 -37
- package/src/gdgraph/repomap.ts +140 -12
- package/src/gdgraph/staleness.ts +22 -9
- package/src/gdgraph/symbol.ts +45 -6
- package/src/gdgraph/treesitter/extract.ts +153 -5
- package/src/gdgraph/wiki-layer.ts +32 -1
- package/src/gdskills/bundled/skills/core/reviewer-skill-creator/SKILL.md +29 -0
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.codex.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.cursor.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.opencode.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.zed.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/flow-orchestrator/SKILL.md +34 -6
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.codex.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.cursor.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.opencode.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.zed.md +1 -1
- package/src/gdskills/bundled/skills/quality/security-audit/SKILL.codex.md +52 -5
- package/src/gdskills/bundled/skills/quality/security-audit/SKILL.cursor.md +52 -5
- package/src/gdskills/bundled/skills/quality/security-audit/SKILL.md +52 -5
- package/src/gdgraph/affected.test.ts +0 -133
- package/src/gdgraph/build-integrity.test.ts +0 -193
- package/src/gdgraph/build-lang.test.ts +0 -406
- package/src/gdgraph/build.test.ts +0 -120
- package/src/gdgraph/config.test.ts +0 -47
- package/src/gdgraph/core-sources.test.ts +0 -99
- package/src/gdgraph/fallback.test.ts +0 -153
- package/src/gdgraph/find.test.ts +0 -78
- package/src/gdgraph/import-kind.test.ts +0 -525
- package/src/gdgraph/path.test.ts +0 -56
- package/src/gdgraph/repomap.test.ts +0 -110
- package/src/gdgraph/service.test.ts +0 -89
- package/src/gdgraph/staleness.test.ts +0 -208
- package/src/gdgraph/symbol.test.ts +0 -89
- package/src/gdgraph/symbols-capability.test.ts +0 -138
- package/src/gdgraph/treesitter/adapter.test.ts +0 -496
- package/src/gdgraph/treesitter/extract.test.ts +0 -278
- package/src/gdgraph/treesitter/no-treesitter-import.test.ts +0 -51
- package/src/gdgraph/treesitter/real-grammar-fixture.test.ts +0 -71
- package/src/gdgraph/treesitter/resolve-calls.test.ts +0 -38
- package/src/gdgraph/wiki-layer-no-git.test.ts +0 -73
- 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
|
-
});
|
package/src/gdgraph/path.test.ts
DELETED
|
@@ -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
|
-
});
|