@remnic/coding-graph 9.3.759

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 (86) hide show
  1. package/README.md +130 -0
  2. package/dist/chunk-5I2DBHOQ.js +1042 -0
  3. package/dist/chunk-5I2DBHOQ.js.map +1 -0
  4. package/dist/chunk-CPYJACC5.js +1838 -0
  5. package/dist/chunk-CPYJACC5.js.map +1 -0
  6. package/dist/chunk-ZVCMIM4T.js +216 -0
  7. package/dist/chunk-ZVCMIM4T.js.map +1 -0
  8. package/dist/cypher/query-parser.d.ts +253 -0
  9. package/dist/cypher/query-parser.js +17 -0
  10. package/dist/cypher/query-parser.js.map +1 -0
  11. package/dist/graph-schema.d.ts +84 -0
  12. package/dist/graph-schema.js +17 -0
  13. package/dist/graph-schema.js.map +1 -0
  14. package/dist/graph-store.d.ts +938 -0
  15. package/dist/graph-store.js +16 -0
  16. package/dist/graph-store.js.map +1 -0
  17. package/dist/index.d.ts +1953 -0
  18. package/dist/index.js +3509 -0
  19. package/dist/index.js.map +1 -0
  20. package/grammars/tree-sitter-bash.wasm +0 -0
  21. package/grammars/tree-sitter-c.wasm +0 -0
  22. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  23. package/grammars/tree-sitter-cpp.wasm +0 -0
  24. package/grammars/tree-sitter-go.wasm +0 -0
  25. package/grammars/tree-sitter-java.wasm +0 -0
  26. package/grammars/tree-sitter-javascript.wasm +0 -0
  27. package/grammars/tree-sitter-kotlin.wasm +0 -0
  28. package/grammars/tree-sitter-php.wasm +0 -0
  29. package/grammars/tree-sitter-python.wasm +0 -0
  30. package/grammars/tree-sitter-ruby.wasm +0 -0
  31. package/grammars/tree-sitter-rust.wasm +0 -0
  32. package/grammars/tree-sitter-swift.wasm +0 -0
  33. package/grammars/tree-sitter-tsx.wasm +0 -0
  34. package/grammars/tree-sitter-typescript.wasm +0 -0
  35. package/package.json +79 -0
  36. package/src/co-change.test.ts +175 -0
  37. package/src/co-change.ts +167 -0
  38. package/src/cypher/query-parser.test.ts +1107 -0
  39. package/src/cypher/query-parser.ts +1692 -0
  40. package/src/detect-changes.test.ts +533 -0
  41. package/src/detect-changes.ts +367 -0
  42. package/src/engine/emit.ts +556 -0
  43. package/src/engine/engine.test.ts +1417 -0
  44. package/src/engine/engine.ts +182 -0
  45. package/src/engine/extractors.ts +486 -0
  46. package/src/engine/fixtures.ts +364 -0
  47. package/src/engine/language-sniff.ts +56 -0
  48. package/src/engine/parser-backend.ts +206 -0
  49. package/src/engine/utf16-offsets.ts +68 -0
  50. package/src/git-invoker.test.ts +116 -0
  51. package/src/git-invoker.ts +426 -0
  52. package/src/graph-schema.test.ts +541 -0
  53. package/src/graph-schema.ts +383 -0
  54. package/src/graph-store-pr2.test.ts +1879 -0
  55. package/src/graph-store.test.ts +1420 -0
  56. package/src/graph-store.ts +3489 -0
  57. package/src/index-status.test.ts +303 -0
  58. package/src/index-status.ts +135 -0
  59. package/src/index.ts +384 -0
  60. package/src/lsp/byte-position.ts +173 -0
  61. package/src/lsp/characterization.test.ts +174 -0
  62. package/src/lsp/client.test.ts +275 -0
  63. package/src/lsp/client.ts +484 -0
  64. package/src/lsp/config.ts +219 -0
  65. package/src/lsp/degradation.ts +86 -0
  66. package/src/lsp/fixtures/fake-server.mjs +198 -0
  67. package/src/lsp/framing.test.ts +180 -0
  68. package/src/lsp/framing.ts +177 -0
  69. package/src/lsp/resolution.test.ts +497 -0
  70. package/src/lsp/resolution.ts +483 -0
  71. package/src/lsp/status.ts +140 -0
  72. package/src/lsp/types.ts +167 -0
  73. package/src/reindex.test.ts +1038 -0
  74. package/src/reindex.ts +908 -0
  75. package/src/row-types.ts +45 -0
  76. package/src/semantic/canonical-text.test.ts +150 -0
  77. package/src/semantic/canonical-text.ts +219 -0
  78. package/src/semantic/config.ts +235 -0
  79. package/src/semantic/index.ts +78 -0
  80. package/src/semantic/minhash.test.ts +197 -0
  81. package/src/semantic/minhash.ts +261 -0
  82. package/src/semantic/semantic-query.ts +173 -0
  83. package/src/semantic/semantic.test.ts +1315 -0
  84. package/src/semantic/similarity.ts +268 -0
  85. package/src/semantic/types.ts +145 -0
  86. package/src/semantic/vectors.ts +235 -0
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@remnic/coding-graph",
3
+ "version": "9.3.759",
4
+ "description": "Web-tree-sitter powered symbol-extraction engine + SQLite knowledge-graph store for codebase memory (Tier 1: TypeScript, TSX, JavaScript, Python, Go, Rust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Bash). Optional companion of @remnic/core — install only when coding-graph features are needed.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "remnic-source": "./src/index.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./graph-schema": {
15
+ "types": "./dist/graph-schema.d.ts",
16
+ "remnic-source": "./src/graph-schema.ts",
17
+ "import": "./dist/graph-schema.js"
18
+ },
19
+ "./graph-store": {
20
+ "types": "./dist/graph-store.d.ts",
21
+ "remnic-source": "./src/graph-store.ts",
22
+ "import": "./dist/graph-store.js"
23
+ },
24
+ "./cypher": {
25
+ "types": "./dist/cypher/query-parser.d.ts",
26
+ "remnic-source": "./src/cypher/query-parser.ts",
27
+ "import": "./dist/cypher/query-parser.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "src",
32
+ "dist",
33
+ "grammars"
34
+ ],
35
+ "scripts": {
36
+ "prebuild": "node ../../scripts/ensure-bench-build-deps.mjs",
37
+ "build": "tsup --config tsup.config.ts",
38
+ "precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
39
+ "check-types": "tsc --noEmit",
40
+ "test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test 'src/**/*.test.ts'",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true
46
+ },
47
+ "dependencies": {
48
+ "better-sqlite3": "^12.6.2",
49
+ "web-tree-sitter": "^0.25.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@remnic/core": "^9.3.759"
53
+ },
54
+ "devDependencies": {
55
+ "@remnic/core": "workspace:*",
56
+ "@types/node": "^22.10.0",
57
+ "tsup": "^8.5.1",
58
+ "tsx": "^4.19.0",
59
+ "typescript": "^5.7.0"
60
+ },
61
+ "license": "MIT",
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/joshuaswarren/remnic.git",
65
+ "directory": "packages/coding-graph"
66
+ },
67
+ "keywords": [
68
+ "remnic",
69
+ "memory",
70
+ "codebase",
71
+ "coding-graph",
72
+ "knowledge-graph",
73
+ "sqlite",
74
+ "tree-sitter",
75
+ "wasm",
76
+ "symbols",
77
+ "extraction"
78
+ ]
79
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Co-change mining tests (issue #1553).
3
+ *
4
+ * mineCoChangeEdges: pure function over commit→files entries.
5
+ * Tests: support/confidence thresholds, idempotency, byte-stability.
6
+ */
7
+ import assert from "node:assert/strict";
8
+ import { mkdtemp, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import path from "node:path";
11
+ import test from "node:test";
12
+
13
+ import { GraphStore } from "./graph-store.js";
14
+ import {
15
+ mineCoChangeEdges,
16
+ mineAndStoreCoChanges,
17
+ DEFAULT_CO_CHANGE_CONFIG,
18
+ type CoChangeConfig,
19
+ } from "./co-change.js";
20
+ import type { CodingGitInvoker, LogFilesEntry } from "./git-invoker.js";
21
+
22
+ // ──────────────────────────────────────────────────────────────────────────
23
+ // Pure mining tests
24
+ // ──────────────────────────────────────────────────────────────────────────
25
+
26
+ const COMMIT_A = "a".repeat(40);
27
+ const COMMIT_B = "b".repeat(40);
28
+ const COMMIT_C = "c".repeat(40);
29
+ const COMMIT_D = "d".repeat(40);
30
+
31
+ test("mineCoChangeEdges: basic — two files co-changed 3 times", () => {
32
+ const entries: LogFilesEntry[] = [
33
+ { sha: COMMIT_A, files: ["src/a.ts", "src/b.ts"] },
34
+ { sha: COMMIT_B, files: ["src/a.ts", "src/b.ts"] },
35
+ { sha: COMMIT_C, files: ["src/a.ts", "src/b.ts"] },
36
+ ];
37
+ const edges = mineCoChangeEdges(entries, {
38
+ maxCommits: 500,
39
+ minSupport: 3,
40
+ minConfidence: 0.3,
41
+ });
42
+ assert.equal(edges.length, 1);
43
+ assert.equal(edges[0]!.fileA, "src/a.ts");
44
+ assert.equal(edges[0]!.fileB, "src/b.ts");
45
+ assert.equal(edges[0]!.support, 3);
46
+ assert.equal(edges[0]!.confidence, 1.0); // 3/3 = 1.0
47
+ });
48
+
49
+ test("mineCoChangeEdges: below support threshold → no edge", () => {
50
+ const entries: LogFilesEntry[] = [
51
+ { sha: COMMIT_A, files: ["src/a.ts", "src/b.ts"] },
52
+ { sha: COMMIT_B, files: ["src/a.ts", "src/b.ts"] },
53
+ ];
54
+ const edges = mineCoChangeEdges(entries, DEFAULT_CO_CHANGE_CONFIG);
55
+ // support=2 < minSupport=3 → no edge.
56
+ assert.equal(edges.length, 0);
57
+ });
58
+
59
+ test("mineCoChangeEdges: below confidence threshold → no edge", () => {
60
+ // a.ts changed 10 times, b.ts changed 10 times, co-changed 3 times.
61
+ // confidence = 3/10 = 0.3 — exactly at threshold (minConfidence=0.3).
62
+ // With minConfidence=0.31 → no edge.
63
+ const entries: LogFilesEntry[] = [
64
+ { sha: COMMIT_A, files: ["src/a.ts", "src/b.ts"] },
65
+ { sha: COMMIT_B, files: ["src/a.ts", "src/b.ts"] },
66
+ { sha: COMMIT_C, files: ["src/a.ts", "src/b.ts"] },
67
+ // a.ts changed alone 7 more times.
68
+ ...Array.from({ length: 7 }, (_, i) => ({
69
+ sha: `${i}${"x".repeat(39)}`,
70
+ files: ["src/a.ts"],
71
+ })),
72
+ // b.ts changed alone 7 more times.
73
+ ...Array.from({ length: 7 }, (_, i) => ({
74
+ sha: `${i + 7}${"y".repeat(39)}`,
75
+ files: ["src/b.ts"],
76
+ })),
77
+ ];
78
+ const config: CoChangeConfig = {
79
+ maxCommits: 500,
80
+ minSupport: 3,
81
+ minConfidence: 0.31,
82
+ };
83
+ const edges = mineCoChangeEdges(entries, config);
84
+ assert.equal(edges.length, 0);
85
+ });
86
+
87
+ test("mineCoChangeEdges: idempotent — same input, same output", () => {
88
+ const entries: LogFilesEntry[] = [
89
+ { sha: COMMIT_A, files: ["src/a.ts", "src/b.ts", "src/c.ts"] },
90
+ { sha: COMMIT_B, files: ["src/a.ts", "src/b.ts"] },
91
+ { sha: COMMIT_C, files: ["src/b.ts", "src/c.ts"] },
92
+ { sha: COMMIT_D, files: ["src/a.ts", "src/c.ts"] },
93
+ ];
94
+ const e1 = mineCoChangeEdges(entries, DEFAULT_CO_CHANGE_CONFIG);
95
+ const e2 = mineCoChangeEdges(entries, DEFAULT_CO_CHANGE_CONFIG);
96
+ assert.deepEqual(e1, e2);
97
+ });
98
+
99
+ test("mineCoChangeEdges: byte-stable sort order", () => {
100
+ const entries: LogFilesEntry[] = [
101
+ { sha: COMMIT_A, files: ["src/z.ts", "src/a.ts"] },
102
+ { sha: COMMIT_B, files: ["src/z.ts", "src/a.ts"] },
103
+ { sha: COMMIT_C, files: ["src/z.ts", "src/a.ts"] },
104
+ ];
105
+ const edges = mineCoChangeEdges(entries, DEFAULT_CO_CHANGE_CONFIG);
106
+ // Sorted by fileA then fileB — a.ts before z.ts.
107
+ assert.equal(edges[0]!.fileA, "src/a.ts");
108
+ assert.equal(edges[0]!.fileB, "src/z.ts");
109
+ });
110
+
111
+ test("mineCoChangeEdges: empty input → empty output", () => {
112
+ const edges = mineCoChangeEdges([], DEFAULT_CO_CHANGE_CONFIG);
113
+ assert.equal(edges.length, 0);
114
+ });
115
+
116
+ // ──────────────────────────────────────────────────────────────────────────
117
+ // mineAndStoreCoChanges — integration with GraphStore
118
+ // ──────────────────────────────────────────────────────────────────────────
119
+
120
+ function mockGitForLog(entries: LogFilesEntry[]): CodingGitInvoker {
121
+ return {
122
+ revParseHead: () => ({ ok: true, head: COMMIT_A }),
123
+ isReachable: () => ({ ok: true, reachable: true }),
124
+ diffNameStatus: () => ({ ok: true, entries: [] }),
125
+ diffHunks: () => ({ ok: true, hunks: [] }),
126
+ logFiles: () => ({ ok: true, entries }),
127
+ listTrackedFiles: () => ({ ok: true, paths: [] }), };
128
+ }
129
+
130
+ async function tempStore(): Promise<{ store: GraphStore; dir: string }> {
131
+ const dir = await mkdtemp(path.join(tmpdir(), "cochange-test-"));
132
+ const store = await GraphStore.open({
133
+ dbPath: path.join(dir, "graph.sqlite"),
134
+ repoRoot: dir,
135
+ });
136
+ return { store, dir };
137
+ }
138
+
139
+ async function dispose(store: GraphStore, dir: string): Promise<void> {
140
+ await store.close();
141
+ await rm(dir, { recursive: true, force: true });
142
+ }
143
+
144
+ test("mineAndStoreCoChanges: stores edges + idempotent re-run", async () => {
145
+ const { store, dir } = await tempStore();
146
+ try {
147
+ const entries: LogFilesEntry[] = [
148
+ { sha: COMMIT_A, files: ["src/a.ts", "src/b.ts"] },
149
+ { sha: COMMIT_B, files: ["src/a.ts", "src/b.ts"] },
150
+ { sha: COMMIT_C, files: ["src/a.ts", "src/b.ts"] },
151
+ ];
152
+ const git = mockGitForLog(entries);
153
+
154
+ const r1 = await mineAndStoreCoChanges({ store, git, repoRoot: dir });
155
+ assert.equal(r1.ok, true);
156
+ if (!r1.ok) return;
157
+ assert.equal(r1.edges.length, 1);
158
+
159
+ // Verify edges persisted.
160
+ const coForA = store.readCoChanges("src/a.ts");
161
+ assert.equal(coForA.ok, true);
162
+ if (!coForA.ok) return;
163
+ assert.equal(coForA.edges.length, 1);
164
+ assert.equal(coForA.edges[0]!.fileB, "src/b.ts");
165
+
166
+ // Re-run: idempotent (clear + repopulate).
167
+ const r2 = await mineAndStoreCoChanges({ store, git, repoRoot: dir });
168
+ assert.equal(r2.ok, true);
169
+ const coForA2 = store.readCoChanges("src/a.ts");
170
+ assert.equal(coForA2.ok, true);
171
+ assert.equal(coForA2.ok ? coForA2.edges.length : -1, 1);
172
+ } finally {
173
+ await dispose(store, dir);
174
+ }
175
+ });
@@ -0,0 +1,167 @@
1
+ /**
2
+ * FILE_CHANGES_WITH co-change edge mining (issue #1553).
3
+ *
4
+ * Mines co-change relationships from `git log --name-only` over a bounded
5
+ * window (default 500 commits). An edge between two files is written when:
6
+ * - support (co-change count) ≥ `minSupport`
7
+ * - confidence (co-change / total changes of the less-changed file) ≥
8
+ * `minConfidence`
9
+ *
10
+ * Deterministic given the same history — re-running on unchanged history
11
+ * is idempotent (same edges, same confidence).
12
+ *
13
+ * Co-change edges are stored in a dedicated `co_changes` table (file-level,
14
+ * not symbol-level — the existing `edges` table is between symbol nodes).
15
+ * The table is additive to schema v1 (CREATE TABLE IF NOT EXISTS — same
16
+ * pattern as `node_attributes` in PR2).
17
+ */
18
+ import type { CodingGitInvoker, GitFailure, LogFilesEntry } from "./git-invoker.js";
19
+ import type { GraphStore } from "./graph-store.js";
20
+
21
+ // ──────────────────────────────────────────────────────────────────────────
22
+ // Public types
23
+ // ──────────────────────────────────────────────────────────────────────────
24
+
25
+ /** A mined co-change edge between two files. */
26
+ export interface CoChangeEdge {
27
+ readonly fileA: string;
28
+ readonly fileB: string;
29
+ /** Number of commits where both files changed together. */
30
+ readonly support: number;
31
+ /** Confidence: support / min(totalChangesA, totalChangesB). */
32
+ readonly confidence: number;
33
+ }
34
+
35
+ /** Result of co-change mining. */
36
+ export type MineCoChangesResult =
37
+ | { readonly ok: true; readonly edges: readonly CoChangeEdge[] }
38
+ | GitFailure
39
+ | { readonly ok: false; readonly code: "store_closed" | "db_error" };
40
+
41
+ /** Configuration for co-change mining. */
42
+ export interface CoChangeConfig {
43
+ /** Maximum commits to scan (default 500). */
44
+ readonly maxCommits: number;
45
+ /** Minimum co-change count to write an edge (default 3). */
46
+ readonly minSupport: number;
47
+ /** Minimum confidence to write an edge (default 0.3). */
48
+ readonly minConfidence: number;
49
+ }
50
+
51
+ export const DEFAULT_CO_CHANGE_CONFIG: CoChangeConfig = {
52
+ maxCommits: 500,
53
+ minSupport: 3,
54
+ minConfidence: 0.3,
55
+ };
56
+
57
+ // ──────────────────────────────────────────────────────────────────────────
58
+ // Pure mining logic — unit-testable without git or SQLite
59
+ // ──────────────────────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Compute co-change edges from a list of commit→files entries.
63
+ *
64
+ * Algorithm:
65
+ * 1. Count total changes per file (how many commits touched it).
66
+ * 2. For each unordered file pair, count how many commits changed both.
67
+ * 3. Confidence = coChangeCount / min(totalA, totalB).
68
+ * 4. Keep edges where support ≥ minSupport AND confidence ≥ minConfidence.
69
+ *
70
+ * Deterministic: the same input always produces the same output, sorted
71
+ * by (fileA, fileB) for byte-stability.
72
+ *
73
+ * Half-open time windows (rule 35): each commit is a discrete point; a
74
+ * pair is "co-changed" in a commit iff both files are in that commit's
75
+ * file list. No off-by-one — a file that appears once in a commit counts
76
+ * once (deduped by the git-invoker's parser).
77
+ */
78
+ export function mineCoChangeEdges(
79
+ entries: readonly LogFilesEntry[],
80
+ config: CoChangeConfig = DEFAULT_CO_CHANGE_CONFIG,
81
+ ): CoChangeEdge[] {
82
+ // Step 1: total changes per file.
83
+ const totalChanges = new Map<string, number>();
84
+ for (const entry of entries) {
85
+ for (const file of entry.files) {
86
+ totalChanges.set(file, (totalChanges.get(file) ?? 0) + 1);
87
+ }
88
+ }
89
+
90
+ // Step 2: co-change counts per unordered pair.
91
+ // Key: "fileA\x00fileB" where fileA < fileB lexicographically.
92
+ const coChangeCounts = new Map<string, { a: string; b: string; count: number }>();
93
+ for (const entry of entries) {
94
+ const files = entry.files;
95
+ // Only consider pairs within the same commit.
96
+ for (let i = 0; i < files.length; i += 1) {
97
+ for (let j = i + 1; j < files.length; j += 1) {
98
+ const [a, b] = files[i]! < files[j]! ? [files[i]!, files[j]!] : [files[j]!, files[i]!];
99
+ const key = `${a}\u0000${b}`;
100
+ const existing = coChangeCounts.get(key);
101
+ if (existing) {
102
+ existing.count += 1;
103
+ } else {
104
+ coChangeCounts.set(key, { a, b, count: 1 });
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // Step 3+4: filter by support + confidence thresholds.
111
+ const edges: CoChangeEdge[] = [];
112
+ for (const { a, b, count } of coChangeCounts.values()) {
113
+ if (count < config.minSupport) continue;
114
+ const totalA = totalChanges.get(a) ?? 0;
115
+ const totalB = totalChanges.get(b) ?? 0;
116
+ const minTotal = Math.min(totalA, totalB);
117
+ if (minTotal === 0) continue;
118
+ const confidence = count / minTotal;
119
+ if (confidence < config.minConfidence) continue;
120
+ edges.push({ fileA: a, fileB: b, support: count, confidence });
121
+ }
122
+
123
+ // Sort for byte-stability (deterministic output).
124
+ edges.sort((x, y) => {
125
+ const cmp = x.fileA.localeCompare(y.fileA);
126
+ if (cmp !== 0) return cmp;
127
+ return x.fileB.localeCompare(y.fileB);
128
+ });
129
+
130
+ return edges;
131
+ }
132
+
133
+ // ──────────────────────────────────────────────────────────────────────────
134
+ // Executor — fetch git log, mine, persist to store
135
+ // ──────────────────────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Mine co-change edges from git history and persist them to the store's
139
+ * `co_changes` table. Idempotent: re-running on unchanged history produces
140
+ * the same edges (the table is cleared + repopulated each run, so stale
141
+ * edges from history changes are pruned automatically).
142
+ */
143
+ export async function mineAndStoreCoChanges(options: {
144
+ readonly store: GraphStore;
145
+ readonly git: CodingGitInvoker;
146
+ readonly repoRoot: string;
147
+ readonly config?: CoChangeConfig;
148
+ }): Promise<MineCoChangesResult> {
149
+ const { store, git, repoRoot } = options;
150
+ const config = options.config ?? DEFAULT_CO_CHANGE_CONFIG;
151
+
152
+ const logResult = git.logFiles(repoRoot, config.maxCommits);
153
+ if (!logResult.ok) return logResult;
154
+
155
+ const edges = mineCoChangeEdges(logResult.entries, config);
156
+
157
+ // Persist: clear + repopulate in one transaction. Propagate a
158
+ // store_closed failure so the caller does not believe mining
159
+ // succeeded while nothing was persisted (cursor Bugbot: 'Co-change
160
+ // store reports false success').
161
+ const persistResult = await store.upsertCoChanges(edges);
162
+ if (!persistResult.ok) {
163
+ return { ok: false, code: persistResult.code };
164
+ }
165
+
166
+ return { ok: true, edges };
167
+ }