@mrciphersmith/keryx 0.2.83 → 0.2.88

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +29861 -20525
  3. package/dist/core.js +25871 -0
  4. package/package.json +16 -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 +50 -13
  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/gdskills/bundled/skills/review/review-pr-feedback/SKILL.md +12 -0
  28. package/src/gdgraph/affected.test.ts +0 -133
  29. package/src/gdgraph/build-integrity.test.ts +0 -193
  30. package/src/gdgraph/build-lang.test.ts +0 -406
  31. package/src/gdgraph/build.test.ts +0 -120
  32. package/src/gdgraph/config.test.ts +0 -47
  33. package/src/gdgraph/core-sources.test.ts +0 -99
  34. package/src/gdgraph/fallback.test.ts +0 -153
  35. package/src/gdgraph/find.test.ts +0 -78
  36. package/src/gdgraph/import-kind.test.ts +0 -525
  37. package/src/gdgraph/path.test.ts +0 -56
  38. package/src/gdgraph/repomap.test.ts +0 -110
  39. package/src/gdgraph/service.test.ts +0 -89
  40. package/src/gdgraph/staleness.test.ts +0 -208
  41. package/src/gdgraph/symbol.test.ts +0 -89
  42. package/src/gdgraph/symbols-capability.test.ts +0 -138
  43. package/src/gdgraph/treesitter/adapter.test.ts +0 -496
  44. package/src/gdgraph/treesitter/extract.test.ts +0 -278
  45. package/src/gdgraph/treesitter/no-treesitter-import.test.ts +0 -51
  46. package/src/gdgraph/treesitter/real-grammar-fixture.test.ts +0 -71
  47. package/src/gdgraph/treesitter/resolve-calls.test.ts +0 -38
  48. package/src/gdgraph/wiki-layer-no-git.test.ts +0 -73
  49. package/src/gdgraph/wiki-layer.test.ts +0 -211
@@ -1,110 +0,0 @@
1
- import { mkdtemp, readFile, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { expect, test } from "bun:test";
6
- import { DEFAULT_GDGRAPH_CONFIG, mergeGdgraphConfig } from "./config";
7
- import { personalizedPageRank } from "./pagerank";
8
- import { buildRankEdges, computeRepomap, estimateTokens, writeRepomap } from "./repomap";
9
- import type { GraphData } from "./types";
10
-
11
- const FIXTURE_DIR = fileURLToPath(new URL("../../fixtures/repomap/", import.meta.url));
12
-
13
- async function loadFixtureGraph(): Promise<GraphData> {
14
- const raw = JSON.parse(await readFile(path.join(FIXTURE_DIR, "graph.json"), "utf8"));
15
- return { nodes: raw.nodes, edges: raw.edges };
16
- }
17
-
18
- async function loadExpected(): Promise<any> {
19
- return JSON.parse(await readFile(path.join(FIXTURE_DIR, "expected.json"), "utf8"));
20
- }
21
-
22
- test("AC3.3 — top-ranked entries match expected centrality ordering", async () => {
23
- const graph = await loadFixtureGraph();
24
- const expected = await loadExpected();
25
- const nodes = graph.nodes.filter((node) => node.kind === "file").map((node) => node.path);
26
- const edges = buildRankEdges(graph, DEFAULT_GDGRAPH_CONFIG);
27
- const ranked = personalizedPageRank(nodes, edges, {
28
- damping: DEFAULT_GDGRAPH_CONFIG.repomap.damping,
29
- iterations: DEFAULT_GDGRAPH_CONFIG.repomap.iterations,
30
- tolerance: DEFAULT_GDGRAPH_CONFIG.repomap.tolerance,
31
- });
32
- expect(ranked.map((entry) => entry.id)).toEqual(expected.centralityOrder);
33
- });
34
-
35
- test("AC3.2 — repomap fits the token budget with a stable omission marker", async () => {
36
- const graph = await loadFixtureGraph();
37
- const expected = await loadExpected();
38
- const config = mergeGdgraphConfig({ repomap: { tokenBudget: expected.tokenBudget } });
39
- const result = computeRepomap(graph, config, {});
40
-
41
- expect(result.tokens).toBeLessThanOrEqual(expected.tokenBudget);
42
- expect(estimateTokens(result.content)).toBeLessThanOrEqual(expected.tokenBudget);
43
- expect(result.entries.map((entry) => entry.path)).toEqual(expected.topWithinBudget);
44
- expect(result.omitted).toBe(expected.omitted);
45
- expect(result.content).toContain(`… ${expected.omitted} entries omitted …`);
46
- });
47
-
48
- test("AC3.2 — --budget override is honored and bounds the output", async () => {
49
- const graph = await loadFixtureGraph();
50
- const result = computeRepomap(graph, DEFAULT_GDGRAPH_CONFIG, { budget: 30 });
51
- expect(result.tokens).toBeLessThanOrEqual(30);
52
- expect(result.omitted).toBeGreaterThan(0);
53
- });
54
-
55
- test("AC3.4 — re-running repomap yields a byte-identical file", async () => {
56
- const graph = await loadFixtureGraph();
57
- const root = await mkdtemp(path.join(tmpdir(), "keryx-repomap-"));
58
- try {
59
- const config = mergeGdgraphConfig({ repomap: { tokenBudget: 40 } });
60
- const first = await writeRepomap(root, graph, config, {});
61
- const firstBytes = await readFile(first.path, "utf8");
62
- const second = await writeRepomap(root, graph, config, {});
63
- const secondBytes = await readFile(second.path, "utf8");
64
- expect(secondBytes).toBe(firstBytes);
65
- } finally {
66
- await rm(root, { recursive: true, force: true });
67
- }
68
- });
69
-
70
- test("AC3.6 — --seed biases personalization; seeded/unseeded each reproducible", async () => {
71
- const graph = await loadFixtureGraph();
72
- const unseeded = computeRepomap(graph, DEFAULT_GDGRAPH_CONFIG, {});
73
- const seeded = computeRepomap(graph, DEFAULT_GDGRAPH_CONFIG, { seed: ["src/a.ts"] });
74
- const seededAgain = computeRepomap(graph, DEFAULT_GDGRAPH_CONFIG, { seed: ["src/a.ts"] });
75
-
76
- // Each run reproducible.
77
- expect(seeded.content).toBe(seededAgain.content);
78
- // Seeding on a.ts lifts it above its unseeded rank position.
79
- const unseededRank = unseeded.entries.findIndex((entry) => entry.path === "src/a.ts");
80
- const seededRank = seeded.entries.findIndex((entry) => entry.path === "src/a.ts");
81
- expect(seededRank).toBeGreaterThanOrEqual(0);
82
- expect(seededRank).toBeLessThan(unseededRank === -1 ? Number.MAX_SAFE_INTEGER : unseededRank + 1);
83
- });
84
-
85
- test("AC3.5 — repomap ranks symbol-aware weights when the layer is present", async () => {
86
- const graph = await loadFixtureGraph();
87
- // Add a tiny symbol layer so buildRankEdges exercises calls/defines weights.
88
- const withSymbols: GraphData = {
89
- ...graph,
90
- symbols: [
91
- {
92
- id: "src/core.ts#run",
93
- kind: "function",
94
- path: "src/core.ts",
95
- name: "run",
96
- container: null,
97
- startLine: 1,
98
- endLine: 2,
99
- language: "typescript",
100
- signature: "run()",
101
- },
102
- ],
103
- calls: [
104
- { id: "d", from: "src/core.ts", to: "src/core.ts#run", kind: "defines", resolved: true },
105
- ],
106
- };
107
- const result = computeRepomap(withSymbols, DEFAULT_GDGRAPH_CONFIG, { budget: 400 });
108
- const core = result.entries.find((entry) => entry.path === "src/core.ts");
109
- expect(core?.symbols).toEqual(["run()"]);
110
- });
@@ -1,89 +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 { createGdgraphService, UnknownGraphTargetError } from "./service";
6
-
7
- async function makeProject(): Promise<string> {
8
- const root = await mkdtemp(path.join(tmpdir(), "keryx-svc-"));
9
- await mkdir(path.join(root, "src"), { recursive: true });
10
- await writeFile(path.join(root, "src", "a.ts"), "export const a = 1;\n");
11
- await writeFile(path.join(root, "src", "b.ts"), "import { a } from './a';\nexport const b = a;\n");
12
- await writeFile(path.join(root, "src", "c.ts"), "import { b } from './b';\nexport const c = b;\n");
13
- return root;
14
- }
15
-
16
- test("AC5.3/T-1 — service.affected is a pure in-process method (transport-independent)", async () => {
17
- const root = await makeProject();
18
- try {
19
- const service = createGdgraphService();
20
- await service.build(root);
21
- // depth 2 closure of a: b (hop1) + c (hop2).
22
- const affected = await service.affected(root, "src/a.ts", { depth: 2 });
23
- expect(affected.dependents).toEqual(["src/b.ts", "src/c.ts"]);
24
- // default depth (config) = 1 ⇒ only direct dependents.
25
- const shallow = await service.affected(root, "src/a.ts");
26
- expect(shallow.dependents).toEqual(["src/b.ts"]);
27
- } finally {
28
- await rm(root, { recursive: true, force: true });
29
- }
30
- });
31
-
32
- test("AC5.3/T-1 — service.repomap writes artifacts/repomap.md deterministically", async () => {
33
- const root = await makeProject();
34
- try {
35
- const service = createGdgraphService();
36
- await service.build(root);
37
- const first = await service.repomap(root, { budget: 2000 });
38
- expect(first.path).toContain(path.join("artifacts", "repomap.md"));
39
- expect(first.tokens).toBeLessThanOrEqual(2000);
40
- const second = await service.repomap(root, { budget: 2000 });
41
- expect(second.content).toBe(first.content);
42
- } finally {
43
- await rm(root, { recursive: true, force: true });
44
- }
45
- });
46
-
47
- // AFC-10 (flow 234, phase 2): before this task, `service.affected` for a
48
- // target the graph never indexed and a target the graph indexed but found no
49
- // edges for returned byte-identical shapes — `{dependencies: [], dependents:
50
- // [], ranked: []}` — differing only in the echoed-back `target` string.
51
- // Measured directly against this exact fixture: an unknown path
52
- // ("src/does-not-exist.ts") and a real, indexed, edge-less leaf file
53
- // produced the same `{dependencies, dependents, ranked}`. That is the same
54
- // defect class phase 1 closed at eight other sites — a failure rendered as a
55
- // legitimate empty success — so an unknown target must now be reported
56
- // distinguishably instead of silently resolving to an empty result.
57
- test("AFC-10 — service.affected on a target the graph never indexed throws UnknownGraphTargetError, distinct from an indexed-but-edgeless target", async () => {
58
- const root = await makeProject();
59
- try {
60
- await writeFile(path.join(root, "src", "isolated.ts"), "export const iso = 1;\n");
61
- const service = createGdgraphService();
62
- await service.build(root);
63
-
64
- // Indexed, legitimately no edges at all: a real success, not an error.
65
- const indexedNoEdges = await service.affected(root, "src/isolated.ts");
66
- expect(indexedNoEdges.dependencies).toEqual([]);
67
- expect(indexedNoEdges.dependents).toEqual([]);
68
-
69
- // Never indexed: must be distinguishable from the above, not the same
70
- // `{dependencies: [], dependents: []}` shape under a different `target`.
71
- await expect(service.affected(root, "src/does-not-exist.ts")).rejects.toThrow(UnknownGraphTargetError);
72
- } finally {
73
- await rm(root, { recursive: true, force: true });
74
- }
75
- });
76
-
77
- test("service.query returns cycles + orphans over the built graph", async () => {
78
- const root = await makeProject();
79
- try {
80
- const service = createGdgraphService();
81
- await service.build(root);
82
- const cycles = await service.query(root, "cycles");
83
- expect(Array.isArray(cycles)).toBe(true);
84
- const orphans = await service.query(root, "orphans");
85
- expect(Array.isArray(orphans)).toBe(true);
86
- } finally {
87
- await rm(root, { recursive: true, force: true });
88
- }
89
- });
@@ -1,208 +0,0 @@
1
- // AFC-10 (flow 234, phase 2): the graph "staleness" signal must actually track
2
- // the five triggers the frozen AC3 names — new commit, untracked, delete,
3
- // rename, config change — and a git failure must never collapse into "fresh".
4
- //
5
- // Before this task, `graphMaybeStale` compared `.git/HEAD`'s own mtime to
6
- // `nodes.jsonl`'s. That misses every trigger here: `.git/HEAD` is a symbolic
7
- // ref ("ref: refs/heads/<branch>") whose CONTENT (and often mtime) does not
8
- // change on an ordinary commit to the current branch, and it never reflects
9
- // untracked/deleted/renamed working-tree state or a config-file edit at all.
10
- // These tests build a real git fixture per trigger (never the project repo
11
- // itself) and assert against `checkGraphStaleness`'s structured result.
12
-
13
- import { execFileSync } from "node:child_process";
14
- import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
15
- import { tmpdir } from "node:os";
16
- import path from "node:path";
17
- import { afterEach, beforeEach, expect, test } from "bun:test";
18
- import { checkGraphStaleness, graphMaybeStale } from "./staleness";
19
- import { recordProvenance } from "../sync/provenance";
20
-
21
- let root: string;
22
-
23
- function git(cwd: string, args: string[]): void {
24
- execFileSync("git", args, { cwd, stdio: "ignore" });
25
- }
26
-
27
- /**
28
- * A minimal built-graph fixture: a real git repo with one committed source
29
- * file, a `nodes.jsonl` "graph storage" file, and recorded build provenance
30
- * pointing at the current HEAD — i.e. the state right after a clean
31
- * `keryx gdgraph build` with nothing having moved since.
32
- */
33
- async function makeBuiltFixture(): Promise<string> {
34
- const dir = await mkdtemp(path.join(tmpdir(), "keryx-staleness-"));
35
- git(dir, ["init", "-q"]);
36
- git(dir, ["config", "user.email", "test@test.com"]);
37
- git(dir, ["config", "user.name", "test"]);
38
- await mkdir(path.join(dir, "src"), { recursive: true });
39
- await mkdir(path.join(dir, ".metaproject", "data", "gdgraph", "storage"), { recursive: true });
40
- await writeFile(path.join(dir, "src", "a.ts"), "export const a = 1;\n");
41
- await writeFile(
42
- path.join(dir, ".metaproject", "data", "gdgraph", "storage", "nodes.jsonl"),
43
- '{"id":"src/a.ts","kind":"file","path":"src/a.ts","language":"typescript"}\n',
44
- );
45
- git(dir, ["add", "-A"]);
46
- git(dir, ["commit", "-q", "-m", "initial build fixture"]);
47
- // `recordProvenance` reads `git rev-parse HEAD`, which fails with zero
48
- // commits — it must run AFTER the first commit exists, or it silently
49
- // no-ops (by design: "no-op outside a git repo", which a HEAD-less repo
50
- // with no commits yet also satisfies). It is deliberately left UNCOMMITTED
51
- // here, matching the real project's own state (`.provenance.json` sits
52
- // modified/untracked in the working tree right after every build) — the
53
- // staleness check must treat that as normal build residue, not a trigger.
54
- await recordProvenance(dir, "gdgraph", new Date().toISOString());
55
- // Ensure nodes.jsonl's mtime is not accidentally newer than a config file
56
- // written a moment later in the same test (mtime resolution on some
57
- // filesystems is coarse) by nudging it slightly into the past.
58
- const past = new Date(Date.now() - 5000);
59
- await utimes(path.join(dir, ".metaproject", "data", "gdgraph", "storage", "nodes.jsonl"), past, past);
60
- return dir;
61
- }
62
-
63
- beforeEach(async () => {
64
- root = await makeBuiltFixture();
65
- });
66
-
67
- afterEach(async () => {
68
- await rm(root, { recursive: true, force: true });
69
- });
70
-
71
- test("AFC-10 control — a clean tree right after build reports fresh", async () => {
72
- const result = await checkGraphStaleness(root);
73
- expect(result.status).toBe("fresh");
74
- expect(await graphMaybeStale(root)).toBe(false);
75
- });
76
-
77
- test("AFC-10 trigger 1/5 — a new commit since the build invalidates the snapshot", async () => {
78
- await writeFile(path.join(root, "src", "b.ts"), "export const b = 1;\n");
79
- git(root, ["add", "-A"]);
80
- git(root, ["commit", "-q", "-m", "a new commit after the graph was built"]);
81
-
82
- const result = await checkGraphStaleness(root);
83
- expect(result.status).not.toBe("fresh");
84
- expect(await graphMaybeStale(root)).toBe(true);
85
- });
86
-
87
- test("AFC-10 trigger 2/5 — an untracked file invalidates the snapshot", async () => {
88
- await writeFile(path.join(root, "src", "untracked.ts"), "export const u = 1;\n");
89
-
90
- const result = await checkGraphStaleness(root);
91
- expect(result.status).not.toBe("fresh");
92
- expect(await graphMaybeStale(root)).toBe(true);
93
- });
94
-
95
- test("AFC-10 trigger 3/5 — a deleted tracked file invalidates the snapshot", async () => {
96
- await rm(path.join(root, "src", "a.ts"));
97
-
98
- const result = await checkGraphStaleness(root);
99
- expect(result.status).not.toBe("fresh");
100
- expect(await graphMaybeStale(root)).toBe(true);
101
- });
102
-
103
- test("AFC-10 trigger 4/5 — a rename invalidates the snapshot even though file count and content are unchanged", async () => {
104
- // The subtle case: `git mv` keeps the file count and byte content identical
105
- // to the fixture's committed state — only the path moved.
106
- git(root, ["mv", "src/a.ts", "src/a-renamed.ts"]);
107
-
108
- const result = await checkGraphStaleness(root);
109
- expect(result.status).not.toBe("fresh");
110
- expect(await graphMaybeStale(root)).toBe(true);
111
- });
112
-
113
- test("AFC-10 trigger 5/5 — a gdgraph.config.json change invalidates the snapshot", async () => {
114
- await writeFile(
115
- path.join(root, ".metaproject", "gdgraph.config.json"),
116
- JSON.stringify({ affected: { defaultDepth: 2 } }) + "\n",
117
- );
118
-
119
- const result = await checkGraphStaleness(root);
120
- expect(result.status).not.toBe("fresh");
121
- expect(await graphMaybeStale(root)).toBe(true);
122
- });
123
-
124
- test("AFC-10 group 3 — a git failure is reported as unknown, never as fresh", async () => {
125
- // Delete the .git directory entirely so every git invocation the check
126
- // makes fails (spawn succeeds, git exits non-zero / "not a git repository").
127
- await rm(path.join(root, ".git"), { recursive: true, force: true });
128
-
129
- const result = await checkGraphStaleness(root);
130
- expect(result.status).toBe("unknown");
131
- expect(result.reasons.length).toBeGreaterThan(0);
132
- // The boolean back-compat wrapper must still never read as "fresh" (false)
133
- // on a git failure — "unknown" collapses to "treat as maybe-stale", not to
134
- // the safe-looking "false" the old mtime-diff check silently returned.
135
- expect(await graphMaybeStale(root)).toBe(true);
136
- });
137
-
138
- test("AFC-10 — graph never built at all is reported as stale (not fresh), not a git failure", async () => {
139
- await rm(path.join(root, ".metaproject", "data", "gdgraph", "storage", "nodes.jsonl"));
140
-
141
- const result = await checkGraphStaleness(root);
142
- expect(result.status).toBe("stale");
143
- expect(await graphMaybeStale(root)).toBe(true);
144
- });
145
-
146
- // ---------------------------------------------------------------------------
147
- // T19 finding 3 (flow 234 review) — `git status --porcelain` prints paths
148
- // relative to the REPOSITORY ROOT, not to the directory git was invoked in.
149
- // The `.metaproject/` exclusion in `categorizeStatusLines` compared a bare
150
- // `.metaproject/` prefix against those repo-root-relative paths, which only
151
- // ever matches when the project root IS the git root. In a monorepo where the
152
- // project root sits below the git root, the prefix never matches, so the
153
- // graph's own build residue (`.provenance.json`, `artifacts/*` — expected to
154
- // sit modified/untracked right after every build) reads as a real untracked
155
- // file and the graph reports stale immediately after every clean build.
156
- // ---------------------------------------------------------------------------
157
-
158
- test("AFC-10 / T19 finding 3 — at the git root, the .metaproject exclusion behaves exactly as before", async () => {
159
- // `root` (from `makeBuiltFixture`) already IS the git root — this is the
160
- // control case the fix must not change. `--show-prefix` there resolves to
161
- // "" and the fix must produce the identical "fresh" result as pre-fix.
162
- const result = await checkGraphStaleness(root);
163
- expect(result.status).toBe("fresh");
164
- });
165
-
166
- test("T19 finding 3 — a project root below the git root still excludes its own .metaproject build residue", async () => {
167
- const gitRoot = await mkdtemp(path.join(tmpdir(), "keryx-staleness-monorepo-"));
168
- try {
169
- git(gitRoot, ["init", "-q"]);
170
- git(gitRoot, ["config", "user.email", "test@test.com"]);
171
- git(gitRoot, ["config", "user.name", "test"]);
172
-
173
- const projectDir = path.join(gitRoot, "packages", "proj");
174
- await mkdir(path.join(projectDir, "src"), { recursive: true });
175
- await mkdir(path.join(projectDir, ".metaproject", "data", "gdgraph", "storage"), { recursive: true });
176
- await writeFile(path.join(projectDir, "src", "a.ts"), "export const a = 1;\n");
177
- await writeFile(
178
- path.join(projectDir, ".metaproject", "data", "gdgraph", "storage", "nodes.jsonl"),
179
- '{"id":"src/a.ts","kind":"file","path":"src/a.ts","language":"typescript"}\n',
180
- );
181
- git(gitRoot, ["add", "-A"]);
182
- git(gitRoot, ["commit", "-q", "-m", "initial monorepo build fixture"]);
183
-
184
- // Mirrors `makeBuiltFixture`: `recordProvenance` leaves `.provenance.json`
185
- // UNCOMMITTED, exactly like a real `keryx gdgraph build` does — this is
186
- // the graph's own bookkeeping the exclusion exists to skip, and (per the
187
- // reviewer's repro) the ONLY dirty entry in the working tree here.
188
- await recordProvenance(projectDir, "gdgraph", new Date().toISOString());
189
- const past = new Date(Date.now() - 5000);
190
- await utimes(
191
- path.join(projectDir, ".metaproject", "data", "gdgraph", "storage", "nodes.jsonl"),
192
- past,
193
- past,
194
- );
195
-
196
- const result = await checkGraphStaleness(projectDir);
197
- // Before the fix: `git status --porcelain` (run with cwd=projectDir)
198
- // prints "packages/proj/.metaproject/data/gdgraph/.provenance.json",
199
- // which does not start with the bare ".metaproject/" prefix the old
200
- // check compared against — so it read as a real untracked file and this
201
- // was "stale" on a repo with nothing but the graph's own residue dirty.
202
- expect(result.status).toBe("fresh");
203
- expect(result.reasons).toEqual([]);
204
- expect(await graphMaybeStale(projectDir)).toBe(false);
205
- } finally {
206
- await rm(gitRoot, { recursive: true, force: true });
207
- }
208
- });
@@ -1,89 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import { querySymbol, resolveSymbols, transitiveCallers } from "./symbol";
3
- import type { GraphData, SymbolNode, CallEdge } from "./types";
4
-
5
- function sym(id: string, name: string, path: string, startLine: number): SymbolNode {
6
- return { id, kind: "function", path, name, container: null, startLine, endLine: startLine + 5, language: "typescript" };
7
- }
8
- function call(from: string, to: string, resolved: boolean): CallEdge {
9
- return { id: `${from}->${to}`, from, to, kind: resolved ? "calls" : "unresolved-call", resolved };
10
- }
11
-
12
- const A = sym("src/a.ts#foo", "foo", "src/a.ts", 10);
13
- const B = sym("src/b.ts#bar", "bar", "src/b.ts", 20);
14
- const C = sym("src/c.ts#Foo", "Foo", "src/c.ts", 5);
15
-
16
- const GRAPH: GraphData = {
17
- nodes: [],
18
- edges: [],
19
- symbols: [A, B, C],
20
- calls: [
21
- call("src/b.ts#bar", "src/a.ts#foo", true), // bar calls foo
22
- call("src/a.ts#foo", "src/c.ts#Foo", true), // foo calls Foo
23
- call("src/a.ts#foo", "someUnknown.method", false), // foo calls unresolved
24
- ],
25
- };
26
-
27
- test("resolveSymbols prefers exact name, then case-insensitive, then substring", () => {
28
- expect(resolveSymbols(GRAPH.symbols!, "foo").map((s) => s.id)).toEqual(["src/a.ts#foo"]);
29
- // case-insensitive tier only when no exact: "FOO" -> foo (exact 'foo'? no; ci matches foo AND Foo)
30
- const ci = resolveSymbols(GRAPH.symbols!, "FOO").map((s) => s.name).sort();
31
- expect(ci).toEqual(["Foo", "foo"]);
32
- // substring tier
33
- expect(resolveSymbols(GRAPH.symbols!, "ba").map((s) => s.name)).toEqual(["bar"]);
34
- });
35
-
36
- test("querySymbol returns definition, callers, and callees", () => {
37
- const r = querySymbol(GRAPH, "foo");
38
- expect(r.definitions.map((d) => d.id)).toEqual(["src/a.ts#foo"]);
39
- // bar calls foo -> caller
40
- expect(r.callers.map((c) => c.label)).toEqual(["bar (src/b.ts:20)"]);
41
- // foo calls Foo (resolved) + someUnknown.method (unresolved)
42
- const labels = r.callees.map((c) => c.label);
43
- expect(labels).toContain("Foo (src/c.ts:5)");
44
- expect(labels).toContain("someUnknown.method");
45
- const unresolved = r.callees.find((c) => c.label === "someUnknown.method");
46
- expect(unresolved?.resolved).toBe(false);
47
- });
48
-
49
- test("querySymbol on no match returns empty definitions", () => {
50
- expect(querySymbol(GRAPH, "nonexistent").definitions).toEqual([]);
51
- });
52
-
53
- test("querySymbol tolerates a graph with no symbol layer", () => {
54
- const r = querySymbol({ nodes: [], edges: [] }, "foo");
55
- expect(r.definitions).toEqual([]);
56
- expect(r.callers).toEqual([]);
57
- });
58
-
59
- test("transitiveCallers walks the reverse call graph with hop distances", () => {
60
- // chain: baz -> bar -> foo (bar calls foo, baz calls bar)
61
- const graph: GraphData = {
62
- nodes: [],
63
- edges: [],
64
- symbols: [A, B, sym("src/d.ts#baz", "baz", "src/d.ts", 40)],
65
- calls: [
66
- call("src/b.ts#bar", "src/a.ts#foo", true),
67
- call("src/d.ts#baz", "src/b.ts#bar", true),
68
- ],
69
- };
70
- const impact = transitiveCallers(graph, ["src/a.ts#foo"], 5);
71
- const byName = Object.fromEntries(impact.map((n) => [n.label.split(" ")[0], n.hop]));
72
- expect(byName["bar"]).toBe(1);
73
- expect(byName["baz"]).toBe(2);
74
- expect(impact.some((n) => n.label.startsWith("foo"))).toBe(false); // seed excluded
75
- });
76
-
77
- test("transitiveCallers respects maxDepth", () => {
78
- const graph: GraphData = {
79
- nodes: [],
80
- edges: [],
81
- symbols: [A, B, sym("src/d.ts#baz", "baz", "src/d.ts", 40)],
82
- calls: [
83
- call("src/b.ts#bar", "src/a.ts#foo", true),
84
- call("src/d.ts#baz", "src/b.ts#bar", true),
85
- ],
86
- };
87
- const impact = transitiveCallers(graph, ["src/a.ts#foo"], 1);
88
- expect(impact.map((n) => n.label.split(" ")[0])).toEqual(["bar"]); // baz is hop 2, excluded
89
- });
@@ -1,138 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import {
3
- isTreesitterEnabled,
4
- requireSymbols,
5
- setTreesitterEnabled,
6
- SymbolsUnavailableError,
7
- TREESITTER_CAPABILITY,
8
- type SymbolsLayerCarrier,
9
- } from "./symbols-capability";
10
- import type { GrammarDiagnosis } from "./treesitter/adapter";
11
-
12
- test("enable adds the capability entry to an empty manifest", () => {
13
- const next = setTreesitterEnabled({}, true);
14
- expect(isTreesitterEnabled(next)).toBe(true);
15
- const caps = (next.modules as any).gdgraph.capabilities;
16
- expect(caps).toHaveLength(1);
17
- expect(caps[0]).toMatchObject({ id: TREESITTER_CAPABILITY, enabled: true, kind: "ceiling" });
18
- });
19
-
20
- test("disable flips enabled to false", () => {
21
- const enabled = setTreesitterEnabled({}, true);
22
- const disabled = setTreesitterEnabled(enabled, false);
23
- expect(isTreesitterEnabled(disabled)).toBe(false);
24
- });
25
-
26
- test("is idempotent — never duplicates the capability entry", () => {
27
- let m: Record<string, unknown> = {};
28
- m = setTreesitterEnabled(m, true);
29
- m = setTreesitterEnabled(m, true);
30
- const caps = (m.modules as any).gdgraph.capabilities.filter(
31
- (c: any) => c.id === TREESITTER_CAPABILITY,
32
- );
33
- expect(caps).toHaveLength(1);
34
- });
35
-
36
- test("preserves other gdgraph capabilities and other modules", () => {
37
- const manifest = {
38
- modules: {
39
- gdgraph: { enabled: true, capabilities: [{ id: "gdgraph.other", enabled: true }] },
40
- security: { enabled: true },
41
- },
42
- schemaVersion: 1,
43
- };
44
- const next = setTreesitterEnabled(manifest, true) as any;
45
- const ids = next.modules.gdgraph.capabilities.map((c: any) => c.id).sort();
46
- expect(ids).toEqual(["gdgraph.other", "gdgraph.treesitter"]);
47
- expect(next.modules.security).toEqual({ enabled: true });
48
- expect(next.schemaVersion).toBe(1);
49
- });
50
-
51
- // --- AFC-13 / AC5 requirement 2: requireSymbols gives a typed, actionable
52
- // error — never undefined, never an empty result, never a generic Error. ---
53
-
54
- test("AC5.req2 — requireSymbols returns graph.symbols when the symbol layer is present (including an empty array — the layer RAN, it just found nothing)", () => {
55
- const withSymbols = { symbols: [{ id: "a" }] };
56
- expect(requireSymbols(withSymbols)).toEqual(withSymbols.symbols);
57
-
58
- const ranButEmpty = { symbols: [] };
59
- expect(requireSymbols(ranButEmpty)).toEqual([]);
60
- });
61
-
62
- test("AC5.req2 — requireSymbols throws SymbolsUnavailableError (not undefined, not an empty result, not a generic Error) when the symbol layer never ran", () => {
63
- // A `GraphData`-shaped object with no `symbols` key — exactly what the
64
- // graph looks like when the symbol layer never ran. The cast (rather than a
65
- // bare literal) is only to satisfy TS's weak-object-literal check, which
66
- // otherwise flags "no properties in common" for a fresh literal compared
67
- // against a type with a single optional property; a real `GraphData` value
68
- // (a named type, not a fresh literal) never triggers this.
69
- const graphWithNoLayer = { nodes: [], edges: [] } as unknown as SymbolsLayerCarrier;
70
- expect(() => requireSymbols(graphWithNoLayer)).toThrow(SymbolsUnavailableError);
71
-
72
- let caught: unknown;
73
- try {
74
- requireSymbols(graphWithNoLayer);
75
- } catch (err) {
76
- caught = err;
77
- }
78
- expect(caught).toBeInstanceOf(SymbolsUnavailableError);
79
- expect(caught).toBeInstanceOf(Error);
80
- const typed = caught as SymbolsUnavailableError;
81
- // Typed and actionable: a stable `code`, a non-empty `remedy`, a non-empty
82
- // `message` — not a bare string thrown, not `err.message === ""`.
83
- expect(typed.code).toBe("no-symbol-layer");
84
- expect(typeof typed.remedy).toBe("string");
85
- expect(typed.remedy.length).toBeGreaterThan(0);
86
- expect(typed.message.length).toBeGreaterThan(0);
87
- });
88
-
89
- // --- AFC-13 / AC5 requirement 3: missing vs incompatible must be
90
- // distinguishable, not collapsed into one outcome. ---
91
-
92
- test("AC5.req3 — requireSymbols reports grammar-missing when diagnoses say the grammar never resolved", () => {
93
- const diagnoses: GrammarDiagnosis[] = [
94
- { language: "typescript", status: "missing", reason: 'grammar asset "tree-sitter-typescript" is not resolved' },
95
- ];
96
- let caught: SymbolsUnavailableError | undefined;
97
- try {
98
- requireSymbols({}, diagnoses);
99
- } catch (err) {
100
- caught = err as SymbolsUnavailableError;
101
- }
102
- expect(caught?.code).toBe("grammar-missing");
103
- expect(caught?.message).toContain("typescript");
104
- });
105
-
106
- test("AC5.req3 — requireSymbols reports grammar-incompatible when diagnoses say the grammar resolved but failed to load (ABI mismatch), and this is a DIFFERENT code/message/remedy than grammar-missing", () => {
107
- const incompatible: GrammarDiagnosis[] = [
108
- {
109
- language: "typescript",
110
- status: "incompatible",
111
- reason: 'grammar at "/cache/tree-sitter-typescript" failed to load (likely an ABI/version mismatch): Incompatible language version 13. Expected minimum 14',
112
- },
113
- ];
114
- let caught: SymbolsUnavailableError | undefined;
115
- try {
116
- requireSymbols({}, incompatible);
117
- } catch (err) {
118
- caught = err as SymbolsUnavailableError;
119
- }
120
- expect(caught?.code).toBe("grammar-incompatible");
121
- expect(caught?.message).toContain("incompatible");
122
-
123
- // Same missing-grammar case as the previous test, compared directly here so
124
- // the distinction is asserted in one place: two different real-world
125
- // conditions must never collapse into the same code/message/remedy.
126
- const missing: GrammarDiagnosis[] = [
127
- { language: "typescript", status: "missing", reason: 'grammar asset "tree-sitter-typescript" is not resolved' },
128
- ];
129
- let missingCaught: SymbolsUnavailableError | undefined;
130
- try {
131
- requireSymbols({}, missing);
132
- } catch (err) {
133
- missingCaught = err as SymbolsUnavailableError;
134
- }
135
- expect(missingCaught?.code).not.toBe(caught?.code);
136
- expect(missingCaught?.message).not.toBe(caught?.message);
137
- expect(missingCaught?.remedy).not.toBe(caught?.remedy);
138
- });