gitnexus 1.6.7 → 1.6.8-rc.10
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 +20 -0
- package/dist/cli/analyze-config.js +39 -0
- package/dist/cli/analyze.d.ts +16 -0
- package/dist/cli/analyze.js +34 -6
- package/dist/cli/clean.d.ts +1 -0
- package/dist/cli/clean.js +43 -1
- package/dist/cli/eval-server.js +43 -0
- package/dist/cli/help-i18n.js +6 -0
- package/dist/cli/i18n/en.d.ts +11 -0
- package/dist/cli/i18n/en.js +11 -0
- package/dist/cli/i18n/resources.d.ts +22 -0
- package/dist/cli/i18n/zh-CN.d.ts +11 -0
- package/dist/cli/i18n/zh-CN.js +11 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/list.js +12 -0
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/status.js +26 -5
- package/dist/cli/tool.d.ts +5 -0
- package/dist/cli/tool.js +5 -0
- package/dist/core/embeddings/embedder.js +4 -0
- package/dist/core/embeddings/embedding-pipeline.js +27 -16
- package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/java.js +3 -50
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/include-extractor.js +7 -7
- package/dist/core/ingestion/language-provider.d.ts +25 -2
- package/dist/core/ingestion/languages/c-cpp.js +11 -4
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +5 -1
- package/dist/core/ingestion/parsing-processor.js +21 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
- package/dist/core/ingestion/pipeline.d.ts +9 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
- package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
- package/dist/core/ingestion/route-extractors/spring.js +136 -0
- package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
- package/dist/core/ingestion/workers/clone-safety.js +465 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +10 -0
- package/dist/core/ingestion/workers/parse-worker.js +27 -52
- package/dist/core/ingestion/workers/post-result.d.ts +22 -0
- package/dist/core/ingestion/workers/post-result.js +87 -0
- package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
- package/dist/core/ingestion/workers/result-merge.js +43 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +6 -0
- package/dist/core/ingestion/workers/worker-pool.js +28 -14
- package/dist/core/lbug/lbug-adapter.d.ts +19 -0
- package/dist/core/lbug/lbug-adapter.js +56 -1
- package/dist/core/run-analyze.d.ts +45 -0
- package/dist/core/run-analyze.js +175 -24
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/mcp/core/embedder.js +4 -0
- package/dist/mcp/local/local-backend.d.ts +55 -2
- package/dist/mcp/local/local-backend.js +363 -25
- package/dist/mcp/tools.js +32 -0
- package/dist/server/analyze-worker-ipc.d.ts +58 -0
- package/dist/server/analyze-worker-ipc.js +16 -0
- package/dist/server/analyze-worker.js +7 -1
- package/dist/storage/branch-index.d.ts +52 -0
- package/dist/storage/branch-index.js +65 -0
- package/dist/storage/git.d.ts +11 -0
- package/dist/storage/git.js +28 -0
- package/dist/storage/parse-cache.js +3 -0
- package/dist/storage/repo-manager.d.ts +57 -2
- package/dist/storage/repo-manager.js +132 -18
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +24 -4
- package/hooks/claude/gitnexus-hook.cjs +18 -3
- package/package.json +2 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/scripts/materialize-vendor-grammars.cjs +0 -97
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-safe projection of `AnalyzeResult` for the analyze-worker → parent IPC
|
|
3
|
+
* boundary (#2112 boundary audit; #2135).
|
|
4
|
+
*
|
|
5
|
+
* The forked analyze worker (`analyze-worker.ts`) reports completion to the
|
|
6
|
+
* parent over `child_process` IPC, which uses Node's DEFAULT `'json'`
|
|
7
|
+
* serialization — `api.ts` forks the worker with no `serialization:` option, so
|
|
8
|
+
* the channel runs `JSON.stringify`/`JSON.parse`, NOT V8 structured clone.
|
|
9
|
+
*
|
|
10
|
+
* `AnalyzeResult.pipelineResult` is populated on every successful analysis
|
|
11
|
+
* (`run-analyze.ts`) and carries `pipelineResult.graph` — the live
|
|
12
|
+
* `KnowledgeGraph` closure object. Sending the raw result across this channel is
|
|
13
|
+
* wrong three ways:
|
|
14
|
+
* 1. Waste — the graph's `nodes`/`relationships` getters force-materialize the
|
|
15
|
+
* ENTIRE graph into two arrays, then JSON-stringify them, on every analyze.
|
|
16
|
+
* On a large repo (the #2112 scenario) that is a multi-hundred-MB
|
|
17
|
+
* stringify+parse whose result is immediately discarded.
|
|
18
|
+
* 2. Silent corruption — the graph's methods are own function properties;
|
|
19
|
+
* `JSON.stringify` drops them with no error, so a `pipelineResult.graph`
|
|
20
|
+
* that survived the wire is a data-only husk whose `forEachNode(...)` throws
|
|
21
|
+
* "is not a function" far from the cause.
|
|
22
|
+
* 3. Conditional crash — a BigInt or circular reference anywhere in the
|
|
23
|
+
* payload makes `process.send` throw `TypeError` synchronously; the throw is
|
|
24
|
+
* caught in the worker and re-sent as `{type:'error'}`, turning a
|
|
25
|
+
* SUCCESSFUL analysis (DB already written) into a reported FAILURE. This is
|
|
26
|
+
* the #2112 failure family on the server path, and — unlike the parse-worker
|
|
27
|
+
* result boundary — it has no clone-safety net.
|
|
28
|
+
*
|
|
29
|
+
* The parent (`api.ts`) reads only `result.repoName`; `pipelineResult`'s real
|
|
30
|
+
* consumers (CLI skill generation) call `runFullAnalysis` in-process and never
|
|
31
|
+
* cross this fork. So the worker sends an explicit allowlist of the scalar
|
|
32
|
+
* fields, JSON-safe by construction.
|
|
33
|
+
*/
|
|
34
|
+
import type { AnalyzeResult } from '../core/run-analyze.js';
|
|
35
|
+
/**
|
|
36
|
+
* The JSON-safe subset of `AnalyzeResult` that crosses the analyze-worker IPC
|
|
37
|
+
* boundary. A `Pick` allowlist — NOT `Omit<…, 'pipelineResult'>`. With `Pick`
|
|
38
|
+
* the allowlist IS the type, so the projection is exhaustive by construction:
|
|
39
|
+
* `projectAnalyzeResultForIpc`'s return literal must name exactly these keys
|
|
40
|
+
* (omitting one is a compile error), and a new field added to `AnalyzeResult`
|
|
41
|
+
* is simply absent from the wire until it is *deliberately* added here. `Omit`
|
|
42
|
+
* couldn't give that guarantee — it kept every other field, including OPTIONAL
|
|
43
|
+
* ones (e.g. `isPrimaryBranch?`), so an optional non-serializable field could be
|
|
44
|
+
* advertised by the type yet silently dropped by the runtime allowlist.
|
|
45
|
+
*
|
|
46
|
+
* `isPrimaryBranch` is intentionally excluded: the parent (`api.ts`) reads only
|
|
47
|
+
* `repoName`, and nothing consumes `isPrimaryBranch` across this fork (its CLI
|
|
48
|
+
* consumer calls `runFullAnalysis` in-process). Add a field here only when a
|
|
49
|
+
* server-side IPC consumer actually needs it — and only if it is JSON-safe.
|
|
50
|
+
*/
|
|
51
|
+
export type AnalyzeResultIpc = Pick<AnalyzeResult, 'repoName' | 'repoPath' | 'stats' | 'alreadyUpToDate' | 'ftsRepairedOnly' | 'ftsSkipped'>;
|
|
52
|
+
/**
|
|
53
|
+
* Project an `AnalyzeResult` down to the JSON-safe fields the parent consumes,
|
|
54
|
+
* dropping `pipelineResult` (the live `KnowledgeGraph`) and any other field not
|
|
55
|
+
* in the `AnalyzeResultIpc` allowlist. The return literal is exhaustive over
|
|
56
|
+
* `AnalyzeResultIpc` (a missing key is a compile error).
|
|
57
|
+
*/
|
|
58
|
+
export declare function projectAnalyzeResultForIpc(result: AnalyzeResult): AnalyzeResultIpc;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project an `AnalyzeResult` down to the JSON-safe fields the parent consumes,
|
|
3
|
+
* dropping `pipelineResult` (the live `KnowledgeGraph`) and any other field not
|
|
4
|
+
* in the `AnalyzeResultIpc` allowlist. The return literal is exhaustive over
|
|
5
|
+
* `AnalyzeResultIpc` (a missing key is a compile error).
|
|
6
|
+
*/
|
|
7
|
+
export function projectAnalyzeResultForIpc(result) {
|
|
8
|
+
return {
|
|
9
|
+
repoName: result.repoName,
|
|
10
|
+
repoPath: result.repoPath,
|
|
11
|
+
stats: result.stats,
|
|
12
|
+
alreadyUpToDate: result.alreadyUpToDate,
|
|
13
|
+
ftsRepairedOnly: result.ftsRepairedOnly,
|
|
14
|
+
ftsSkipped: result.ftsSkipped,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Child -> Parent: { type: 'error', message: string }
|
|
12
12
|
*/
|
|
13
13
|
import { runFullAnalysis } from '../core/run-analyze.js';
|
|
14
|
+
import { projectAnalyzeResultForIpc } from './analyze-worker-ipc.js';
|
|
14
15
|
import { closeLbug } from '../core/lbug/lbug-adapter.js';
|
|
15
16
|
function send(msg) {
|
|
16
17
|
process.send?.(msg);
|
|
@@ -48,7 +49,12 @@ process.on('message', async (msg) => {
|
|
|
48
49
|
send({ type: 'progress', phase: 'log', percent: -1, message });
|
|
49
50
|
},
|
|
50
51
|
});
|
|
51
|
-
|
|
52
|
+
// Send a JSON-safe projection, NOT the raw result: the IPC channel is
|
|
53
|
+
// default-JSON serialization and `result.pipelineResult` carries the live
|
|
54
|
+
// KnowledgeGraph (wasteful to materialize, silently corrupted by JSON, and
|
|
55
|
+
// a BigInt/circular value would throw and mis-report this success as a
|
|
56
|
+
// failure). See analyze-worker-ipc.ts.
|
|
57
|
+
send({ type: 'complete', result: projectAnalyzeResultForIpc(result) });
|
|
52
58
|
}
|
|
53
59
|
catch (err) {
|
|
54
60
|
send({ type: 'error', message: err?.message || 'Analysis failed' });
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branch-index primitives (#2106).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `repo-manager.ts` to keep the multi-branch slug/placement
|
|
5
|
+
* logic in one focused module. `getStoragePaths`, `loadMeta`, and the registry
|
|
6
|
+
* I/O stay in `repo-manager.ts`; this module imports the two it needs at
|
|
7
|
+
* call-time only (no module-load cross-calls), so the repo-manager ⇄
|
|
8
|
+
* branch-index import cycle is ESM-safe. `repo-manager.ts` re-exports these so
|
|
9
|
+
* existing import sites keep working unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { type RepoMeta } from './repo-manager.js';
|
|
12
|
+
/**
|
|
13
|
+
* Per-branch index summary nested under a registry entry (#2106). Records
|
|
14
|
+
* non-primary branches indexed for the same repo path so `list`, `status`, and
|
|
15
|
+
* `list_repos` can surface them without a second registry entry.
|
|
16
|
+
*/
|
|
17
|
+
export interface BranchSummary {
|
|
18
|
+
/** Git branch name this sub-index represents. */
|
|
19
|
+
branch: string;
|
|
20
|
+
indexedAt: string;
|
|
21
|
+
lastCommit: string;
|
|
22
|
+
stats?: RepoMeta['stats'];
|
|
23
|
+
}
|
|
24
|
+
/** Branch-index sub-directory name, relative to the flat `.gitnexus` storage. */
|
|
25
|
+
export declare const BRANCHES_DIR = "branches";
|
|
26
|
+
/**
|
|
27
|
+
* Filesystem-safe slug for a git branch ref (#2106).
|
|
28
|
+
*
|
|
29
|
+
* `sanitizeRepoName` alone is lossy — it maps `/`→`_`, so `feature/x` and
|
|
30
|
+
* `feature_x` would collide into the same directory. We append a short sha256
|
|
31
|
+
* of the RAW ref (mirroring `assignRepoId`'s digest fallback) so two distinct
|
|
32
|
+
* refs can never share a branch directory, while keeping the human prefix
|
|
33
|
+
* readable.
|
|
34
|
+
*/
|
|
35
|
+
export declare const branchSlug: (rawRef: string) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Decide where a freshly-analyzed branch's index lives: the flat (primary) slot
|
|
38
|
+
* or a per-branch sub-directory (#2106 KTD2).
|
|
39
|
+
*
|
|
40
|
+
* Returns `{}` for the flat/primary placement (byte-identical layout) or
|
|
41
|
+
* `{ branch }` for a `branches/<slug>/` sub-directory. The flat slot is owned by
|
|
42
|
+
* the FIRST branch indexed, recorded as `branch` in the flat `meta.json`; a
|
|
43
|
+
* different checked-out branch then auto-routes to its own sub-directory so it
|
|
44
|
+
* never overwrites the primary index.
|
|
45
|
+
*
|
|
46
|
+
* `label` is the resolved index-branch (explicit `--branch`, else the
|
|
47
|
+
* checked-out branch, else `null`). A `null` label — detached HEAD, non-git
|
|
48
|
+
* folder, or CI checkout — always maps to the flat slot.
|
|
49
|
+
*/
|
|
50
|
+
export declare const resolveBranchPlacement: (repoPath: string, label: string | null) => Promise<{
|
|
51
|
+
branch?: string;
|
|
52
|
+
}>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branch-index primitives (#2106).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `repo-manager.ts` to keep the multi-branch slug/placement
|
|
5
|
+
* logic in one focused module. `getStoragePaths`, `loadMeta`, and the registry
|
|
6
|
+
* I/O stay in `repo-manager.ts`; this module imports the two it needs at
|
|
7
|
+
* call-time only (no module-load cross-calls), so the repo-manager ⇄
|
|
8
|
+
* branch-index import cycle is ESM-safe. `repo-manager.ts` re-exports these so
|
|
9
|
+
* existing import sites keep working unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import { sanitizeRepoName } from './git.js';
|
|
13
|
+
import { getStoragePaths, loadMeta } from './repo-manager.js';
|
|
14
|
+
/** Branch-index sub-directory name, relative to the flat `.gitnexus` storage. */
|
|
15
|
+
export const BRANCHES_DIR = 'branches';
|
|
16
|
+
/**
|
|
17
|
+
* Filesystem-safe slug for a git branch ref (#2106).
|
|
18
|
+
*
|
|
19
|
+
* `sanitizeRepoName` alone is lossy — it maps `/`→`_`, so `feature/x` and
|
|
20
|
+
* `feature_x` would collide into the same directory. We append a short sha256
|
|
21
|
+
* of the RAW ref (mirroring `assignRepoId`'s digest fallback) so two distinct
|
|
22
|
+
* refs can never share a branch directory, while keeping the human prefix
|
|
23
|
+
* readable.
|
|
24
|
+
*/
|
|
25
|
+
export const branchSlug = (rawRef) => {
|
|
26
|
+
const safe = sanitizeRepoName(rawRef);
|
|
27
|
+
const hash = createHash('sha256').update(rawRef).digest('hex').slice(0, 8);
|
|
28
|
+
return `${safe}-${hash}`;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Decide where a freshly-analyzed branch's index lives: the flat (primary) slot
|
|
32
|
+
* or a per-branch sub-directory (#2106 KTD2).
|
|
33
|
+
*
|
|
34
|
+
* Returns `{}` for the flat/primary placement (byte-identical layout) or
|
|
35
|
+
* `{ branch }` for a `branches/<slug>/` sub-directory. The flat slot is owned by
|
|
36
|
+
* the FIRST branch indexed, recorded as `branch` in the flat `meta.json`; a
|
|
37
|
+
* different checked-out branch then auto-routes to its own sub-directory so it
|
|
38
|
+
* never overwrites the primary index.
|
|
39
|
+
*
|
|
40
|
+
* `label` is the resolved index-branch (explicit `--branch`, else the
|
|
41
|
+
* checked-out branch, else `null`). A `null` label — detached HEAD, non-git
|
|
42
|
+
* folder, or CI checkout — always maps to the flat slot.
|
|
43
|
+
*/
|
|
44
|
+
export const resolveBranchPlacement = async (repoPath, label) => {
|
|
45
|
+
// Detached HEAD / non-git / no label → flat (CI-safe, byte-identical).
|
|
46
|
+
if (!label)
|
|
47
|
+
return {};
|
|
48
|
+
const { storagePath } = getStoragePaths(repoPath);
|
|
49
|
+
const flatMeta = await loadMeta(storagePath);
|
|
50
|
+
// The flat slot's owner is authoritative ONLY when it is a non-empty string.
|
|
51
|
+
// A corrupt/hand-edited meta (empty string, or a non-string value that slips
|
|
52
|
+
// past JSON typing) must not be trusted to route the real primary into a
|
|
53
|
+
// sub-directory (#2106 review R5).
|
|
54
|
+
const owner = flatMeta && typeof flatMeta.branch === 'string' && flatMeta.branch.length > 0
|
|
55
|
+
? flatMeta.branch
|
|
56
|
+
: undefined;
|
|
57
|
+
// Fresh repo (no flat index) or legacy/unstamped flat index (no recorded
|
|
58
|
+
// owner): the current label claims/adopts the flat slot. The legacy case
|
|
59
|
+
// preserves today's overwrite-in-place behavior until the slot is stamped.
|
|
60
|
+
if (!owner)
|
|
61
|
+
return {};
|
|
62
|
+
// Flat slot is owned. Same branch → flat; otherwise this branch gets its own
|
|
63
|
+
// sub-directory.
|
|
64
|
+
return owner === label ? {} : { branch: label };
|
|
65
|
+
};
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -118,6 +118,17 @@ export declare const getRemoteOriginUrl: (repoPath: string) => string | null;
|
|
|
118
118
|
* git is unavailable, so callers can fall back to a configured/default branch.
|
|
119
119
|
*/
|
|
120
120
|
export declare const getDefaultBranch: (repoPath: string) => string | null;
|
|
121
|
+
/**
|
|
122
|
+
* Name of the currently checked-out branch, or `null` when HEAD is detached
|
|
123
|
+
* (CI checkouts, `git checkout <sha>`), the directory is not a git worktree, or
|
|
124
|
+
* git is unavailable.
|
|
125
|
+
*
|
|
126
|
+
* `git rev-parse --abbrev-ref HEAD` prints the literal `HEAD` for a detached
|
|
127
|
+
* checkout. We map that (and empty output) to `null` so callers fall back to the
|
|
128
|
+
* flat/default index rather than ever creating a branch literally named
|
|
129
|
+
* "HEAD" (#2106).
|
|
130
|
+
*/
|
|
131
|
+
export declare const getCurrentBranch: (repoPath: string) => string | null;
|
|
121
132
|
/**
|
|
122
133
|
* Sanitize a repository name to prevent argument injection and ensure
|
|
123
134
|
* cross-platform filesystem compatibility.
|
package/dist/storage/git.js
CHANGED
|
@@ -295,6 +295,34 @@ export const getDefaultBranch = (repoPath) => {
|
|
|
295
295
|
return null;
|
|
296
296
|
}
|
|
297
297
|
};
|
|
298
|
+
/**
|
|
299
|
+
* Name of the currently checked-out branch, or `null` when HEAD is detached
|
|
300
|
+
* (CI checkouts, `git checkout <sha>`), the directory is not a git worktree, or
|
|
301
|
+
* git is unavailable.
|
|
302
|
+
*
|
|
303
|
+
* `git rev-parse --abbrev-ref HEAD` prints the literal `HEAD` for a detached
|
|
304
|
+
* checkout. We map that (and empty output) to `null` so callers fall back to the
|
|
305
|
+
* flat/default index rather than ever creating a branch literally named
|
|
306
|
+
* "HEAD" (#2106).
|
|
307
|
+
*/
|
|
308
|
+
export const getCurrentBranch = (repoPath) => {
|
|
309
|
+
try {
|
|
310
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
311
|
+
cwd: repoPath,
|
|
312
|
+
// Suppress stderr -- see getCurrentCommit comment and #1172.
|
|
313
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
314
|
+
windowsHide: true,
|
|
315
|
+
})
|
|
316
|
+
.toString()
|
|
317
|
+
.trim();
|
|
318
|
+
if (!branch || branch === 'HEAD')
|
|
319
|
+
return null;
|
|
320
|
+
return branch;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
298
326
|
/**
|
|
299
327
|
* Sanitize a repository name to prevent argument injection and ensure
|
|
300
328
|
* cross-platform filesystem compatibility.
|
|
@@ -149,6 +149,9 @@ export const slimParseWorkerResultsForCache = (chunkResults) => {
|
|
|
149
149
|
assignments: [],
|
|
150
150
|
constructorBindings: [],
|
|
151
151
|
parsedFiles: [],
|
|
152
|
+
// #2112: a clone-safety skip list is per-run telemetry, not graph data —
|
|
153
|
+
// replay ignores it. Drop it so it doesn't bloat the cached shard.
|
|
154
|
+
skippedPaths: [],
|
|
152
155
|
});
|
|
153
156
|
}
|
|
154
157
|
return slim;
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Also maintains a global registry at ~/.gitnexus/registry.json
|
|
6
6
|
* so the MCP server can discover indexed repos from any cwd.
|
|
7
7
|
*/
|
|
8
|
+
import { branchSlug, resolveBranchPlacement, type BranchSummary } from './branch-index.js';
|
|
9
|
+
export { branchSlug, resolveBranchPlacement };
|
|
10
|
+
export type { BranchSummary };
|
|
8
11
|
/**
|
|
9
12
|
* Normalise a repo path for registry comparison across platforms
|
|
10
13
|
* (#664 review feedback from @evander-wang).
|
|
@@ -83,6 +86,23 @@ export interface RepoMeta {
|
|
|
83
86
|
/** Number of files in the writable set, for diagnostic logs. */
|
|
84
87
|
toWriteCount: number;
|
|
85
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Name of the git branch this index represents (#2106). Absent for the
|
|
91
|
+
* default/legacy single-branch case so the flat `meta.json` stays
|
|
92
|
+
* byte-identical to pre-multi-branch output. When present in the FLAT
|
|
93
|
+
* `meta.json`, it records which branch "owns" the flat slot (the first
|
|
94
|
+
* branch indexed); per-branch indexes under `branches/<slug>/` always carry
|
|
95
|
+
* their own `branch`.
|
|
96
|
+
*/
|
|
97
|
+
branch?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The parse-cache chunk keys this branch's index needs (#2106 R6). The
|
|
100
|
+
* parse-cache and durable parsedfile store live ONCE at the repo root and are
|
|
101
|
+
* shared across branches; recording each branch's live chunk keys lets the
|
|
102
|
+
* prune step union them so re-analyzing one branch doesn't evict another
|
|
103
|
+
* branch's still-live shards. Additive/optional; absent in legacy metas.
|
|
104
|
+
*/
|
|
105
|
+
cacheKeys?: string[];
|
|
86
106
|
}
|
|
87
107
|
/**
|
|
88
108
|
* Bumped whenever incremental-indexing invariants change incompatibly.
|
|
@@ -107,15 +127,33 @@ export interface RegistryEntry {
|
|
|
107
127
|
/** See {@link RepoMeta.remoteUrl}. Mirrored from meta at register time. */
|
|
108
128
|
remoteUrl?: string;
|
|
109
129
|
stats?: RepoMeta['stats'];
|
|
130
|
+
/**
|
|
131
|
+
* Branch name owning the flat/primary index (#2106). Mirrors the flat
|
|
132
|
+
* `meta.branch`. Absent for legacy single-branch entries and non-git repos —
|
|
133
|
+
* additive and backward compatible.
|
|
134
|
+
*/
|
|
135
|
+
branch?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Non-primary branch indexes for this same path (#2106). Absent when only the
|
|
138
|
+
* primary branch is indexed, preserving the one-entry-per-path model and the
|
|
139
|
+
* legacy registry shape.
|
|
140
|
+
*/
|
|
141
|
+
branches?: BranchSummary[];
|
|
110
142
|
}
|
|
111
143
|
/**
|
|
112
144
|
* Get the .gitnexus storage path for a repository
|
|
113
145
|
*/
|
|
114
146
|
export declare const getStoragePath: (repoPath: string) => string;
|
|
115
147
|
/**
|
|
116
|
-
* Get paths to key storage files
|
|
148
|
+
* Get paths to key storage files.
|
|
149
|
+
*
|
|
150
|
+
* `storagePath` is ALWAYS the flat `<repo>/.gitnexus` — content-addressed
|
|
151
|
+
* caches (`parse-cache/`, `parsedfile-store/`) live there and are shared
|
|
152
|
+
* across branches (#2106 KTD7). When `branch` is provided, only `lbugPath` and
|
|
153
|
+
* `metaPath` are scoped under `branches/<slug>/`; the flat call (no `branch`)
|
|
154
|
+
* returns byte-identical paths to the pre-multi-branch behavior.
|
|
117
155
|
*/
|
|
118
|
-
export declare const getStoragePaths: (repoPath: string) => {
|
|
156
|
+
export declare const getStoragePaths: (repoPath: string, branch?: string) => {
|
|
119
157
|
storagePath: string;
|
|
120
158
|
lbugPath: string;
|
|
121
159
|
metaPath: string;
|
|
@@ -213,6 +251,14 @@ export interface RegisterRepoOptions {
|
|
|
213
251
|
* re-run the full pipeline.
|
|
214
252
|
*/
|
|
215
253
|
allowDuplicateName?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Non-primary branch this run indexed (#2106). When set, the branch's
|
|
256
|
+
* summary is upserted into the entry's `branches[]` and the primary
|
|
257
|
+
* top-level fields are left untouched. When `undefined`, this is a
|
|
258
|
+
* primary/flat run that refreshes the top-level fields (and preserves any
|
|
259
|
+
* existing branch summaries).
|
|
260
|
+
*/
|
|
261
|
+
branch?: string;
|
|
216
262
|
}
|
|
217
263
|
/**
|
|
218
264
|
* Thrown by {@link registerRepo} when a requested name is already in
|
|
@@ -262,6 +308,15 @@ export declare const registerRepo: (repoPath: string, meta: RepoMeta, opts?: Reg
|
|
|
262
308
|
* Called after `gitnexus clean`.
|
|
263
309
|
*/
|
|
264
310
|
export declare const unregisterRepo: (repoPath: string) => Promise<void>;
|
|
311
|
+
/**
|
|
312
|
+
* Remove a single non-primary branch's summary from a repo's registry entry
|
|
313
|
+
* (#2106 R7). Called by `gitnexus clean --branch`. Returns `true` when a
|
|
314
|
+
* matching `branches[]` summary was found and removed; `false` otherwise (so
|
|
315
|
+
* the CLI can report "no such indexed branch" without crashing). The top-level
|
|
316
|
+
* primary entry is left intact; an empty `branches[]` is dropped to keep the
|
|
317
|
+
* registry shape legacy-clean.
|
|
318
|
+
*/
|
|
319
|
+
export declare const removeBranchIndex: (repoPath: string, branch: string) => Promise<boolean>;
|
|
265
320
|
/**
|
|
266
321
|
* Thrown by {@link resolveRegistryEntry} when no registered repo matches
|
|
267
322
|
* the caller's target string (by alias, basename, remote-inferred name,
|
|
@@ -11,6 +11,10 @@ import path from 'path';
|
|
|
11
11
|
import os from 'os';
|
|
12
12
|
import { getInferredRepoName, resolveRepoIdentityRoot } from './git.js';
|
|
13
13
|
import { logger } from '../core/logger.js';
|
|
14
|
+
import { branchSlug, BRANCHES_DIR, resolveBranchPlacement, } from './branch-index.js';
|
|
15
|
+
// Re-export the #2106 branch primitives (extracted to branch-index.ts, R10) so
|
|
16
|
+
// existing `repo-manager` import sites and tests keep working unchanged.
|
|
17
|
+
export { branchSlug, resolveBranchPlacement };
|
|
14
18
|
/**
|
|
15
19
|
* Normalise a repo path for registry comparison across platforms
|
|
16
20
|
* (#664 review feedback from @evander-wang).
|
|
@@ -64,14 +68,21 @@ export const getStoragePath = (repoPath) => {
|
|
|
64
68
|
return path.join(path.resolve(repoPath), GITNEXUS_DIR);
|
|
65
69
|
};
|
|
66
70
|
/**
|
|
67
|
-
* Get paths to key storage files
|
|
71
|
+
* Get paths to key storage files.
|
|
72
|
+
*
|
|
73
|
+
* `storagePath` is ALWAYS the flat `<repo>/.gitnexus` — content-addressed
|
|
74
|
+
* caches (`parse-cache/`, `parsedfile-store/`) live there and are shared
|
|
75
|
+
* across branches (#2106 KTD7). When `branch` is provided, only `lbugPath` and
|
|
76
|
+
* `metaPath` are scoped under `branches/<slug>/`; the flat call (no `branch`)
|
|
77
|
+
* returns byte-identical paths to the pre-multi-branch behavior.
|
|
68
78
|
*/
|
|
69
|
-
export const getStoragePaths = (repoPath) => {
|
|
79
|
+
export const getStoragePaths = (repoPath, branch) => {
|
|
70
80
|
const storagePath = getStoragePath(repoPath);
|
|
81
|
+
const baseDir = branch ? path.join(storagePath, BRANCHES_DIR, branchSlug(branch)) : storagePath;
|
|
71
82
|
return {
|
|
72
83
|
storagePath,
|
|
73
|
-
lbugPath: path.join(
|
|
74
|
-
metaPath: path.join(
|
|
84
|
+
lbugPath: path.join(baseDir, 'lbug'),
|
|
85
|
+
metaPath: path.join(baseDir, 'meta.json'),
|
|
75
86
|
};
|
|
76
87
|
};
|
|
77
88
|
/**
|
|
@@ -310,7 +321,13 @@ export const readRegistry = async () => {
|
|
|
310
321
|
const writeRegistry = async (entries) => {
|
|
311
322
|
const dir = getGlobalDir();
|
|
312
323
|
await fs.mkdir(dir, { recursive: true });
|
|
313
|
-
|
|
324
|
+
// Atomic tmp+rename (mirrors saveMeta): a crash mid-write can never leave a
|
|
325
|
+
// truncated/half-written registry.json that the next load would treat as
|
|
326
|
+
// empty and silently drop every registered repo (#2106 R9).
|
|
327
|
+
const target = getGlobalRegistryPath();
|
|
328
|
+
const tmp = `${target}.tmp`;
|
|
329
|
+
await fs.writeFile(tmp, JSON.stringify(entries, null, 2), 'utf-8');
|
|
330
|
+
await fs.rename(tmp, target);
|
|
314
331
|
};
|
|
315
332
|
/**
|
|
316
333
|
* Thrown by {@link registerRepo} when a requested name is already in
|
|
@@ -466,22 +483,89 @@ export const registerRepo = async (repoPath, meta, opts) => {
|
|
|
466
483
|
throw new RegistryNameCollisionError(name, collidingEntry.path, resolved);
|
|
467
484
|
}
|
|
468
485
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
486
|
+
// This run's branch summary (non-primary runs only); hoisted so the
|
|
487
|
+
// re-read-before-write merge below can re-apply it against a fresh snapshot.
|
|
488
|
+
const summary = opts?.branch
|
|
489
|
+
? {
|
|
490
|
+
branch: opts.branch,
|
|
491
|
+
indexedAt: meta.indexedAt,
|
|
492
|
+
lastCommit: meta.lastCommit,
|
|
493
|
+
stats: meta.stats,
|
|
494
|
+
}
|
|
495
|
+
: null;
|
|
496
|
+
let entry;
|
|
497
|
+
if (summary) {
|
|
498
|
+
// Non-primary branch run (#2106): keep the primary's top-level fields and
|
|
499
|
+
// upsert this branch into branches[]. One entry per path is preserved.
|
|
500
|
+
// When the registry entry is missing (lost/rebuilt registry.json), rebuild
|
|
501
|
+
// the primary top-level from the FLAT meta.json rather than this branch's
|
|
502
|
+
// meta, so `--branch <primary>` can still resolve (#2106 review).
|
|
503
|
+
const flatMeta = existing ? null : await loadMeta(storagePath);
|
|
504
|
+
const base = existing ?? {
|
|
505
|
+
name,
|
|
506
|
+
path: resolved,
|
|
507
|
+
storagePath,
|
|
508
|
+
indexedAt: flatMeta?.indexedAt ?? meta.indexedAt,
|
|
509
|
+
lastCommit: flatMeta?.lastCommit ?? meta.lastCommit,
|
|
510
|
+
remoteUrl: flatMeta?.remoteUrl ?? meta.remoteUrl,
|
|
511
|
+
stats: flatMeta?.stats ?? meta.stats,
|
|
512
|
+
...(flatMeta?.branch ? { branch: flatMeta.branch } : {}),
|
|
513
|
+
};
|
|
514
|
+
const branches = (base.branches ?? []).filter((b) => b.branch !== summary.branch);
|
|
515
|
+
branches.push(summary);
|
|
516
|
+
entry = { ...base, name, branches };
|
|
480
517
|
}
|
|
481
518
|
else {
|
|
482
|
-
|
|
519
|
+
// Primary/flat run: refresh top-level fields, preserve any branch summaries
|
|
520
|
+
// already recorded for this path so a primary re-analyze does not drop them.
|
|
521
|
+
entry = {
|
|
522
|
+
name,
|
|
523
|
+
path: resolved,
|
|
524
|
+
storagePath,
|
|
525
|
+
indexedAt: meta.indexedAt,
|
|
526
|
+
lastCommit: meta.lastCommit,
|
|
527
|
+
remoteUrl: meta.remoteUrl,
|
|
528
|
+
stats: meta.stats,
|
|
529
|
+
...(meta.branch ? { branch: meta.branch } : {}),
|
|
530
|
+
...(existing?.branches ? { branches: existing.branches } : {}),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// Re-read immediately before writing to narrow the lost-update window (#2106
|
|
534
|
+
// R9): re-derive THIS run's delta against the FRESHEST snapshot so a
|
|
535
|
+
// concurrent change to the OTHER axis (a branch upsert vs a primary refresh)
|
|
536
|
+
// survives instead of being clobbered by a stale entry-time view.
|
|
537
|
+
const fresh = await readRegistry();
|
|
538
|
+
const freshIdx = fresh.findIndex((e) => {
|
|
539
|
+
const a = canonicalizePath(e.path);
|
|
540
|
+
return process.platform === 'win32'
|
|
541
|
+
? a.toLowerCase() === canonicalInput.toLowerCase()
|
|
542
|
+
: a === canonicalInput;
|
|
543
|
+
});
|
|
544
|
+
const freshExisting = freshIdx >= 0 ? fresh[freshIdx] : null;
|
|
545
|
+
let merged;
|
|
546
|
+
if (summary) {
|
|
547
|
+
// Branch run: keep the FRESH top-level + branches, just upsert our summary.
|
|
548
|
+
const base = freshExisting ?? entry;
|
|
549
|
+
const branches = (base.branches ?? []).filter((b) => b.branch !== summary.branch);
|
|
550
|
+
branches.push(summary);
|
|
551
|
+
merged = { ...base, name, branches };
|
|
483
552
|
}
|
|
484
|
-
|
|
553
|
+
else {
|
|
554
|
+
// Primary run: apply our refreshed top-level, but defer to the FRESH
|
|
555
|
+
// branches[] (a concurrent branch upsert or `clean --branch` wins).
|
|
556
|
+
merged = { ...entry };
|
|
557
|
+
if (freshExisting?.branches)
|
|
558
|
+
merged.branches = freshExisting.branches;
|
|
559
|
+
else
|
|
560
|
+
delete merged.branches;
|
|
561
|
+
}
|
|
562
|
+
if (freshIdx >= 0) {
|
|
563
|
+
fresh[freshIdx] = merged;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
fresh.push(merged);
|
|
567
|
+
}
|
|
568
|
+
await writeRegistry(fresh);
|
|
485
569
|
return name;
|
|
486
570
|
};
|
|
487
571
|
/**
|
|
@@ -500,6 +584,36 @@ export const unregisterRepo = async (repoPath) => {
|
|
|
500
584
|
const filtered = entries.filter((e) => !matches(canonicalizePath(e.path), resolved));
|
|
501
585
|
await writeRegistry(filtered);
|
|
502
586
|
};
|
|
587
|
+
/**
|
|
588
|
+
* Remove a single non-primary branch's summary from a repo's registry entry
|
|
589
|
+
* (#2106 R7). Called by `gitnexus clean --branch`. Returns `true` when a
|
|
590
|
+
* matching `branches[]` summary was found and removed; `false` otherwise (so
|
|
591
|
+
* the CLI can report "no such indexed branch" without crashing). The top-level
|
|
592
|
+
* primary entry is left intact; an empty `branches[]` is dropped to keep the
|
|
593
|
+
* registry shape legacy-clean.
|
|
594
|
+
*/
|
|
595
|
+
export const removeBranchIndex = async (repoPath, branch) => {
|
|
596
|
+
const resolved = canonicalizePath(repoPath);
|
|
597
|
+
const matches = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
598
|
+
const entries = await readRegistry();
|
|
599
|
+
const idx = entries.findIndex((e) => matches(canonicalizePath(e.path), resolved));
|
|
600
|
+
if (idx < 0)
|
|
601
|
+
return false;
|
|
602
|
+
const entry = entries[idx];
|
|
603
|
+
const before = entry.branches?.length ?? 0;
|
|
604
|
+
if (!entry.branches || before === 0)
|
|
605
|
+
return false;
|
|
606
|
+
const remaining = entry.branches.filter((b) => b.branch !== branch);
|
|
607
|
+
if (remaining.length === before)
|
|
608
|
+
return false; // branch not recorded
|
|
609
|
+
if (remaining.length > 0)
|
|
610
|
+
entry.branches = remaining;
|
|
611
|
+
else
|
|
612
|
+
delete entry.branches;
|
|
613
|
+
entries[idx] = entry;
|
|
614
|
+
await writeRegistry(entries);
|
|
615
|
+
return true;
|
|
616
|
+
};
|
|
503
617
|
/**
|
|
504
618
|
* Thrown by {@link resolveRegistryEntry} when no registered repo matches
|
|
505
619
|
* the caller's target string (by alias, basename, remote-inferred name,
|
|
@@ -91,10 +91,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
|
|
|
91
91
|
return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Whether opt-in diagnostics should be written to the hook's stderr. Strict
|
|
96
|
+
* hook runners validate hook output, so normal, non-error skip paths must stay
|
|
97
|
+
* silent unless the operator explicitly asks for diagnostics via GITNEXUS_DEBUG.
|
|
98
|
+
* See issue #1913.
|
|
99
|
+
*/
|
|
100
|
+
function isDebugEnabled() {
|
|
101
|
+
return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
function extractAugmentContext(stderr) {
|
|
95
105
|
const output = (stderr || '').trim();
|
|
96
106
|
const marker = output.indexOf('[GitNexus]');
|
|
97
|
-
const debug =
|
|
107
|
+
const debug = isDebugEnabled();
|
|
98
108
|
if (debug && output.length > 0) {
|
|
99
109
|
// Emit the FULL discarded prefix (everything before the marker, or all of
|
|
100
110
|
// it when no marker is present) so suppressed diagnostics — LadybugDB lock
|
|
@@ -258,8 +268,14 @@ function buildAfterToolContext(input) {
|
|
|
258
268
|
if (/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) {
|
|
259
269
|
const hint = buildStaleIndexHint(gitNexusDir, cwd);
|
|
260
270
|
if (hint) {
|
|
261
|
-
|
|
271
|
+
// The hint always reaches the agent via additionalContext (parts). Mirror
|
|
272
|
+
// it to stderr (for terminal users) only under GITNEXUS_DEBUG, so strict
|
|
273
|
+
// hook runners see no unexpected output on this normal path (#1913). The
|
|
274
|
+
// claude hook never mirrored this to stderr — this aligns the two adapters.
|
|
262
275
|
parts.push(hint);
|
|
276
|
+
if (isDebugEnabled()) {
|
|
277
|
+
process.stderr.write(`${hint}\n`);
|
|
278
|
+
}
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
281
|
}
|
|
@@ -269,7 +285,11 @@ function buildAfterToolContext(input) {
|
|
|
269
285
|
|
|
270
286
|
function runAugment(gitNexusDir, cwd, pattern) {
|
|
271
287
|
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
272
|
-
|
|
288
|
+
// Normal skip path: the MCP server owns the DB. Stay silent for strict
|
|
289
|
+
// hook runners (issue #1913); surface the reason only under GITNEXUS_DEBUG.
|
|
290
|
+
if (isDebugEnabled()) {
|
|
291
|
+
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
292
|
+
}
|
|
273
293
|
return '';
|
|
274
294
|
}
|
|
275
295
|
const release = acquireHookSlot(gitNexusDir);
|
|
@@ -338,7 +358,7 @@ function main() {
|
|
|
338
358
|
const handler = handlers[input.hook_event_name || ''];
|
|
339
359
|
if (handler) handler(input);
|
|
340
360
|
} catch (err) {
|
|
341
|
-
if (
|
|
361
|
+
if (isDebugEnabled()) {
|
|
342
362
|
console.error('GitNexus antigravity hook error:', (err.message || '').slice(0, 200));
|
|
343
363
|
}
|
|
344
364
|
}
|