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.
- package/README.md +43 -1
- package/dist/_shared/impact-risk.d.ts +37 -0
- package/dist/_shared/impact-risk.d.ts.map +1 -0
- package/dist/_shared/impact-risk.js +92 -0
- package/dist/_shared/impact-risk.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/analyze-config.d.ts +2 -0
- package/dist/cli/analyze-config.js +16 -0
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze.d.ts +17 -0
- package/dist/cli/analyze.js +43 -7
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/group.js +7 -1
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +12 -2
- package/dist/cli/i18n/en.js +12 -2
- package/dist/cli/i18n/resources.d.ts +22 -2
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +12 -2
- package/dist/cli/index.js +11 -4
- package/dist/cli/status.js +91 -6
- package/dist/cli/watch-queue.d.ts +41 -0
- package/dist/cli/watch-queue.js +184 -0
- package/dist/cli/watch.d.ts +20 -0
- package/dist/cli/watch.js +372 -0
- package/dist/cli/wiki.js +15 -2
- package/dist/config/ignore-service.d.ts +11 -0
- package/dist/config/ignore-service.js +41 -4
- package/dist/config/repo-control-file.d.ts +3 -0
- package/dist/config/repo-control-file.js +115 -0
- package/dist/core/group/config-parser.js +20 -2
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +33 -2
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/service.js +5 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.d.ts +3 -1
- package/dist/core/group/sync.js +43 -5
- package/dist/core/group/types.d.ts +19 -5
- package/dist/core/incremental/derived-writeback.d.ts +36 -0
- package/dist/core/incremental/derived-writeback.js +68 -0
- package/dist/core/incremental/subgraph-extract.d.ts +6 -4
- package/dist/core/incremental/subgraph-extract.js +7 -5
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +14 -1
- package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
- package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
- package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
- package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +44 -12
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/ingestion/workers/parse-worker.js +28 -2
- package/dist/core/lbug/lbug-adapter.d.ts +59 -0
- package/dist/core/lbug/lbug-adapter.js +154 -1
- package/dist/core/run-analyze.d.ts +17 -0
- package/dist/core/run-analyze.js +231 -23
- package/dist/core/search/fts-indexes.d.ts +19 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/mcp/local/local-backend.d.ts +24 -7
- package/dist/mcp/local/local-backend.js +190 -81
- package/dist/mcp/local/pdg-impact.d.ts +8 -4
- package/dist/mcp/local/pdg-impact.js +7 -2
- package/dist/mcp/repository-policy.d.ts +5 -1
- package/dist/mcp/repository-policy.js +48 -4
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools.js +31 -19
- package/dist/server/api.js +23 -64
- package/dist/server/grep-params.d.ts +18 -0
- package/dist/server/grep-params.js +83 -0
- package/dist/server/grep-scan.d.ts +23 -0
- package/dist/server/grep-scan.js +107 -0
- package/dist/server/grep-worker.d.ts +1 -0
- package/dist/server/grep-worker.js +12 -0
- package/dist/server/mcp-http.d.ts +8 -0
- package/dist/server/mcp-http.js +16 -1
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/fs-atomic.d.ts +24 -0
- package/dist/storage/fs-atomic.js +86 -2
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/parse-cache.d.ts +22 -4
- package/dist/storage/parse-cache.js +106 -29
- package/dist/storage/parsedfile-store.d.ts +34 -60
- package/dist/storage/parsedfile-store.js +177 -171
- package/dist/storage/repo-manager.d.ts +11 -1
- package/dist/storage/repo-manager.js +23 -2
- package/dist/storage/repo-meta.d.ts +12 -0
- package/dist/storage/v8-sidecar.d.ts +48 -0
- package/dist/storage/v8-sidecar.js +347 -0
- package/dist/types/pipeline.d.ts +14 -0
- package/package.json +4 -1
- package/scripts/cross-platform-shard.ts +4 -2
- package/scripts/cross-platform-tests.ts +7 -1
- package/skills/gitnexus-cli.md +11 -3
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
- package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
- 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
|
+
};
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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` (
|
|
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
|
package/skills/gitnexus-cli.md
CHANGED
|
@@ -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.
|
|
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**:
|
|
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:
|