@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,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
- });
@@ -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
- });