@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.
- package/README.md +130 -0
- package/dist/chunk-5I2DBHOQ.js +1042 -0
- package/dist/chunk-5I2DBHOQ.js.map +1 -0
- package/dist/chunk-CPYJACC5.js +1838 -0
- package/dist/chunk-CPYJACC5.js.map +1 -0
- package/dist/chunk-ZVCMIM4T.js +216 -0
- package/dist/chunk-ZVCMIM4T.js.map +1 -0
- package/dist/cypher/query-parser.d.ts +253 -0
- package/dist/cypher/query-parser.js +17 -0
- package/dist/cypher/query-parser.js.map +1 -0
- package/dist/graph-schema.d.ts +84 -0
- package/dist/graph-schema.js +17 -0
- package/dist/graph-schema.js.map +1 -0
- package/dist/graph-store.d.ts +938 -0
- package/dist/graph-store.js +16 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/index.d.ts +1953 -0
- package/dist/index.js +3509 -0
- package/dist/index.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +79 -0
- package/src/co-change.test.ts +175 -0
- package/src/co-change.ts +167 -0
- package/src/cypher/query-parser.test.ts +1107 -0
- package/src/cypher/query-parser.ts +1692 -0
- package/src/detect-changes.test.ts +533 -0
- package/src/detect-changes.ts +367 -0
- package/src/engine/emit.ts +556 -0
- package/src/engine/engine.test.ts +1417 -0
- package/src/engine/engine.ts +182 -0
- package/src/engine/extractors.ts +486 -0
- package/src/engine/fixtures.ts +364 -0
- package/src/engine/language-sniff.ts +56 -0
- package/src/engine/parser-backend.ts +206 -0
- package/src/engine/utf16-offsets.ts +68 -0
- package/src/git-invoker.test.ts +116 -0
- package/src/git-invoker.ts +426 -0
- package/src/graph-schema.test.ts +541 -0
- package/src/graph-schema.ts +383 -0
- package/src/graph-store-pr2.test.ts +1879 -0
- package/src/graph-store.test.ts +1420 -0
- package/src/graph-store.ts +3489 -0
- package/src/index-status.test.ts +303 -0
- package/src/index-status.ts +135 -0
- package/src/index.ts +384 -0
- package/src/lsp/byte-position.ts +173 -0
- package/src/lsp/characterization.test.ts +174 -0
- package/src/lsp/client.test.ts +275 -0
- package/src/lsp/client.ts +484 -0
- package/src/lsp/config.ts +219 -0
- package/src/lsp/degradation.ts +86 -0
- package/src/lsp/fixtures/fake-server.mjs +198 -0
- package/src/lsp/framing.test.ts +180 -0
- package/src/lsp/framing.ts +177 -0
- package/src/lsp/resolution.test.ts +497 -0
- package/src/lsp/resolution.ts +483 -0
- package/src/lsp/status.ts +140 -0
- package/src/lsp/types.ts +167 -0
- package/src/reindex.test.ts +1038 -0
- package/src/reindex.ts +908 -0
- package/src/row-types.ts +45 -0
- package/src/semantic/canonical-text.test.ts +150 -0
- package/src/semantic/canonical-text.ts +219 -0
- package/src/semantic/config.ts +235 -0
- package/src/semantic/index.ts +78 -0
- package/src/semantic/minhash.test.ts +197 -0
- package/src/semantic/minhash.ts +261 -0
- package/src/semantic/semantic-query.ts +173 -0
- package/src/semantic/semantic.test.ts +1315 -0
- package/src/semantic/similarity.ts +268 -0
- package/src/semantic/types.ts +145 -0
- package/src/semantic/vectors.ts +235 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index-status + git-invoker parser tests (issue #1553).
|
|
3
|
+
*
|
|
4
|
+
* index_status: stale index with autoIndex:"manual" reports its staleness
|
|
5
|
+
* (last_indexed_head + dirty flag) via index_status rather than pretending
|
|
6
|
+
* freshness.
|
|
7
|
+
*
|
|
8
|
+
* git-invoker parsers: parseNameStatus, parseHunks, parseLogFiles — pure
|
|
9
|
+
* functions tested with synthetic output strings.
|
|
10
|
+
*/
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
|
|
18
|
+
import { GraphStore } from "./graph-store.js";
|
|
19
|
+
import { getIndexStatus } from "./index-status.js";
|
|
20
|
+
import { META_KEY_LAST_HEAD } from "./reindex.js";
|
|
21
|
+
import {
|
|
22
|
+
defaultCodingGitInvoker,
|
|
23
|
+
parseNameStatus,
|
|
24
|
+
parseHunks,
|
|
25
|
+
parseLogFiles,
|
|
26
|
+
type CodingGitInvoker,
|
|
27
|
+
} from "./git-invoker.js";
|
|
28
|
+
|
|
29
|
+
const SHA_A = "a".repeat(40);
|
|
30
|
+
const SHA_B = "b".repeat(40);
|
|
31
|
+
|
|
32
|
+
async function tempStore(): Promise<{ store: GraphStore; dir: string }> {
|
|
33
|
+
const dir = await mkdtemp(path.join(tmpdir(), "index-status-test-"));
|
|
34
|
+
const store = await GraphStore.open({
|
|
35
|
+
dbPath: path.join(dir, "graph.sqlite"),
|
|
36
|
+
repoRoot: dir,
|
|
37
|
+
});
|
|
38
|
+
return { store, dir };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function dispose(store: GraphStore, dir: string): Promise<void> {
|
|
42
|
+
await store.close();
|
|
43
|
+
await rm(dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mockGit(head: string | null): CodingGitInvoker {
|
|
47
|
+
return {
|
|
48
|
+
revParseHead: () => ({ ok: true, head }),
|
|
49
|
+
isReachable: () => ({ ok: true, reachable: true }),
|
|
50
|
+
diffNameStatus: () => ({ ok: true, entries: [] }),
|
|
51
|
+
diffHunks: () => ({ ok: true, hunks: [] }),
|
|
52
|
+
logFiles: () => ({ ok: true, entries: [] }),
|
|
53
|
+
listTrackedFiles: () => ({ ok: true, paths: [] }), };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
57
|
+
// index_status
|
|
58
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
test("index_status: empty store → mode 'empty'", async () => {
|
|
61
|
+
const { store, dir } = await tempStore();
|
|
62
|
+
try {
|
|
63
|
+
const status = getIndexStatus(store, mockGit(SHA_A), dir);
|
|
64
|
+
assert.equal(status.mode, "empty");
|
|
65
|
+
assert.equal(status.lastIndexedHead, null);
|
|
66
|
+
assert.equal(status.currentHead, SHA_A);
|
|
67
|
+
assert.equal(status.dirty, true);
|
|
68
|
+
} finally {
|
|
69
|
+
await dispose(store, dir);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("index_status: fresh index → mode 'fresh'", async () => {
|
|
74
|
+
const { store, dir } = await tempStore();
|
|
75
|
+
try {
|
|
76
|
+
store.writeMeta(META_KEY_LAST_HEAD, SHA_A);
|
|
77
|
+
const status = getIndexStatus(store, mockGit(SHA_A), dir);
|
|
78
|
+
assert.equal(status.mode, "fresh");
|
|
79
|
+
assert.equal(status.dirty, false);
|
|
80
|
+
} finally {
|
|
81
|
+
await dispose(store, dir);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("index_status: stale index → mode 'stale', dirty=true", async () => {
|
|
86
|
+
const { store, dir } = await tempStore();
|
|
87
|
+
try {
|
|
88
|
+
store.writeMeta(META_KEY_LAST_HEAD, SHA_A);
|
|
89
|
+
const status = getIndexStatus(store, mockGit(SHA_B), dir);
|
|
90
|
+
assert.equal(status.mode, "stale");
|
|
91
|
+
assert.equal(status.dirty, true);
|
|
92
|
+
assert.equal(status.lastIndexedHead, SHA_A);
|
|
93
|
+
assert.equal(status.currentHead, SHA_B);
|
|
94
|
+
} finally {
|
|
95
|
+
await dispose(store, dir);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("index_status: repo with no commits but stored head → stale, not fresh (cursor Bugbot: 'Index status false fresh')", async () => {
|
|
100
|
+
const { store, dir } = await tempStore();
|
|
101
|
+
try {
|
|
102
|
+
// The index was built against a real HEAD, but the repo now reports
|
|
103
|
+
// no commits (unborn/reset HEAD). The index cannot match a repo with
|
|
104
|
+
// no commits — it must read as stale, not fresh.
|
|
105
|
+
store.writeMeta(META_KEY_LAST_HEAD, SHA_A);
|
|
106
|
+
const status = getIndexStatus(store, mockGit(null), dir);
|
|
107
|
+
assert.equal(status.mode, "stale");
|
|
108
|
+
assert.equal(status.dirty, true);
|
|
109
|
+
assert.equal(status.lastIndexedHead, SHA_A);
|
|
110
|
+
assert.equal(status.currentHead, null);
|
|
111
|
+
} finally {
|
|
112
|
+
await dispose(store, dir);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
test("diffHunks: captures staged changes via git diff HEAD (chatgpt-codex-connector: 'Include staged hunks in diffHunks')", async () => {
|
|
118
|
+
const dir = await mkdtemp(path.join(tmpdir(), "git-staged-"));
|
|
119
|
+
try {
|
|
120
|
+
const exec = (cmd: string) =>
|
|
121
|
+
execSync(cmd, { cwd: dir, stdio: "ignore" });
|
|
122
|
+
exec("git init -q");
|
|
123
|
+
exec('git config user.email "t@t.t"');
|
|
124
|
+
exec('git config user.name "t"');
|
|
125
|
+
await import("node:fs/promises").then((fs) => fs.mkdir(path.join(dir, "src"), { recursive: true }));
|
|
126
|
+
await writeFile(path.join(dir, "src/a.ts"), "export function foo() {}\n");
|
|
127
|
+
exec("git add src/a.ts");
|
|
128
|
+
exec('git -c commit.gpgsign=false commit -q -m init');
|
|
129
|
+
// Stage a change (git add) — plain `git diff` would miss this.
|
|
130
|
+
await writeFile(path.join(dir, "src/a.ts"), "export function foo() { return 1; }\n");
|
|
131
|
+
exec("git add src/a.ts");
|
|
132
|
+
const invoker = defaultCodingGitInvoker();
|
|
133
|
+
const result = invoker.diffHunks(dir, ["src/a.ts"]);
|
|
134
|
+
assert.equal(result.ok, true);
|
|
135
|
+
if (!result.ok) return;
|
|
136
|
+
assert.ok(result.hunks.length > 0, "staged hunks must be captured (git diff HEAD)");
|
|
137
|
+
} finally {
|
|
138
|
+
await rm(dir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
143
|
+
// git-invoker parsers
|
|
144
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
test("parseNameStatus: basic add/modify/delete", () => {
|
|
147
|
+
const stdout = "A\tsrc/new.ts\nM\tsrc/changed.ts\nD\tsrc/gone.ts\n";
|
|
148
|
+
const entries = parseNameStatus(stdout);
|
|
149
|
+
assert.equal(entries.length, 3);
|
|
150
|
+
assert.equal(entries[0]!.status, "A");
|
|
151
|
+
assert.equal(entries[0]!.path, "src/new.ts");
|
|
152
|
+
assert.equal(entries[1]!.status, "M");
|
|
153
|
+
assert.equal(entries[1]!.path, "src/changed.ts");
|
|
154
|
+
assert.equal(entries[2]!.status, "D");
|
|
155
|
+
assert.equal(entries[2]!.path, "src/gone.ts");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("parseNameStatus: rename R100", () => {
|
|
159
|
+
const stdout = "R100\tsrc/old.ts\tsrc/new.ts\n";
|
|
160
|
+
const entries = parseNameStatus(stdout);
|
|
161
|
+
assert.equal(entries.length, 1);
|
|
162
|
+
assert.equal(entries[0]!.status, "R100");
|
|
163
|
+
assert.equal(entries[0]!.path, "src/new.ts");
|
|
164
|
+
assert.equal(entries[0]!.oldPath, "src/old.ts");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("parseNameStatus: empty output", () => {
|
|
168
|
+
assert.equal(parseNameStatus("").length, 0);
|
|
169
|
+
assert.equal(parseNameStatus("\n\n").length, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("parseHunks: basic hunk header", () => {
|
|
173
|
+
const stdout = [
|
|
174
|
+
"diff --git a/src/a.ts b/src/a.ts",
|
|
175
|
+
"+++ b/src/a.ts",
|
|
176
|
+
"@@ -5,3 +5,4 @@",
|
|
177
|
+
" context",
|
|
178
|
+
"+added line",
|
|
179
|
+
" context",
|
|
180
|
+
].join("\n");
|
|
181
|
+
const hunks = parseHunks(stdout);
|
|
182
|
+
assert.equal(hunks.length, 1);
|
|
183
|
+
assert.equal(hunks[0]!.path, "src/a.ts");
|
|
184
|
+
assert.equal(hunks[0]!.newRange.startLine, 5);
|
|
185
|
+
assert.equal(hunks[0]!.newRange.endLine, 9); // 5 + 4
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("parseHunks: single-line hunk (no count)", () => {
|
|
189
|
+
const stdout = [
|
|
190
|
+
"+++ b/src/b.ts",
|
|
191
|
+
"@@ -10 +10,1 @@",
|
|
192
|
+
].join("\n");
|
|
193
|
+
const hunks = parseHunks(stdout);
|
|
194
|
+
assert.equal(hunks.length, 1);
|
|
195
|
+
assert.equal(hunks[0]!.path, "src/b.ts");
|
|
196
|
+
assert.equal(hunks[0]!.newRange.startLine, 10);
|
|
197
|
+
assert.equal(hunks[0]!.newRange.endLine, 11);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("parseLogFiles: basic commits", () => {
|
|
201
|
+
const sha1 = "1".repeat(40);
|
|
202
|
+
const sha2 = "2".repeat(40);
|
|
203
|
+
const stdout = `${sha1}\nsrc/a.ts\nsrc/b.ts\n\n${sha2}\nsrc/c.ts\n`;
|
|
204
|
+
const entries = parseLogFiles(stdout);
|
|
205
|
+
assert.equal(entries.length, 2);
|
|
206
|
+
assert.equal(entries[0]!.sha, sha1);
|
|
207
|
+
assert.deepEqual(entries[0]!.files, ["src/a.ts", "src/b.ts"]);
|
|
208
|
+
assert.equal(entries[1]!.sha, sha2);
|
|
209
|
+
assert.deepEqual(entries[1]!.files, ["src/c.ts"]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("parseLogFiles: dedupes files within a commit", () => {
|
|
213
|
+
const sha = "f".repeat(40);
|
|
214
|
+
const stdout = `${sha}\nsrc/a.ts\nsrc/a.ts\nsrc/b.ts\n`;
|
|
215
|
+
const entries = parseLogFiles(stdout);
|
|
216
|
+
assert.equal(entries.length, 1);
|
|
217
|
+
assert.equal(entries[0]!.files.length, 2); // a.ts appears once after dedupe
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
222
|
+
// Round-2 review fixes — git-invoker parsers + index_status
|
|
223
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
test("parseLogFiles: real git layout (blank line after SHA) (chatgpt-codex-connector: 'Parse real git log records')", () => {
|
|
226
|
+
const sha1 = "1".repeat(40);
|
|
227
|
+
const sha2 = "2".repeat(40);
|
|
228
|
+
// Real git log --format=%H --name-only emits a blank line AFTER the
|
|
229
|
+
// SHA before the file names. The old blank-line block split put the SHA
|
|
230
|
+
// alone in one block (zero files) and the names in the next (failed).
|
|
231
|
+
const stdout = `${sha1}\n\nsrc/a.ts\nsrc/b.ts\n\n${sha2}\n\nsrc/c.ts\n`;
|
|
232
|
+
const entries = parseLogFiles(stdout);
|
|
233
|
+
assert.equal(entries.length, 2);
|
|
234
|
+
assert.equal(entries[0]!.sha, sha1);
|
|
235
|
+
assert.deepEqual(entries[0]!.files, ["src/a.ts", "src/b.ts"]);
|
|
236
|
+
assert.equal(entries[1]!.sha, sha2);
|
|
237
|
+
assert.deepEqual(entries[1]!.files, ["src/c.ts"]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("parseHunks: deletion-only hunk (zero new-count) covers the adjacent line (chatgpt-codex-connector: 'Map deletion-only hunks')", () => {
|
|
241
|
+
// `@@ -2 +1,0 @@` = delete old line 2; new side has 0 lines at line 1.
|
|
242
|
+
const stdout = ["+++ b/src/a.ts", "@@ -2 +1,0 @@"].join("\n");
|
|
243
|
+
const hunks = parseHunks(stdout);
|
|
244
|
+
assert.equal(hunks.length, 1);
|
|
245
|
+
assert.equal(hunks[0]!.newRange.startLine, 1);
|
|
246
|
+
// Must be a non-empty range so it can overlap a symbol span.
|
|
247
|
+
assert.ok(hunks[0]!.newRange.endLine > hunks[0]!.newRange.startLine);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("index_status: pending parse failures report stale even when heads match (chatgpt-codex-connector: 'Mark pending parse retries as stale')", async () => {
|
|
251
|
+
const { store, dir } = await tempStore();
|
|
252
|
+
try {
|
|
253
|
+
store.writeMeta(META_KEY_LAST_HEAD, SHA_A);
|
|
254
|
+
store.writeMeta("pending_parse_failures", JSON.stringify(["src/broken.ts"]));
|
|
255
|
+
const status = getIndexStatus(store, mockGit(SHA_A), dir);
|
|
256
|
+
assert.equal(status.mode, "stale");
|
|
257
|
+
assert.equal(status.dirty, true);
|
|
258
|
+
} finally {
|
|
259
|
+
await dispose(store, dir);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("revParseHead: non-git directory returns git_error, not head:null (chatgpt-codex-connector: 'Return a git failure for non-repository HEAD')", async () => {
|
|
264
|
+
const dir = await mkdtemp(path.join(tmpdir(), "notgit-"));
|
|
265
|
+
try {
|
|
266
|
+
const invoker = defaultCodingGitInvoker();
|
|
267
|
+
const result = invoker.revParseHead(dir);
|
|
268
|
+
assert.equal(result.ok, false);
|
|
269
|
+
if (result.ok) return;
|
|
270
|
+
assert.equal(result.code, "git_error");
|
|
271
|
+
} finally {
|
|
272
|
+
await rm(dir, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("parseHunks: deleted file (+++ /dev/null) does not leak its hunks to the previous file (chatgpt-codex-connector: 'Clear hunk path for deleted files')", () => {
|
|
277
|
+
// a.ts is modified, then b.ts is deleted (+++ /dev/null). A following
|
|
278
|
+
// @@ hunk for b.ts must NOT be attributed to a.ts.
|
|
279
|
+
const stdout = [
|
|
280
|
+
"diff --git a/src/a.ts b/src/a.ts",
|
|
281
|
+
"+++ b/src/a.ts",
|
|
282
|
+
"@@ -1 +1,2 @@",
|
|
283
|
+
"diff --git a/src/b.ts b/src/b.ts",
|
|
284
|
+
"deleted file mode 100644",
|
|
285
|
+
"--- a/src/b.ts",
|
|
286
|
+
"+++ /dev/null",
|
|
287
|
+
"@@ -1 +0,0 @@",
|
|
288
|
+
].join("\n");
|
|
289
|
+
const hunks = parseHunks(stdout);
|
|
290
|
+
// b.ts's deletion hunk followed `+++ /dev/null`, which clears
|
|
291
|
+
// currentPath → the @@ hunk is dropped entirely, NOT attributed to
|
|
292
|
+
// a.ts. Exactly one hunk survives: a.ts's own +1,2 hunk.
|
|
293
|
+
assert.equal(hunks.length, 1, "b.ts deletion hunk dropped after /dev/null");
|
|
294
|
+
assert.equal(hunks[0].path, "src/a.ts");
|
|
295
|
+
assert.equal(hunks[0].newRange.startLine, 1);
|
|
296
|
+
assert.equal(hunks[0].newRange.endLine, 3); // +1,2 → [1,3)
|
|
297
|
+
// Defensive: no surviving hunk may carry a path that leaked from a
|
|
298
|
+
// prior file across a /dev/null boundary.
|
|
299
|
+
assert.ok(
|
|
300
|
+
hunks.every((h) => h.path === "src/a.ts"),
|
|
301
|
+
"no hunk leaked a foreign path across /dev/null",
|
|
302
|
+
);
|
|
303
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index staleness reporting (issue #1553 done-when: "a deliberately stale
|
|
3
|
+
* index with `autoIndex: "manual"` reports its staleness via `index_status`
|
|
4
|
+
* rather than pretending freshness").
|
|
5
|
+
*
|
|
6
|
+
* Reports:
|
|
7
|
+
* - `lastIndexedHead` — the HEAD at the last successful reindex.
|
|
8
|
+
* - `currentHead` — the current repo HEAD (null when git unavailable).
|
|
9
|
+
* - `dirty` — true when `lastIndexedHead !== currentHead` (the index
|
|
10
|
+
* is behind the repo and needs a reindex).
|
|
11
|
+
* - `mode` — "fresh" | "stale" | "empty" | "git_unavailable".
|
|
12
|
+
*
|
|
13
|
+
* Never throws — a git failure degrades to `mode: "git_unavailable"`
|
|
14
|
+
* so callers (remnic doctor / xray) can surface it without crashing.
|
|
15
|
+
*/
|
|
16
|
+
import type { CodingGitInvoker } from "./git-invoker.js";
|
|
17
|
+
import type { GraphStore } from "./graph-store.js";
|
|
18
|
+
import { META_KEY_LAST_HEAD, META_KEY_PENDING_PARSE_FAILURES } from "./reindex.js";
|
|
19
|
+
|
|
20
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Public types
|
|
22
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export type IndexStatusMode =
|
|
25
|
+
| "fresh" // lastIndexedHead === currentHead
|
|
26
|
+
| "stale" // lastIndexedHead !== currentHead
|
|
27
|
+
| "empty" // no prior index (lastIndexedHead === null)
|
|
28
|
+
| "git_unavailable"; // git failed
|
|
29
|
+
|
|
30
|
+
export interface IndexStatus {
|
|
31
|
+
readonly lastIndexedHead: string | null;
|
|
32
|
+
readonly currentHead: string | null;
|
|
33
|
+
readonly dirty: boolean;
|
|
34
|
+
readonly mode: IndexStatusMode;
|
|
35
|
+
readonly fileCount: number;
|
|
36
|
+
readonly nodeCount: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Implementation
|
|
41
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Report the current index status. Pure over the store + git facts — no
|
|
45
|
+
* side effects, never throws.
|
|
46
|
+
*/
|
|
47
|
+
export function getIndexStatus(
|
|
48
|
+
store: GraphStore,
|
|
49
|
+
git: CodingGitInvoker,
|
|
50
|
+
repoRoot: string,
|
|
51
|
+
): IndexStatus {
|
|
52
|
+
// readMeta now returns a tagged result (rule 22). index_status is a
|
|
53
|
+
// best-effort, never-throws display feed (doctor/xray); the load-bearing
|
|
54
|
+
// reindex prune/head-advance path is independently hardened in reindex.ts.
|
|
55
|
+
// Degrade an unreadable head to null (reported as "empty"/needs-reindex)
|
|
56
|
+
// rather than crashing the display.
|
|
57
|
+
const lastHeadResult = store.readMeta(META_KEY_LAST_HEAD);
|
|
58
|
+
const lastIndexedHead = lastHeadResult.ok ? lastHeadResult.value : null;
|
|
59
|
+
const headResult = git.revParseHead(repoRoot);
|
|
60
|
+
|
|
61
|
+
// Gather graph stats for context.
|
|
62
|
+
const stats = store.schemaStats();
|
|
63
|
+
const fileCount = stats.ok ? stats.stats.files : 0;
|
|
64
|
+
const nodeCount = stats.ok ? stats.stats.nodes : 0;
|
|
65
|
+
|
|
66
|
+
if (!headResult.ok) {
|
|
67
|
+
return {
|
|
68
|
+
lastIndexedHead,
|
|
69
|
+
currentHead: null,
|
|
70
|
+
dirty: true,
|
|
71
|
+
mode: "git_unavailable",
|
|
72
|
+
fileCount,
|
|
73
|
+
nodeCount,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const currentHead = headResult.head;
|
|
78
|
+
|
|
79
|
+
if (lastIndexedHead === null) {
|
|
80
|
+
return {
|
|
81
|
+
lastIndexedHead: null,
|
|
82
|
+
currentHead,
|
|
83
|
+
dirty: currentHead !== null,
|
|
84
|
+
mode: "empty",
|
|
85
|
+
fileCount,
|
|
86
|
+
nodeCount,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (currentHead === null) {
|
|
91
|
+
// Repo currently has no commits (unborn HEAD / reset) but the index
|
|
92
|
+
// still has a last_indexed_head — the index cannot match a repo
|
|
93
|
+
// with no commits, so it is stale, not fresh. Reporting "fresh"
|
|
94
|
+
// here would let callers act on a graph that no longer reflects
|
|
95
|
+
// the repo (cursor Bugbot: 'Index status false fresh').
|
|
96
|
+
return {
|
|
97
|
+
lastIndexedHead,
|
|
98
|
+
currentHead: null,
|
|
99
|
+
dirty: true,
|
|
100
|
+
mode: "stale",
|
|
101
|
+
fileCount,
|
|
102
|
+
nodeCount,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pending parse failures mean some changed paths are missing from the
|
|
107
|
+
// graph even though the heads match. Report stale+dirty so doctor/xray
|
|
108
|
+
// surface it instead of pretending freshness (chatgpt-codex-connector:
|
|
109
|
+
// 'Mark pending parse retries as stale').
|
|
110
|
+
const pendingResult = store.readMeta(META_KEY_PENDING_PARSE_FAILURES);
|
|
111
|
+
const pendingRaw = pendingResult.ok ? pendingResult.value : null;
|
|
112
|
+
let hasPendingFailures = false;
|
|
113
|
+
if (pendingRaw !== null) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(pendingRaw);
|
|
116
|
+
hasPendingFailures = Array.isArray(parsed) && parsed.length > 0;
|
|
117
|
+
} catch {
|
|
118
|
+
hasPendingFailures = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const dirty = lastIndexedHead !== currentHead || hasPendingFailures;
|
|
122
|
+
const mode: IndexStatusMode = hasPendingFailures
|
|
123
|
+
? "stale"
|
|
124
|
+
: dirty
|
|
125
|
+
? "stale"
|
|
126
|
+
: "fresh";
|
|
127
|
+
return {
|
|
128
|
+
lastIndexedHead,
|
|
129
|
+
currentHead,
|
|
130
|
+
dirty,
|
|
131
|
+
mode,
|
|
132
|
+
fileCount,
|
|
133
|
+
nodeCount,
|
|
134
|
+
};
|
|
135
|
+
}
|