@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,120 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { expect, test } from "bun:test";
|
|
5
|
-
import { buildGraph } from "./build";
|
|
6
|
-
import { loadGraph } from "./query";
|
|
7
|
-
import { uniqueTestRoot } from "../lib/test-tmp";
|
|
8
|
-
|
|
9
|
-
test("buildGraph ignores generated frontend outputs and resolves imported assets", async () => {
|
|
10
|
-
const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-build");
|
|
11
|
-
await reset(root);
|
|
12
|
-
await mkdir(path.join(root, "src", "feature"), { recursive: true });
|
|
13
|
-
await mkdir(path.join(root, "public"), { recursive: true });
|
|
14
|
-
await mkdir(path.join(root, "storybook-static", "assets"), { recursive: true });
|
|
15
|
-
|
|
16
|
-
await writeFile(
|
|
17
|
-
path.join(root, "src", "feature", "index.ts"),
|
|
18
|
-
[
|
|
19
|
-
importLine("'./style.css';"),
|
|
20
|
-
importLine("icon from './icon.svg?react';"),
|
|
21
|
-
importLine("raw from './template.hbs?raw';"),
|
|
22
|
-
importLine("{ value } from './value';"),
|
|
23
|
-
"export const result = `${value}-${icon}-${raw}`;",
|
|
24
|
-
"",
|
|
25
|
-
].join("\n"),
|
|
26
|
-
);
|
|
27
|
-
await writeFile(path.join(root, "src", "feature", "value.ts"), "export const value = 'ok';\n");
|
|
28
|
-
await writeFile(path.join(root, "src", "feature", "style.css"), ".x { color: red; }\n");
|
|
29
|
-
await writeFile(path.join(root, "src", "feature", "icon.svg"), "<svg />\n");
|
|
30
|
-
await writeFile(path.join(root, "src", "feature", "template.hbs"), "<div>{{value}}</div>\n");
|
|
31
|
-
await writeFile(path.join(root, "public", "worker.js"), "export const generated = true;\n");
|
|
32
|
-
await writeFile(path.join(root, "storybook-static", "assets", "bundle.js"), "export const generated = true;\n");
|
|
33
|
-
|
|
34
|
-
const result = await buildGraph(root);
|
|
35
|
-
const graph = await loadGraph(root);
|
|
36
|
-
const summary = await readFile(result.summaryPath, "utf8");
|
|
37
|
-
|
|
38
|
-
expect(graph.nodes.map((node) => node.path).sort()).toEqual([
|
|
39
|
-
"src/feature/icon.svg",
|
|
40
|
-
"src/feature/index.ts",
|
|
41
|
-
"src/feature/style.css",
|
|
42
|
-
"src/feature/template.hbs",
|
|
43
|
-
"src/feature/value.ts",
|
|
44
|
-
]);
|
|
45
|
-
expect(graph.nodes.filter((node) => node.kind === "file").length).toBe(2);
|
|
46
|
-
expect(graph.nodes.filter((node) => node.kind === "asset").length).toBe(3);
|
|
47
|
-
expect(graph.edges.filter((edge) => edge.kind === "imports").length).toBe(1);
|
|
48
|
-
expect(graph.edges.filter((edge) => edge.kind === "asset").length).toBe(3);
|
|
49
|
-
expect(graph.edges.filter((edge) => edge.kind === "unresolved").length).toBe(0);
|
|
50
|
-
expect(summary).toContain("Skipped generated/static directories: 2");
|
|
51
|
-
expect(summary).toContain("| feature | 2 |");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("buildGraph resolves tsconfig path aliases and parser-scanned imports", async () => {
|
|
55
|
-
const root = uniqueTestRoot(tmpdir(), "keryx-gdgraph-aliases");
|
|
56
|
-
await reset(root);
|
|
57
|
-
await mkdir(path.join(root, "src", "components"), { recursive: true });
|
|
58
|
-
await mkdir(path.join(root, "src", "utils"), { recursive: true });
|
|
59
|
-
await mkdir(path.join(root, "src", "assets"), { recursive: true });
|
|
60
|
-
|
|
61
|
-
await writeFile(
|
|
62
|
-
path.join(root, "tsconfig.json"),
|
|
63
|
-
JSON.stringify({
|
|
64
|
-
compilerOptions: {
|
|
65
|
-
baseUrl: ".",
|
|
66
|
-
paths: {
|
|
67
|
-
"@/*": ["src/*"],
|
|
68
|
-
"~assets/*": ["src/assets/*"],
|
|
69
|
-
"#config": ["src/config.ts"],
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
}),
|
|
73
|
-
);
|
|
74
|
-
await writeFile(
|
|
75
|
-
path.join(root, "src", "index.ts"),
|
|
76
|
-
[
|
|
77
|
-
"import { Button } from '@/components/Button';",
|
|
78
|
-
"export { value } from '@/utils';",
|
|
79
|
-
"import logo from '~assets/logo.svg?raw';",
|
|
80
|
-
"import config from '#config';",
|
|
81
|
-
"const lazy = () => import('@/lazy');",
|
|
82
|
-
"const cjs = require('@/cjs');",
|
|
83
|
-
"const ignored = (name: string) => import(`@/dynamic/${name}`);",
|
|
84
|
-
"export const result = { Button, logo, config, lazy, cjs, ignored };",
|
|
85
|
-
"",
|
|
86
|
-
].join("\n"),
|
|
87
|
-
);
|
|
88
|
-
await writeFile(path.join(root, "src", "components", "Button.tsx"), "import React from 'react';\nexport const Button = () => null;\n");
|
|
89
|
-
await writeFile(path.join(root, "src", "utils", "index.ts"), "export const value = 1;\n");
|
|
90
|
-
await writeFile(path.join(root, "src", "config.ts"), "export default { ok: true };\n");
|
|
91
|
-
await writeFile(path.join(root, "src", "lazy.ts"), "export const lazy = true;\n");
|
|
92
|
-
await writeFile(path.join(root, "src", "cjs.ts"), "export const cjs = true;\n");
|
|
93
|
-
await writeFile(path.join(root, "src", "assets", "logo.svg"), "<svg />\n");
|
|
94
|
-
|
|
95
|
-
await buildGraph(root);
|
|
96
|
-
const graph = await loadGraph(root);
|
|
97
|
-
const edgesFromIndex = graph.edges.filter((edge) => edge.from === "src/index.ts");
|
|
98
|
-
|
|
99
|
-
expect(edgesFromIndex.filter((edge) => edge.kind === "imports").map((edge) => edge.to).sort()).toEqual([
|
|
100
|
-
"src/cjs.ts",
|
|
101
|
-
"src/components/Button.tsx",
|
|
102
|
-
"src/config.ts",
|
|
103
|
-
"src/lazy.ts",
|
|
104
|
-
"src/utils/index.ts",
|
|
105
|
-
]);
|
|
106
|
-
expect(edgesFromIndex.filter((edge) => edge.kind === "asset").map((edge) => edge.to)).toEqual([
|
|
107
|
-
"src/assets/logo.svg",
|
|
108
|
-
]);
|
|
109
|
-
expect(graph.edges.filter((edge) => edge.kind === "unresolved")).toEqual([]);
|
|
110
|
-
expect(graph.edges.some((edge) => edge.specifier.includes("dynamic"))).toBe(false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
async function reset(root: string): Promise<void> {
|
|
114
|
-
await rm(root, { recursive: true, force: true });
|
|
115
|
-
await mkdir(root, { recursive: true });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function importLine(rest: string): string {
|
|
119
|
-
return `im${"port"} ${rest}`;
|
|
120
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { expect, test } from "bun:test";
|
|
5
|
-
import { DEFAULT_GDGRAPH_CONFIG, loadGdgraphConfig, mergeGdgraphConfig } from "./config";
|
|
6
|
-
|
|
7
|
-
test("AC5.1 — defaults: affected.defaultDepth is 1 (back-compat)", () => {
|
|
8
|
-
expect(DEFAULT_GDGRAPH_CONFIG.affected.defaultDepth).toBe(1);
|
|
9
|
-
expect(mergeGdgraphConfig({}).repomap.tokenBudget).toBe(8000);
|
|
10
|
-
expect(mergeGdgraphConfig({}).repomap.tokenEstimator).toBe("chars-div-4");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("AC5.1 — deep-merge overrides individual fields, keeps the rest", () => {
|
|
14
|
-
const merged = mergeGdgraphConfig({
|
|
15
|
-
affected: { defaultDepth: 3 },
|
|
16
|
-
repomap: { tokenBudget: 100 },
|
|
17
|
-
treesitter: { languages: ["typescript"] },
|
|
18
|
-
});
|
|
19
|
-
expect(merged.affected.defaultDepth).toBe(3);
|
|
20
|
-
expect(merged.repomap.tokenBudget).toBe(100);
|
|
21
|
-
// Untouched fields fall back to defaults.
|
|
22
|
-
expect(merged.repomap.damping).toBe(DEFAULT_GDGRAPH_CONFIG.repomap.damping);
|
|
23
|
-
expect(merged.treesitter.languages).toEqual(["typescript"]);
|
|
24
|
-
expect(merged.treesitter.grammarsPath).toBeNull();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("AC5.1/C0-8 — missing config file ⇒ defaults", async () => {
|
|
28
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-cfg-"));
|
|
29
|
-
try {
|
|
30
|
-
const config = await loadGdgraphConfig(root);
|
|
31
|
-
expect(config).toEqual(mergeGdgraphConfig({}));
|
|
32
|
-
} finally {
|
|
33
|
-
await rm(root, { recursive: true, force: true });
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("AC5.1/C0-8 — malformed JSON ⇒ defaults (never throws)", async () => {
|
|
38
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-cfg-bad-"));
|
|
39
|
-
try {
|
|
40
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
41
|
-
await writeFile(path.join(root, ".metaproject", "gdgraph.config.json"), "{ not valid json ");
|
|
42
|
-
const config = await loadGdgraphConfig(root);
|
|
43
|
-
expect(config).toEqual(mergeGdgraphConfig({}));
|
|
44
|
-
} finally {
|
|
45
|
-
await rm(root, { recursive: true, force: true });
|
|
46
|
-
}
|
|
47
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// The copied gdgraph core must be import-closed.
|
|
2
|
-
//
|
|
3
|
-
// `init` and `update` copy a few `src/gdgraph/*.ts` files into
|
|
4
|
-
// `.metaproject/core/gdgraph/` so a project can run the graph builder without
|
|
5
|
-
// the full toolkit. The list was hand-maintained, in two places, and nothing
|
|
6
|
-
// checked it against what those files actually import.
|
|
7
|
-
//
|
|
8
|
-
// So when `query.ts` gained `import { resolveGraphTarget } from "./target"`,
|
|
9
|
-
// neither copy learned about it, and `keryx gdgraph build` — the very first
|
|
10
|
-
// "Next step" that `init` prints — died on every fresh install with
|
|
11
|
-
// `Cannot find module './target'`. The suite stayed green throughout, because
|
|
12
|
-
// nothing ever ran the copied tree.
|
|
13
|
-
//
|
|
14
|
-
// The domain of "which files does the copied core need" is not a list. It is
|
|
15
|
-
// the transitive closure of RUNTIME imports from the entry points, and written
|
|
16
|
-
// as a list it goes stale on the next `import`.
|
|
17
|
-
//
|
|
18
|
-
// The first version of this test matched imports with a regular expression and
|
|
19
|
-
// immediately proved the repository's own lesson about pattern guards: it
|
|
20
|
-
// matched the string `from "./m"` inside a COMMENT in `build.ts` and demanded a
|
|
21
|
-
// file called `m.ts`. So it uses `Bun.Transpiler.scanImports` instead — the same
|
|
22
|
-
// parser-backed scan gdgraph itself uses. That also gets the semantics right:
|
|
23
|
-
// the transpiler drops type-only imports, and a type-only import genuinely does
|
|
24
|
-
// not need its file present at runtime.
|
|
25
|
-
import { describe, expect, test } from "bun:test";
|
|
26
|
-
import { readFileSync } from "node:fs";
|
|
27
|
-
import path from "node:path";
|
|
28
|
-
import { GDGRAPH_CORE_SOURCES } from "./core-sources";
|
|
29
|
-
|
|
30
|
-
const GDGRAPH_DIR = import.meta.dir;
|
|
31
|
-
const transpiler = new Bun.Transpiler({ loader: "ts" });
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Local imports of one module that must resolve for it to LOAD.
|
|
35
|
-
*
|
|
36
|
-
* Two exclusions, both deliberate rather than convenient:
|
|
37
|
-
*
|
|
38
|
-
* * type-only imports never reach runtime — the transpiler drops them, and
|
|
39
|
-
* `scanImports` does not report them;
|
|
40
|
-
* * a `dynamic-import` is a deliberate lazy edge. `build.ts` reaches `enrich`
|
|
41
|
-
* that way precisely so it can run in an environment that lacks it —
|
|
42
|
-
* `enrich.ts:9` names "the copied `.metaproject/core/gdgraph`" as that
|
|
43
|
-
* environment. Requiring it would copy the whole optional-capability tree
|
|
44
|
-
* into every scaffolded project and invert the design.
|
|
45
|
-
*
|
|
46
|
-
* So the closure is over `import-statement` only. That is the set whose absence
|
|
47
|
-
* is a crash at load, which is the failure this test exists to prevent.
|
|
48
|
-
*/
|
|
49
|
-
function runtimeImportsOf(basename: string): string[] {
|
|
50
|
-
const source = readFileSync(path.join(GDGRAPH_DIR, basename), "utf8");
|
|
51
|
-
return transpiler
|
|
52
|
-
.scanImports(source)
|
|
53
|
-
.filter((entry) => entry.kind === "import-statement" && entry.path.startsWith("./"))
|
|
54
|
-
.map((entry) => `${entry.path.slice(2).replace(/\.(ts|js)$/, "")}.ts`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Every module reachable from `entries` through runtime local imports. */
|
|
58
|
-
function importClosure(entries: readonly string[]): Set<string> {
|
|
59
|
-
const seen = new Set<string>();
|
|
60
|
-
const queue = [...entries];
|
|
61
|
-
while (queue.length > 0) {
|
|
62
|
-
const current = queue.shift()!;
|
|
63
|
-
if (seen.has(current)) continue;
|
|
64
|
-
seen.add(current);
|
|
65
|
-
for (const next of runtimeImportsOf(current)) queue.push(next);
|
|
66
|
-
}
|
|
67
|
-
return seen;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe("the copied gdgraph core is import-closed", () => {
|
|
71
|
-
// `cli.ts` is rendered rather than copied, and it drives these two.
|
|
72
|
-
const ENTRY_POINTS = ["build.ts", "query.ts"] as const;
|
|
73
|
-
|
|
74
|
-
test("every module the entry points reach at runtime is copied", () => {
|
|
75
|
-
const needed = importClosure(ENTRY_POINTS);
|
|
76
|
-
const copied = new Set(GDGRAPH_CORE_SOURCES);
|
|
77
|
-
const missing = [...needed].filter((f) => !copied.has(f)).sort();
|
|
78
|
-
// A failure here means a fresh `keryx init` followed by `keryx gdgraph
|
|
79
|
-
// build` dies on a missing module. Add the file to GDGRAPH_CORE_SOURCES.
|
|
80
|
-
expect(missing).toEqual([]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("every file in the copy list exists", () => {
|
|
84
|
-
for (const file of GDGRAPH_CORE_SOURCES) {
|
|
85
|
-
expect(() => readFileSync(path.join(GDGRAPH_DIR, file), "utf8")).not.toThrow();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("the closure is non-trivial, so a passing test is not vacuous", () => {
|
|
90
|
-
// If `scanImports` ever silently returned nothing, the first assertion
|
|
91
|
-
// would pass against an empty set. Pin the floor, and pin the specific file
|
|
92
|
-
// whose absence caused the outage.
|
|
93
|
-
const needed = importClosure(ENTRY_POINTS);
|
|
94
|
-
expect(needed.size).toBeGreaterThanOrEqual(3);
|
|
95
|
-
expect(needed.has("build.ts")).toBe(true);
|
|
96
|
-
expect(needed.has("query.ts")).toBe(true);
|
|
97
|
-
expect(needed.has("target.ts")).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile, stat } from "node:fs/promises";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import net from "node:net";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { expect, test } from "bun:test";
|
|
7
|
-
import { buildGraph } from "./build";
|
|
8
|
-
import { computeAffected } from "./affected";
|
|
9
|
-
import { loadGdgraphConfig } from "./config";
|
|
10
|
-
import { computeRepomap } from "./repomap";
|
|
11
|
-
import { loadGraph } from "./query";
|
|
12
|
-
import { resetWarnOnce, hasWarned } from "../capability/warn-once";
|
|
13
|
-
|
|
14
|
-
const ARTIFACTS = [
|
|
15
|
-
"storage/nodes.jsonl",
|
|
16
|
-
"storage/edges.jsonl",
|
|
17
|
-
"artifacts/module-map.json",
|
|
18
|
-
"artifacts/summary.md",
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
async function makeProject(): Promise<string> {
|
|
22
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-fallback-"));
|
|
23
|
-
await mkdir(path.join(root, "src", "feature"), { recursive: true });
|
|
24
|
-
await writeFile(
|
|
25
|
-
path.join(root, "src", "index.ts"),
|
|
26
|
-
"import { value } from './feature/value';\nexport const app = value;\n",
|
|
27
|
-
);
|
|
28
|
-
await writeFile(path.join(root, "src", "feature", "value.ts"), "export const value = 1;\n");
|
|
29
|
-
return root;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function snapshot(root: string): Promise<Record<string, string>> {
|
|
33
|
-
const out: Record<string, string> = {};
|
|
34
|
-
for (const rel of ARTIFACTS) {
|
|
35
|
-
out[rel] = await readFile(
|
|
36
|
-
path.join(root, ".metaproject", "data", "gdgraph", rel),
|
|
37
|
-
"utf8",
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
test("AC4.1/AC4.2 — capability OFF: 4 legacy artifacts deterministic; no symbol files written", async () => {
|
|
44
|
-
// Two fresh, identical projects, each built ONCE (a rebuild in the same dir is
|
|
45
|
-
// pre-existingly non-deterministic because the build creates `.metaproject`,
|
|
46
|
-
// which then appears in the summary's skipped-dirs — unrelated to this block).
|
|
47
|
-
// Comparing two first-builds isolates the capability-off artifacts precisely.
|
|
48
|
-
const a = await makeProject();
|
|
49
|
-
const b = await makeProject();
|
|
50
|
-
try {
|
|
51
|
-
await buildGraph(a);
|
|
52
|
-
await buildGraph(b);
|
|
53
|
-
expect(await snapshot(b)).toEqual(await snapshot(a));
|
|
54
|
-
|
|
55
|
-
for (const root of [a, b]) {
|
|
56
|
-
const storage = path.join(root, ".metaproject", "data", "gdgraph", "storage");
|
|
57
|
-
expect(existsSync(path.join(storage, "symbols.jsonl"))).toBe(false);
|
|
58
|
-
expect(existsSync(path.join(storage, "calls.jsonl"))).toBe(false);
|
|
59
|
-
}
|
|
60
|
-
} finally {
|
|
61
|
-
await rm(a, { recursive: true, force: true });
|
|
62
|
-
await rm(b, { recursive: true, force: true });
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("AC4.4 — no-network sandbox: build/affected/repomap open zero sockets", async () => {
|
|
67
|
-
const root = await makeProject();
|
|
68
|
-
const originalConnect = net.Socket.prototype.connect;
|
|
69
|
-
const originalFetch = globalThis.fetch;
|
|
70
|
-
let socketAttempts = 0;
|
|
71
|
-
let fetchAttempts = 0;
|
|
72
|
-
|
|
73
|
-
net.Socket.prototype.connect = function connect(this: net.Socket) {
|
|
74
|
-
socketAttempts += 1;
|
|
75
|
-
// Never actually connect.
|
|
76
|
-
return this;
|
|
77
|
-
};
|
|
78
|
-
// @ts-expect-error — deliberately override for the sandbox assertion.
|
|
79
|
-
globalThis.fetch = async () => {
|
|
80
|
-
fetchAttempts += 1;
|
|
81
|
-
throw new Error("network blocked in sandbox");
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
await buildGraph(root);
|
|
86
|
-
const config = await loadGdgraphConfig(root);
|
|
87
|
-
const graph = await loadGraph(root);
|
|
88
|
-
computeAffected(graph, "src/feature/value.ts", { depth: 3 });
|
|
89
|
-
computeRepomap(graph, config, {});
|
|
90
|
-
expect(socketAttempts).toBe(0);
|
|
91
|
-
expect(fetchAttempts).toBe(0);
|
|
92
|
-
} finally {
|
|
93
|
-
net.Socket.prototype.connect = originalConnect;
|
|
94
|
-
globalThis.fetch = originalFetch;
|
|
95
|
-
await rm(root, { recursive: true, force: true });
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("AC4.3 — capability ENABLED but unavailable: exactly one warn, regex path, exit 0, no symbol files", async () => {
|
|
100
|
-
const root = await makeProject();
|
|
101
|
-
resetWarnOnce();
|
|
102
|
-
try {
|
|
103
|
-
// Enable the ceiling in the manifest but provide NO resolvable grammar and
|
|
104
|
-
// no installed dep ⇒ resolveCapability degrades with a single warn-once.
|
|
105
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
106
|
-
await writeFile(
|
|
107
|
-
path.join(root, ".metaproject", "metaproject.json"),
|
|
108
|
-
JSON.stringify({
|
|
109
|
-
modules: {
|
|
110
|
-
gdgraph: {
|
|
111
|
-
enabled: true,
|
|
112
|
-
capabilities: [
|
|
113
|
-
{ id: "gdgraph.treesitter", enabled: true, kind: "ceiling", optionalDependency: "web-tree-sitter" },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
let stderr = "";
|
|
121
|
-
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
122
|
-
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
123
|
-
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
|
|
124
|
-
return true;
|
|
125
|
-
}) as typeof process.stderr.write;
|
|
126
|
-
|
|
127
|
-
const exitCodeBefore = process.exitCode;
|
|
128
|
-
try {
|
|
129
|
-
await buildGraph(root);
|
|
130
|
-
} finally {
|
|
131
|
-
process.stderr.write = originalWrite;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Build did not hard-fail (exitCode untouched by build).
|
|
135
|
-
expect(process.exitCode).toBe(exitCodeBefore);
|
|
136
|
-
// Exactly one capability degradation warning was emitted.
|
|
137
|
-
const warnings = stderr.split("\n").filter((line) => line.includes("gdgraph.treesitter unavailable"));
|
|
138
|
-
expect(warnings).toHaveLength(1);
|
|
139
|
-
expect(hasWarned("gdgraph.treesitter")).toBe(true);
|
|
140
|
-
|
|
141
|
-
// No symbol files were written (regex/scan path only).
|
|
142
|
-
const storage = path.join(root, ".metaproject", "data", "gdgraph", "storage");
|
|
143
|
-
expect(existsSync(path.join(storage, "symbols.jsonl"))).toBe(false);
|
|
144
|
-
// The 4 legacy artifacts were still produced.
|
|
145
|
-
for (const rel of ARTIFACTS) {
|
|
146
|
-
const info = await stat(path.join(root, ".metaproject", "data", "gdgraph", rel));
|
|
147
|
-
expect(info.isFile()).toBe(true);
|
|
148
|
-
}
|
|
149
|
-
} finally {
|
|
150
|
-
resetWarnOnce();
|
|
151
|
-
await rm(root, { recursive: true, force: true });
|
|
152
|
-
}
|
|
153
|
-
});
|
package/src/gdgraph/find.test.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { findNodes, findSymbols } from "./find";
|
|
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
|
-
|
|
9
|
-
function edge(from: string, to: string) {
|
|
10
|
-
return { id: `${from}->${to}`, from, to, kind: "imports" as const, specifier: to };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const GRAPH: GraphData = {
|
|
14
|
-
nodes: [
|
|
15
|
-
fileNode("src/pipelines/utils/clone-pipeline.ts"),
|
|
16
|
-
fileNode("src/pipelines/features/pipeline-variables/pipeline-variables-store.ts"),
|
|
17
|
-
fileNode("src/pipelines/api/pipeline-api.ts"),
|
|
18
|
-
fileNode("src/case/store/case-store.ts"),
|
|
19
|
-
],
|
|
20
|
-
edges: [
|
|
21
|
-
edge("src/case/store/case-store.ts", "src/pipelines/api/pipeline-api.ts"),
|
|
22
|
-
edge("src/pipelines/utils/clone-pipeline.ts", "src/pipelines/api/pipeline-api.ts"),
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
test("finds files by concept and ranks by matched terms", () => {
|
|
27
|
-
const results = findNodes(GRAPH, "pipeline clone");
|
|
28
|
-
expect(results[0]?.path).toBe("src/pipelines/utils/clone-pipeline.ts");
|
|
29
|
-
expect(results[0]?.matched.sort()).toEqual(["clone", "pipeline"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("ranks pipeline-variables highest for that query", () => {
|
|
33
|
-
const results = findNodes(GRAPH, "pipeline variables store");
|
|
34
|
-
expect(results[0]?.path).toContain("pipeline-variables-store.ts");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("drops stopwords and short tokens from the query", () => {
|
|
38
|
-
const results = findNodes(GRAPH, "the clone of a pipeline");
|
|
39
|
-
expect(results[0]?.path).toBe("src/pipelines/utils/clone-pipeline.ts");
|
|
40
|
-
// "the", "of", "a" must not have matched anything on their own
|
|
41
|
-
expect(results.every((r) => !r.matched.includes("the"))).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("returns empty for a query that matches no path", () => {
|
|
45
|
-
expect(findNodes(GRAPH, "kubernetes helm chart")).toEqual([]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("fan-in breaks ties (more-depended-on file ranks higher)", () => {
|
|
49
|
-
// both match "pipeline api" equally; pipeline-api has 2 dependents
|
|
50
|
-
const results = findNodes(GRAPH, "pipeline");
|
|
51
|
-
const apiIdx = results.findIndex((r) => r.path.endsWith("pipeline-api.ts"));
|
|
52
|
-
expect(results[apiIdx]?.dependents).toBe(2);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
function sym(id: string, name: string, path: string, line: number): SymbolNode {
|
|
56
|
-
return { id, kind: "function", path, name, container: null, startLine: line, endLine: line + 2, language: "typescript" };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const SYMBOL_GRAPH: GraphData = {
|
|
60
|
-
nodes: [],
|
|
61
|
-
edges: [],
|
|
62
|
-
symbols: [
|
|
63
|
-
sym("a#clonePipeline", "clonePipeline", "src/a.ts", 12),
|
|
64
|
-
sym("b#clonePipelineDeep", "clonePipelineDeep", "src/b.ts", 30),
|
|
65
|
-
sym("c#unrelated", "unrelated", "src/c.ts", 1),
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
test("findSymbols ranks an exact name match above a substring match", () => {
|
|
70
|
-
const results = findSymbols(SYMBOL_GRAPH, "clonePipeline");
|
|
71
|
-
expect(results[0]?.name).toBe("clonePipeline"); // exact-name bonus wins
|
|
72
|
-
expect(results.map((r) => r.name)).toContain("clonePipelineDeep");
|
|
73
|
-
expect(results.every((r) => r.name !== "unrelated")).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("findSymbols returns [] when the symbol layer is absent", () => {
|
|
77
|
-
expect(findSymbols({ nodes: [], edges: [] }, "clonePipeline")).toEqual([]);
|
|
78
|
-
});
|