gitnexus 1.6.11-rc.2 → 1.6.11-rc.21

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 (158) hide show
  1. package/README.md +43 -1
  2. package/dist/_shared/impact-risk.d.ts +37 -0
  3. package/dist/_shared/impact-risk.d.ts.map +1 -0
  4. package/dist/_shared/impact-risk.js +92 -0
  5. package/dist/_shared/impact-risk.js.map +1 -0
  6. package/dist/_shared/index.d.ts +2 -0
  7. package/dist/_shared/index.d.ts.map +1 -1
  8. package/dist/_shared/index.js +2 -0
  9. package/dist/_shared/index.js.map +1 -1
  10. package/dist/cli/ai-context.js +4 -4
  11. package/dist/cli/analyze-config.d.ts +2 -0
  12. package/dist/cli/analyze-config.js +16 -0
  13. package/dist/cli/analyze-options.d.ts +4 -0
  14. package/dist/cli/analyze.d.ts +17 -0
  15. package/dist/cli/analyze.js +43 -7
  16. package/dist/cli/eval-server.js +22 -5
  17. package/dist/cli/group.js +7 -1
  18. package/dist/cli/help-i18n.js +2 -0
  19. package/dist/cli/i18n/en.d.ts +12 -2
  20. package/dist/cli/i18n/en.js +12 -2
  21. package/dist/cli/i18n/resources.d.ts +22 -2
  22. package/dist/cli/i18n/zh-CN.d.ts +10 -0
  23. package/dist/cli/i18n/zh-CN.js +12 -2
  24. package/dist/cli/index.js +11 -4
  25. package/dist/cli/status.js +91 -6
  26. package/dist/cli/watch-queue.d.ts +41 -0
  27. package/dist/cli/watch-queue.js +184 -0
  28. package/dist/cli/watch.d.ts +20 -0
  29. package/dist/cli/watch.js +372 -0
  30. package/dist/cli/wiki.js +15 -2
  31. package/dist/config/ignore-service.d.ts +11 -0
  32. package/dist/config/ignore-service.js +41 -4
  33. package/dist/config/repo-control-file.d.ts +3 -0
  34. package/dist/config/repo-control-file.js +115 -0
  35. package/dist/core/group/config-parser.js +20 -2
  36. package/dist/core/group/cross-impact.d.ts +2 -1
  37. package/dist/core/group/cross-impact.js +33 -2
  38. package/dist/core/group/extractors/fs-utils.d.ts +2 -0
  39. package/dist/core/group/extractors/fs-utils.js +86 -0
  40. package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
  41. package/dist/core/group/extractors/graphql-extractor.js +652 -0
  42. package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
  43. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  44. package/dist/core/group/extractors/manifest-extractor.js +1 -1
  45. package/dist/core/group/matching.js +8 -1
  46. package/dist/core/group/service.js +5 -1
  47. package/dist/core/group/storage.js +1 -0
  48. package/dist/core/group/sync.d.ts +3 -1
  49. package/dist/core/group/sync.js +43 -5
  50. package/dist/core/group/types.d.ts +19 -5
  51. package/dist/core/incremental/derived-writeback.d.ts +36 -0
  52. package/dist/core/incremental/derived-writeback.js +68 -0
  53. package/dist/core/incremental/subgraph-extract.d.ts +6 -4
  54. package/dist/core/incremental/subgraph-extract.js +7 -5
  55. package/dist/core/index-content-drift.d.ts +54 -0
  56. package/dist/core/index-content-drift.js +127 -0
  57. package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
  58. package/dist/core/ingestion/filesystem-walker.js +20 -3
  59. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
  60. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
  61. package/dist/core/ingestion/language-provider.d.ts +43 -0
  62. package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
  63. package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
  64. package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
  65. package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
  66. package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
  67. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
  68. package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
  69. package/dist/core/ingestion/languages/java/captures.js +14 -1
  70. package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
  71. package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
  72. package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
  73. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
  74. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
  75. package/dist/core/ingestion/languages/java.js +2 -0
  76. package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
  77. package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
  78. package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
  79. package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
  80. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
  81. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
  82. package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
  83. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
  84. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
  85. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
  86. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
  87. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
  88. package/dist/core/ingestion/languages/kotlin.js +2 -0
  89. package/dist/core/ingestion/pipeline-phases/di.js +47 -14
  90. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
  91. package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
  92. package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
  93. package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
  94. package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
  95. package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
  96. package/dist/core/ingestion/pipeline.d.ts +6 -0
  97. package/dist/core/ingestion/pipeline.js +44 -12
  98. package/dist/core/ingestion/scope-extractor.js +1 -0
  99. package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
  100. package/dist/core/ingestion/utils/symbol-labels.js +2 -2
  101. package/dist/core/ingestion/workers/parse-worker.js +28 -2
  102. package/dist/core/lbug/lbug-adapter.d.ts +59 -0
  103. package/dist/core/lbug/lbug-adapter.js +154 -1
  104. package/dist/core/run-analyze.d.ts +17 -0
  105. package/dist/core/run-analyze.js +231 -23
  106. package/dist/core/search/fts-indexes.d.ts +19 -1
  107. package/dist/core/search/fts-indexes.js +28 -1
  108. package/dist/core/wiki/generator.js +8 -0
  109. package/dist/core/wiki/grok-client.d.ts +21 -0
  110. package/dist/core/wiki/grok-client.js +287 -0
  111. package/dist/core/wiki/llm-client.d.ts +1 -1
  112. package/dist/core/wiki/llm-client.js +5 -2
  113. package/dist/core/wiki/local-cli-client.d.ts +11 -0
  114. package/dist/core/wiki/local-cli-client.js +22 -9
  115. package/dist/mcp/local/local-backend.d.ts +24 -7
  116. package/dist/mcp/local/local-backend.js +190 -81
  117. package/dist/mcp/local/pdg-impact.d.ts +8 -4
  118. package/dist/mcp/local/pdg-impact.js +7 -2
  119. package/dist/mcp/repository-policy.d.ts +5 -1
  120. package/dist/mcp/repository-policy.js +48 -4
  121. package/dist/mcp/resources.js +2 -1
  122. package/dist/mcp/server.js +6 -5
  123. package/dist/mcp/tools.js +31 -19
  124. package/dist/server/api.js +23 -64
  125. package/dist/server/grep-params.d.ts +18 -0
  126. package/dist/server/grep-params.js +83 -0
  127. package/dist/server/grep-scan.d.ts +23 -0
  128. package/dist/server/grep-scan.js +107 -0
  129. package/dist/server/grep-worker.d.ts +1 -0
  130. package/dist/server/grep-worker.js +12 -0
  131. package/dist/server/mcp-http.d.ts +8 -0
  132. package/dist/server/mcp-http.js +16 -1
  133. package/dist/storage/file-hash.d.ts +5 -0
  134. package/dist/storage/file-hash.js +16 -6
  135. package/dist/storage/fs-atomic.d.ts +24 -0
  136. package/dist/storage/fs-atomic.js +86 -2
  137. package/dist/storage/git.d.ts +19 -8
  138. package/dist/storage/git.js +83 -24
  139. package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
  140. package/dist/storage/gitnexus-managed-paths.js +46 -0
  141. package/dist/storage/parse-cache.d.ts +22 -4
  142. package/dist/storage/parse-cache.js +106 -29
  143. package/dist/storage/parsedfile-store.d.ts +34 -60
  144. package/dist/storage/parsedfile-store.js +177 -171
  145. package/dist/storage/repo-manager.d.ts +11 -1
  146. package/dist/storage/repo-manager.js +23 -2
  147. package/dist/storage/repo-meta.d.ts +12 -0
  148. package/dist/storage/v8-sidecar.d.ts +48 -0
  149. package/dist/storage/v8-sidecar.js +347 -0
  150. package/dist/types/pipeline.d.ts +14 -0
  151. package/package.json +4 -1
  152. package/scripts/cross-platform-shard.ts +4 -2
  153. package/scripts/cross-platform-tests.ts +7 -1
  154. package/skills/gitnexus-cli.md +11 -3
  155. package/skills/gitnexus-impact-analysis.md +9 -0
  156. package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
  157. package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
  158. package/web/index.html +1 -1
@@ -0,0 +1,347 @@
1
+ /**
2
+ * One-file V8 cache envelope for parse-cache and ParsedFile shards.
3
+ *
4
+ * Each object-graph shard is a single immutable `.v8` file published by
5
+ * tmp+rename. JSON is not a fallback: a missing, corrupt, runtime-incompatible,
6
+ * or deserialize-failed envelope is a cache miss (re-extract / re-dispatch).
7
+ * Tiny JSON manifests (`index.json`) stay outside this module.
8
+ *
9
+ * Envelope: magic, format, Node major, V8 version, optional path listing,
10
+ * `v8.serialize` of the live graph, then SHA-256 of listing||payload. Path
11
+ * bytes live before the payload so a ParsedFile loader can skip a shard whose
12
+ * authenticated listing misses `wantPaths` without deserializing. An
13
+ * unreadable/invalid listing is fail-closed: deserialize, never skip.
14
+ */
15
+ import { promises as fs } from 'node:fs';
16
+ import { createHash } from 'node:crypto';
17
+ import v8 from 'node:v8';
18
+ import { logger } from '../core/logger.js';
19
+ import { linkOrCopyFile, writeFileAtomicBytes, writeFileAtomicBytesSync } from './fs-atomic.js';
20
+ const MAGIC = Buffer.from('GNXV8CF1', 'ascii');
21
+ /** Envelope version — independent of PARSE_CACHE_VERSION / SCHEMA_BUMP. */
22
+ export const V8_CACHE_FORMAT = 5;
23
+ const U32 = 4;
24
+ const U16 = 2;
25
+ const PAYLOAD_HASH_LEN = 32;
26
+ const MAGIC_LEN = 8;
27
+ const FIXED_PREFIX = MAGIC_LEN + U32 + U16 + U16; // magic + format + nodeMajor + v8len
28
+ const internString = (value, pool) => {
29
+ const hit = pool.get(value);
30
+ if (hit !== undefined)
31
+ return hit;
32
+ pool.set(value, value);
33
+ return value;
34
+ };
35
+ /**
36
+ * Collapse duplicate strings in a live deserialized graph into `pool`, mutating
37
+ * in place so object identity (shared `SymbolDefinition`s, Maps) is preserved.
38
+ * Required after `v8.deserialize` of ParsedFile shards: V8 does not recreate
39
+ * the JSON reviver's cross-shard string intern, and skipping it regresses
40
+ * retained heap (~+59% measured vs interned JSON).
41
+ */
42
+ export const internGraphStrings = (root, pool) => {
43
+ const seen = new WeakSet();
44
+ const walk = (value) => {
45
+ if (typeof value === 'string')
46
+ return internString(value, pool);
47
+ if (value === null || typeof value !== 'object')
48
+ return value;
49
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer)
50
+ return value;
51
+ if (seen.has(value))
52
+ return value;
53
+ seen.add(value);
54
+ if (value instanceof Map) {
55
+ const entries = [...value];
56
+ value.clear();
57
+ for (const [k, v] of entries)
58
+ value.set(walk(k), walk(v));
59
+ return value;
60
+ }
61
+ if (value instanceof Set) {
62
+ const entries = [...value];
63
+ value.clear();
64
+ for (const v of entries)
65
+ value.add(walk(v));
66
+ return value;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ for (let i = 0; i < value.length; i++)
70
+ value[i] = walk(value[i]);
71
+ return value;
72
+ }
73
+ const rec = value;
74
+ for (const key of Object.keys(rec)) {
75
+ rec[key] = walk(rec[key]);
76
+ }
77
+ return value;
78
+ };
79
+ return walk(root);
80
+ };
81
+ const nodeMajor = () => Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
82
+ const isEnoent = (err) => err.code === 'ENOENT';
83
+ const warnCache = (err, filePath, msg) => {
84
+ logger.warn({ err, filePath }, msg);
85
+ };
86
+ /** NDJSON listing cannot encode paths that themselves contain CR/LF/NUL. */
87
+ export const encodeCachePathListing = (paths) => {
88
+ if (paths.length === 0)
89
+ return Buffer.alloc(0);
90
+ if (paths.some((p) => /[\r\n\0]/.test(p)))
91
+ return Buffer.alloc(0);
92
+ return Buffer.from(`${paths.length}\n${paths.join('\n')}\n`, 'utf8');
93
+ };
94
+ /**
95
+ * Parse a counted NDJSON path listing. Returns `null` when it must not be
96
+ * trusted to skip the payload: missing trailing newline, CR/NUL, or a count
97
+ * that does not match the remaining lines.
98
+ */
99
+ export const parseCachePathListing = (raw) => {
100
+ if (raw.byteLength === 0)
101
+ return [];
102
+ let sidecarRaw;
103
+ try {
104
+ sidecarRaw = new TextDecoder('utf-8', { fatal: true }).decode(raw);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ if (sidecarRaw.includes('\0') || sidecarRaw.includes('\r') || !sidecarRaw.endsWith('\n')) {
110
+ return null;
111
+ }
112
+ const nl = sidecarRaw.indexOf('\n');
113
+ if (nl < 0)
114
+ return null;
115
+ const countToken = sidecarRaw.slice(0, nl);
116
+ if (!/^[0-9]+$/.test(countToken))
117
+ return null;
118
+ const count = Number(countToken);
119
+ const body = sidecarRaw.slice(nl + 1);
120
+ const listed = body === '' ? [] : body.slice(0, -1).split('\n');
121
+ if (listed.length !== count)
122
+ return null;
123
+ return listed;
124
+ };
125
+ const encodeEnvelope = (graph, paths) => {
126
+ let payload;
127
+ try {
128
+ payload = v8.serialize(graph);
129
+ }
130
+ catch (err) {
131
+ warnCache(err, '', 'v8 cache: serialize failed; treating as miss on next load');
132
+ return undefined;
133
+ }
134
+ const pathListing = encodeCachePathListing(paths);
135
+ const listed = parseCachePathListing(pathListing);
136
+ const pathCount = listed === null ? 0 : listed.length;
137
+ if (payload.byteLength > 0xffff_ffff || pathListing.byteLength > 0xffff_ffff)
138
+ return undefined;
139
+ const v8ver = Buffer.from(process.versions.v8, 'utf8');
140
+ if (v8ver.byteLength > 0xffff)
141
+ return undefined;
142
+ const header = Buffer.allocUnsafe(FIXED_PREFIX + v8ver.length + U32 * 3);
143
+ MAGIC.copy(header, 0);
144
+ header.writeUInt32LE(V8_CACHE_FORMAT, MAGIC_LEN);
145
+ header.writeUInt16LE(nodeMajor(), MAGIC_LEN + U32);
146
+ header.writeUInt16LE(v8ver.length, MAGIC_LEN + U32 + U16);
147
+ v8ver.copy(header, FIXED_PREFIX);
148
+ let off = FIXED_PREFIX + v8ver.length;
149
+ header.writeUInt32LE(pathCount, off);
150
+ off += U32;
151
+ header.writeUInt32LE(pathListing.byteLength, off);
152
+ off += U32;
153
+ header.writeUInt32LE(payload.byteLength, off);
154
+ const payloadHash = createHash('sha256').update(pathListing).update(payload).digest();
155
+ return Buffer.concat([header, pathListing, payload, payloadHash]);
156
+ };
157
+ const decodePrefix = (buf) => {
158
+ if (buf.byteLength < FIXED_PREFIX)
159
+ return undefined;
160
+ if (!buf.subarray(0, MAGIC_LEN).equals(MAGIC))
161
+ return undefined;
162
+ if (buf.readUInt32LE(MAGIC_LEN) !== V8_CACHE_FORMAT)
163
+ return undefined;
164
+ const recordedNodeMajor = buf.readUInt16LE(MAGIC_LEN + U32);
165
+ const v8len = buf.readUInt16LE(MAGIC_LEN + U32 + U16);
166
+ const v8off = FIXED_PREFIX;
167
+ const countsOff = v8off + v8len;
168
+ if (buf.byteLength < countsOff + U32 * 3)
169
+ return undefined;
170
+ const recordedV8 = buf.subarray(v8off, v8off + v8len).toString('utf8');
171
+ const pathCount = buf.readUInt32LE(countsOff);
172
+ const pathBytes = buf.readUInt32LE(countsOff + U32);
173
+ const payloadLen = buf.readUInt32LE(countsOff + U32 * 2);
174
+ const pathsOff = countsOff + U32 * 3;
175
+ const payloadOff = pathsOff + pathBytes;
176
+ return {
177
+ recordedNodeMajor,
178
+ recordedV8,
179
+ pathCount,
180
+ pathBytes,
181
+ payloadLen,
182
+ pathsOff,
183
+ payloadOff,
184
+ };
185
+ };
186
+ const runtimeCompatible = (meta) => meta.recordedNodeMajor === nodeMajor() && meta.recordedV8 === process.versions.v8;
187
+ const readExact = async (fh, offset, length) => {
188
+ const buf = Buffer.allocUnsafe(length);
189
+ let got = 0;
190
+ while (got < length) {
191
+ const { bytesRead } = await fh.read(buf, got, length - got, offset + got);
192
+ if (bytesRead === 0)
193
+ return undefined;
194
+ got += bytesRead;
195
+ }
196
+ return buf;
197
+ };
198
+ const readVerifiedPayload = async (fh, meta, pathRaw) => {
199
+ const payloadAndHash = await readExact(fh, meta.payloadOff, meta.payloadLen + PAYLOAD_HASH_LEN);
200
+ if (!payloadAndHash)
201
+ return undefined;
202
+ const payload = payloadAndHash.subarray(0, meta.payloadLen);
203
+ const expected = payloadAndHash.subarray(meta.payloadLen);
204
+ const digest = createHash('sha256').update(pathRaw).update(payload).digest();
205
+ return digest.equals(expected) ? payload : undefined;
206
+ };
207
+ /**
208
+ * Validate the immutable envelope metadata needed by the durable ParsedFile
209
+ * warm-hit gate without deserializing its payload. Atomic publication means a
210
+ * runtime-compatible envelope whose exact file length and counted path listing
211
+ * validate is a stable snapshot candidate; malformed/truncated envelopes miss.
212
+ */
213
+ export const inspectV8Cache = async (filePath) => {
214
+ let fh;
215
+ try {
216
+ fh = await fs.open(filePath, 'r');
217
+ const st = await fh.stat();
218
+ const prefix = await readExact(fh, 0, Math.min(st.size, FIXED_PREFIX + 256 + U32 * 3));
219
+ if (!prefix)
220
+ return undefined;
221
+ const meta = decodePrefix(prefix);
222
+ if (!meta || !runtimeCompatible(meta))
223
+ return undefined;
224
+ if (meta.payloadOff + meta.payloadLen + PAYLOAD_HASH_LEN !== st.size || meta.pathBytes === 0) {
225
+ return undefined;
226
+ }
227
+ const pathRaw = prefix.byteLength >= meta.payloadOff
228
+ ? Buffer.from(prefix.subarray(meta.pathsOff, meta.payloadOff))
229
+ : await readExact(fh, meta.pathsOff, meta.pathBytes);
230
+ if (!pathRaw)
231
+ return undefined;
232
+ const listed = parseCachePathListing(pathRaw);
233
+ if (listed === null || listed.length !== meta.pathCount || listed.length === 0) {
234
+ return undefined;
235
+ }
236
+ if (!(await readVerifiedPayload(fh, meta, pathRaw)))
237
+ return undefined;
238
+ return { paths: listed };
239
+ }
240
+ catch (err) {
241
+ if (!isEnoent(err)) {
242
+ logger.debug({ err, filePath }, 'v8 cache: inspection failed; treating as miss');
243
+ }
244
+ return undefined;
245
+ }
246
+ finally {
247
+ await fh?.close().catch(() => { });
248
+ }
249
+ };
250
+ /**
251
+ * Load a cache file. When `wantPaths` is set and a digest-verified non-empty
252
+ * path listing has no intersection, returns `{ kind: 'skip' }` without
253
+ * deserializing. An authentic listing that does not parse deserializes (fail
254
+ * closed). Envelope/runtime/digest failure returns undefined (miss).
255
+ */
256
+ export const tryLoadV8Cache = async (filePath, internPool, wantPaths) => {
257
+ let fh;
258
+ try {
259
+ fh = await fs.open(filePath, 'r');
260
+ const st = await fh.stat();
261
+ const prefix = await readExact(fh, 0, Math.min(st.size, FIXED_PREFIX + 256 + U32 * 3));
262
+ if (!prefix)
263
+ return undefined;
264
+ const meta = decodePrefix(prefix);
265
+ if (!meta)
266
+ return undefined;
267
+ if (!runtimeCompatible(meta))
268
+ return undefined;
269
+ if (meta.payloadOff + meta.payloadLen + PAYLOAD_HASH_LEN !== st.size)
270
+ return undefined;
271
+ let pathRaw = Buffer.alloc(0);
272
+ if (meta.pathBytes > 0) {
273
+ if (prefix.byteLength >= meta.payloadOff) {
274
+ pathRaw = Buffer.from(prefix.subarray(meta.pathsOff, meta.payloadOff));
275
+ }
276
+ else {
277
+ const raw = await readExact(fh, meta.pathsOff, meta.pathBytes);
278
+ if (!raw)
279
+ return undefined;
280
+ pathRaw = Buffer.from(raw);
281
+ }
282
+ }
283
+ const payload = await readVerifiedPayload(fh, meta, pathRaw);
284
+ if (!payload)
285
+ return undefined;
286
+ if (wantPaths && wantPaths.size > 0 && meta.pathBytes > 0) {
287
+ const listed = parseCachePathListing(pathRaw);
288
+ if (listed !== null &&
289
+ listed.length === meta.pathCount &&
290
+ listed.length > 0 &&
291
+ !listed.some((p) => wantPaths.has(p))) {
292
+ return { kind: 'skip', bytes: st.size };
293
+ }
294
+ }
295
+ const value = v8.deserialize(payload);
296
+ if (internPool)
297
+ internGraphStrings(value, internPool);
298
+ return { kind: 'hit', value, bytes: st.size };
299
+ }
300
+ catch (err) {
301
+ if (!isEnoent(err)) {
302
+ logger.debug({ err, filePath }, 'v8 cache: load failed; treating as miss');
303
+ }
304
+ return undefined;
305
+ }
306
+ finally {
307
+ await fh?.close().catch(() => { });
308
+ }
309
+ };
310
+ export const writeV8CacheFile = async (filePath, graph, paths) => {
311
+ const blob = encodeEnvelope(graph, paths ?? []);
312
+ if (!blob)
313
+ return false;
314
+ try {
315
+ await writeFileAtomicBytes(filePath, blob, 1);
316
+ return true;
317
+ }
318
+ catch (err) {
319
+ warnCache(err, filePath, 'v8 cache: write failed; treating as miss');
320
+ return false;
321
+ }
322
+ };
323
+ export const writeV8CacheFileSync = (filePath, graph, paths) => {
324
+ const blob = encodeEnvelope(graph, paths ?? []);
325
+ if (!blob)
326
+ return false;
327
+ try {
328
+ writeFileAtomicBytesSync(filePath, blob);
329
+ return true;
330
+ }
331
+ catch (err) {
332
+ warnCache(err, filePath, 'v8 cache: write failed; treating as miss');
333
+ return false;
334
+ }
335
+ };
336
+ export const copyV8CacheIfPresent = async (srcPath, dstPath) => {
337
+ try {
338
+ await linkOrCopyFile(srcPath, dstPath);
339
+ return true;
340
+ }
341
+ catch (copyErr) {
342
+ if (!isEnoent(copyErr)) {
343
+ warnCache(copyErr, srcPath, 'v8 cache: copy failed; treating as miss');
344
+ }
345
+ return false;
346
+ }
347
+ };
@@ -13,6 +13,18 @@ export interface PipelineResult {
13
13
  totalFileCount: number;
14
14
  communityResult?: CommunityDetectionResult;
15
15
  processResult?: ProcessDetectionResult;
16
+ /**
17
+ * Runs the community/process phases that `skipDerivedGraphPhases` held back
18
+ * (#3016), against the same graph and phase outputs the pipeline already
19
+ * produced, and populates `communityResult`/`processResult` on this object.
20
+ *
21
+ * Present ONLY when those phases were skipped for that reason, so a caller
22
+ * that optimistically skipped them can still get a byte-identical derived
23
+ * layer on the paths that turn out to need one (full rebuild, escalated
24
+ * write, or an incremental run with deleted files). Absent means the phases
25
+ * either already ran or were disabled for an unrelated reason.
26
+ */
27
+ runDeferredDerivedPhases?: () => Promise<void>;
16
28
  /**
17
29
  * Additive diagnostics for registry-primary resolution decisions that
18
30
  * deliberately suppress edge emission. Empty means no diagnostic was
@@ -38,6 +50,8 @@ export interface PipelineResult {
38
50
  * affordance so regression suites can prove the pool engaged.
39
51
  */
40
52
  usedWorkerPool: boolean;
53
+ /** Files actually dispatched to parser workers after parse-cache lookup. */
54
+ reparsedFileCount: number;
41
55
  /** Files omitted from scope-resolution while the rest of analysis continued. */
42
56
  scopeExtractionFailures: readonly string[];
43
57
  /** Files scope resolution could not inspect because their parser was unavailable. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.2",
3
+ "version": "1.6.11-rc.21",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -60,15 +60,18 @@
60
60
  "@modelcontextprotocol/sdk": "^1.0.0",
61
61
  "@scarf/scarf": "^1.4.0",
62
62
  "busboy": "^1.6.0",
63
+ "chokidar": "^4.0.3",
63
64
  "cli-progress": "^3.12.0",
64
65
  "commander": "^15.0.0",
65
66
  "cors": "^2.8.5",
66
67
  "express": "^5.2.1",
67
68
  "express-rate-limit": "^8.4.1",
69
+ "fast-xml-parser": "^5.11.1",
68
70
  "glob": "^13.0.6",
69
71
  "graphology": "^0.26.0",
70
72
  "graphology-indices": "^0.17.0",
71
73
  "graphology-utils": "^2.3.0",
74
+ "graphql": "^16.14.2",
72
75
  "ignore": "^7.0.5",
73
76
  "js-yaml": "^5.0.0",
74
77
  "jsonc-parser": "^3.3.1",
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * WHY THIS EXISTS. `run-cross-platform.ts` used to hand vitest the whole file
5
5
  * list plus `--shard=i/n`, and vitest partitions by file COUNT. Runtime on this
6
- * suite is wildly uneven — measured on the Windows runner, `cli-e2e` is 361 s
6
+ * suite is wildly uneven — measured on the Windows runner, `cli-e2e` is 621 s
7
7
  * and `worker-pool` 221 s, while most files are under a second — so a
8
8
  * count-split routinely put several of the heaviest suites on one shard. That
9
9
  * is #2449, and this file's sibling header has documented the symptom ("the
@@ -44,7 +44,9 @@
44
44
  * partition depend on the very machine load it is trying to protect against.
45
45
  */
46
46
  export const WINDOWS_WEIGHTS_SEC: Readonly<Record<string, number>> = {
47
- 'test/integration/cli-e2e.test.ts': 361,
47
+ // Re-measured after the analyze --watch e2e landed in #3072. The previous
48
+ // 361 s entry undercharged this suite and left shard 1 close to the watchdog.
49
+ 'test/integration/cli-e2e.test.ts': 621,
48
50
  'test/integration/worker-pool.test.ts': 222,
49
51
  'test/unit/incremental-vector-extension-ordering.test.ts': 87,
50
52
  // ESTIMATE, not a measurement (#2841): this suite drives more full
@@ -90,6 +90,7 @@ const PLATFORM_LOGIC = [
90
90
  'test/unit/ignore-service.test.ts',
91
91
  'test/unit/group/bridge-db.test.ts',
92
92
  'test/unit/group/bridge-db-edge.test.ts',
93
+ 'test/unit/group/fs-utils.test.ts',
93
94
  'test/unit/onnxruntime-node-resolver.test.ts',
94
95
  // Windows cmd.exe arg-quoting + compose-and-spawn for the npm install (#2372):
95
96
  // the quoting rules and win32 single-string spawn shape are OS-sensitive, so
@@ -113,6 +114,9 @@ const PLATFORM_LOGIC = [
113
114
  // POSIX and Windows — the fail-closed path-claim semantics must hold on the
114
115
  // real windows-latest path implementation (#2419/#2420).
115
116
  'test/unit/server-api-repo-resolution.test.ts',
117
+ // #3073: cwd-based repository selection canonicalizes real paths, compares
118
+ // platform separators/case, and rejects nested Git-boundary fallthrough.
119
+ 'test/unit/calltool-dispatch.test.ts',
116
120
  // The index write-lock (#2658) selects its backend by process.platform — the
117
121
  // OS socket lock (Windows named pipe / Linux abstract socket) vs the file
118
122
  // fallback — and its socket-backend describe block is gated to linux/win32.
@@ -140,6 +144,7 @@ const LBUG_NATIVE = [
140
144
  // opens them through the pool adapter (native addon + bridge file locking).
141
145
  // Windows is skipped in-file (describeReopen) due to the bridge reopen lock.
142
146
  'test/integration/group/cross-trace-e2e.test.ts',
147
+ 'test/integration/group/graphql-resolve-symbol.test.ts',
143
148
  'test/integration/local-backend.test.ts',
144
149
  'test/integration/local-backend-calltool.test.ts',
145
150
  'test/integration/search-core.test.ts',
@@ -232,7 +237,7 @@ const SPAWN_CLI = [
232
237
  // Cheap: measured on the Windows runner at 448 ms, 53 ms and sub-second. An
233
238
  // earlier attempt to register them still turned the matrix red — not from
234
239
  // their own cost, but because vitest sharded by file COUNT, so inserting any
235
- // file re-partitioned the list and happened to cluster `cli-e2e` (361 s) with
240
+ // file re-partitioned the list and happened to cluster `cli-e2e` (621 s) with
236
241
  // `cli-limit-e2e` (75 s) on one shard. The split is weight-aware now
237
242
  // (`scripts/cross-platform-shard.ts`), so a cheap file can no longer move a
238
243
  // heavy one.
@@ -271,6 +276,7 @@ const NATIVE_ADDON_SMOKE = [
271
276
  // platforms (CRLF, symlinks, permissions, temp dirs)
272
277
  const FILESYSTEM = [
273
278
  'test/integration/filesystem-walker.test.ts',
279
+ 'test/integration/watch-filesystem.test.ts',
274
280
  'test/integration/markdown-processor-crlf.test.ts',
275
281
  'test/integration/ignore-and-skip-e2e.test.ts',
276
282
  // Pins that the bridge pairing verdict is measured before the database is
@@ -21,6 +21,8 @@ Run from the project root. This parses all source files, builds the knowledge gr
21
21
 
22
22
  | Flag | Effect |
23
23
  | -------------- | ---------------------------------------------------------------- |
24
+ | `--watch` | Keep a Git repository index current with serialized refreshes |
25
+ | `--debounce <ms>` | Watch quiet period before refresh (default: 300 ms) |
24
26
  | `--force` | Force full re-index even if up to date |
25
27
  | `--embeddings` | Enable embedding generation for semantic search (off by default) |
26
28
  | `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |
@@ -28,6 +30,8 @@ Run from the project root. This parses all source files, builds the knowledge gr
28
30
 
29
31
  **When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook detects staleness after `git commit` and `git merge` and notifies the agent to run `analyze` — the hook does not run analyze itself, to avoid blocking the agent for up to 120s and risking KuzuDB corruption on timeout.
30
32
 
33
+ Use `node .gitnexus/run.cjs analyze --watch` for a long-lived local Git repository. It performs an initial analysis, queues scanner-admitted file changes, and retries intact failed batches with bounded backoff. Watch refreshes update only the graph: they skip AGENTS.md / CLAUDE.md injection and standard skill installation, so run a one-shot `analyze` when those generated files need updating. Watch rejects one-shot or context-output flags including `--force`, embedding flags, `--skills`, `--default-branch`, `--skip-agents-md`, `--skip-skills`, `--no-stats`, `--self-commit`, `--index-only`, and `--skip-git`. It never pulls remotes. Running MCP and `serve` processes periodically check for a published replacement and reopen it without a restart. MCP checks are throttled to once every five seconds, so a tool call before the next check can briefly use the previous index.
34
+
31
35
  ### status — Check index freshness
32
36
 
33
37
  ```bash
@@ -55,15 +59,19 @@ Deletes the `.gitnexus/` directory and unregisters the repo from the global regi
55
59
  node .gitnexus/run.cjs wiki
56
60
  ```
57
61
 
58
- Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
62
+ Generates repository documentation from the knowledge graph using an LLM. HTTP providers require an API key (saved to `~/.gitnexus/config.json` on first use). Local CLI providers (`--provider cursor|claude|codex|opencode|grok`) use your existing CLI login.
59
63
 
60
64
  | Flag | Effect |
61
65
  | ------------------- | ----------------------------------------- |
62
- | `--force` | Force full regeneration |
66
+ | `--force` | Force full regeneration, also required to re-generate an existing wiki in a different language |
67
+ | `--provider <name>` | LLM provider: minimax, openai, openrouter, azure, custom, cursor, claude, codex, opencode, or grok (default: minimax). Local CLIs (`cursor`, `claude`, `codex`, `opencode`, `grok`) use your existing CLI login and skip `--api-key`. |
63
68
  | `--model <model>` | LLM model (default: MiniMax-M3) |
64
69
  | `--base-url <url>` | LLM API base URL |
65
70
  | `--api-key <key>` | LLM API key |
66
71
  | `--concurrency <n>` | Parallel LLM calls (default: 3) |
72
+ | `--timeout <seconds>` | LLM request timeout in seconds (default: disabled) |
73
+ | `--retries <n>` | Max LLM retry attempts per request (default: 3) |
74
+ | `--lang <lang>` | Output language for generated documentation (e.g. english, chinese, spanish, japanese) |
67
75
  | `--gist` | Publish wiki as a public GitHub Gist |
68
76
 
69
77
  ### list — Show all indexed repos
@@ -82,5 +90,5 @@ Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_
82
90
  ## Troubleshooting
83
91
 
84
92
  - **"Not inside a git repository"**: Run from a directory inside a git repo
85
- - **Index is stale after re-analyzing**: Restart Claude Code to reload the MCP server
93
+ - **Index is stale after re-analyzing**: Wait for the next MCP tool call to reopen the published index; this normally takes no more than five seconds
86
94
  - **Embeddings slow**: Omit `--embeddings` (it's off by default) or set `OPENAI_API_KEY` for faster API-based embedding
@@ -93,6 +93,15 @@ dispatch, cross-language calls), so few-callers ⇒ LOW does **not** apply. The
93
93
  result carries a `riskNote` saying so. Confirm with a text search before
94
94
  treating the symbol as safe to change or delete.
95
95
 
96
+ `risk` is the edit gate: warn on HIGH/CRITICAL and stop on UNKNOWN until the
97
+ uncertainty is resolved. Within single-repo mode, compare File and symbol
98
+ targets with local `riskSharedAxes` (direct/total only). Within group mode,
99
+ compare only group results: their `riskSharedAxes` overlays resolved
100
+ cross-repo crossings on that local value. Never use either field to waive the
101
+ edit gate. Check `riskScale.unusedAxes` before comparing kinds: MCP File walks
102
+ omit process/module axes, while web Graph-RAG expands File targets to in-file
103
+ symbols before enrichment.
104
+
96
105
  ## Tools
97
106
 
98
107
  **impact** — the primary tool for symbol blast radius. If MCP is unavailable, use `node .gitnexus/run.cjs impact <symbol> --direction upstream --repo .` instead: