@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.
- package/README.md +1 -1
- package/dist/cli.js +29861 -20525
- package/dist/core.js +25871 -0
- package/package.json +16 -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 +50 -13
- 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/gdskills/bundled/skills/review/review-pr-feedback/SKILL.md +12 -0
- 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,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
|
-
});
|