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
|
@@ -27,6 +27,11 @@ export declare const computeFileHash: (absPath: string) => Promise<string | null
|
|
|
27
27
|
* fail to read are omitted from the result map.
|
|
28
28
|
*/
|
|
29
29
|
export declare const computeFileHashes: (repoPath: string, relPaths: readonly string[]) => Promise<Map<string, string>>;
|
|
30
|
+
/** Like {@link computeFileHashes}, but keeps paths whose content could not be read. */
|
|
31
|
+
export declare const computeFileHashesDetailed: (repoPath: string, relPaths: readonly string[]) => Promise<{
|
|
32
|
+
hashes: Map<string, string>;
|
|
33
|
+
unreadable: string[];
|
|
34
|
+
}>;
|
|
30
35
|
/** Result of comparing the current on-disk hashes against stored ones. */
|
|
31
36
|
export interface FileHashDiff {
|
|
32
37
|
/** Files whose content hash differs from stored. */
|
|
@@ -39,18 +39,28 @@ export const computeFileHash = async (absPath) => {
|
|
|
39
39
|
* fail to read are omitted from the result map.
|
|
40
40
|
*/
|
|
41
41
|
export const computeFileHashes = async (repoPath, relPaths) => {
|
|
42
|
-
const
|
|
42
|
+
const { hashes } = await computeFileHashesDetailed(repoPath, relPaths);
|
|
43
|
+
return hashes;
|
|
44
|
+
};
|
|
45
|
+
/** Like {@link computeFileHashes}, but keeps paths whose content could not be read. */
|
|
46
|
+
export const computeFileHashesDetailed = async (repoPath, relPaths) => {
|
|
47
|
+
const hashes = new Map();
|
|
48
|
+
const unreadable = [];
|
|
43
49
|
const BATCH = 100;
|
|
44
50
|
for (const batch of chunk(relPaths, BATCH)) {
|
|
45
51
|
const results = await Promise.all(batch.map(async (rel) => {
|
|
46
52
|
const h = await computeFileHash(path.join(repoPath, rel));
|
|
47
|
-
return
|
|
53
|
+
return { rel, h };
|
|
48
54
|
}));
|
|
49
|
-
for (const
|
|
50
|
-
if (
|
|
51
|
-
|
|
55
|
+
for (const { rel, h } of results) {
|
|
56
|
+
if (h)
|
|
57
|
+
hashes.set(rel, h);
|
|
58
|
+
else
|
|
59
|
+
unreadable.push(rel);
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
|
-
|
|
62
|
+
unreadable.sort();
|
|
63
|
+
return { hashes, unreadable };
|
|
54
64
|
};
|
|
55
65
|
/**
|
|
56
66
|
* Diff a current hash map against a previously stored one.
|
|
@@ -28,3 +28,27 @@ export declare function retryRename(src: string, dst: string, attempts?: number)
|
|
|
28
28
|
* retry backoff (and, under a lock, make everyone else wait for it).
|
|
29
29
|
*/
|
|
30
30
|
export declare function writeFileAtomic(targetPath: string, data: string, attempts?: number): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Publish `src` at `dst` without ever writing through the inode currently
|
|
33
|
+
* named by `dst` (#3090). Fast path is `fs.link`. Any failure (EEXIST, EXDEV,
|
|
34
|
+
* EPERM, unsupported hardlinks) copies into a unique sibling and
|
|
35
|
+
* {@link retryRename}s over `dst`. POSIX rename of a file over an existing
|
|
36
|
+
* file unlinks the old name; it does not open or truncate that inode.
|
|
37
|
+
*
|
|
38
|
+
* Invariant: may replace the destination directory entry; must never modify
|
|
39
|
+
* the inode that entry currently references. There is no dest unlink and no
|
|
40
|
+
* in-place `copyFile(src, dst)`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function linkOrCopyFile(src: string, dst: string): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Binary sibling of {@link writeFileAtomic}. Same random tmp + `'wx'` + `0o600`
|
|
45
|
+
* + rename contract; used for optional V8 cache sidecars that cannot go through
|
|
46
|
+
* a UTF-8 `writeFile`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeFileAtomicBytes(targetPath: string, data: Uint8Array, attempts?: number): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Sync binary publish for parse workers. Same exclusive-tmp + mode contract as
|
|
51
|
+
* {@link writeFileAtomicBytes}; rename is not retried (the worker is not the
|
|
52
|
+
* Windows multi-reader case `retryRename` exists for).
|
|
53
|
+
*/
|
|
54
|
+
export declare function writeFileAtomicBytesSync(targetPath: string, data: Uint8Array): void;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* core/group/ import (the established direction is core/group/ -> storage/,
|
|
7
7
|
* e.g. core/group/service.ts already imports loadMeta from here).
|
|
8
8
|
*/
|
|
9
|
-
import fsp from 'fs
|
|
9
|
+
import { closeSync, openSync, promises as fsp, renameSync, unlinkSync, writeSync } from 'node:fs';
|
|
10
10
|
import { randomBytes } from 'crypto';
|
|
11
11
|
const RETRY_CODES = new Set(['EBUSY', 'EPERM', 'EACCES']);
|
|
12
12
|
/**
|
|
@@ -52,7 +52,7 @@ export async function retryRename(src, dst, attempts = 3) {
|
|
|
52
52
|
* retry backoff (and, under a lock, make everyone else wait for it).
|
|
53
53
|
*/
|
|
54
54
|
export async function writeFileAtomic(targetPath, data, attempts) {
|
|
55
|
-
const tmpPath =
|
|
55
|
+
const tmpPath = tmpBeside(targetPath);
|
|
56
56
|
const handle = await fsp.open(tmpPath, 'wx', 0o600);
|
|
57
57
|
try {
|
|
58
58
|
try {
|
|
@@ -68,3 +68,87 @@ export async function writeFileAtomic(targetPath, data, attempts) {
|
|
|
68
68
|
throw err;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
function tmpBeside(targetPath) {
|
|
72
|
+
return `${targetPath}.tmp.${randomBytes(8).toString('hex')}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Publish `src` at `dst` without ever writing through the inode currently
|
|
76
|
+
* named by `dst` (#3090). Fast path is `fs.link`. Any failure (EEXIST, EXDEV,
|
|
77
|
+
* EPERM, unsupported hardlinks) copies into a unique sibling and
|
|
78
|
+
* {@link retryRename}s over `dst`. POSIX rename of a file over an existing
|
|
79
|
+
* file unlinks the old name; it does not open or truncate that inode.
|
|
80
|
+
*
|
|
81
|
+
* Invariant: may replace the destination directory entry; must never modify
|
|
82
|
+
* the inode that entry currently references. There is no dest unlink and no
|
|
83
|
+
* in-place `copyFile(src, dst)`.
|
|
84
|
+
*/
|
|
85
|
+
export async function linkOrCopyFile(src, dst) {
|
|
86
|
+
try {
|
|
87
|
+
await fsp.link(src, dst);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* any link failure → publish a new inode via tmp + rename */
|
|
92
|
+
}
|
|
93
|
+
const tmp = tmpBeside(dst);
|
|
94
|
+
try {
|
|
95
|
+
await fsp.copyFile(src, tmp);
|
|
96
|
+
await retryRename(tmp, dst);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
await fsp.unlink(tmp).catch(() => { });
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Binary sibling of {@link writeFileAtomic}. Same random tmp + `'wx'` + `0o600`
|
|
105
|
+
* + rename contract; used for optional V8 cache sidecars that cannot go through
|
|
106
|
+
* a UTF-8 `writeFile`.
|
|
107
|
+
*/
|
|
108
|
+
export async function writeFileAtomicBytes(targetPath, data, attempts) {
|
|
109
|
+
const tmpPath = tmpBeside(targetPath);
|
|
110
|
+
const handle = await fsp.open(tmpPath, 'wx', 0o600);
|
|
111
|
+
try {
|
|
112
|
+
try {
|
|
113
|
+
await handle.writeFile(data);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await handle.close();
|
|
117
|
+
}
|
|
118
|
+
await retryRename(tmpPath, targetPath, attempts);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
await fsp.unlink(tmpPath).catch(() => { });
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Sync binary publish for parse workers. Same exclusive-tmp + mode contract as
|
|
127
|
+
* {@link writeFileAtomicBytes}; rename is not retried (the worker is not the
|
|
128
|
+
* Windows multi-reader case `retryRename` exists for).
|
|
129
|
+
*/
|
|
130
|
+
export function writeFileAtomicBytesSync(targetPath, data) {
|
|
131
|
+
const tmpPath = tmpBeside(targetPath);
|
|
132
|
+
const fd = openSync(tmpPath, 'wx', 0o600);
|
|
133
|
+
try {
|
|
134
|
+
try {
|
|
135
|
+
let offset = 0;
|
|
136
|
+
while (offset < data.byteLength) {
|
|
137
|
+
offset += writeSync(fd, data, offset);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
closeSync(fd);
|
|
142
|
+
}
|
|
143
|
+
renameSync(tmpPath, targetPath);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
try {
|
|
147
|
+
unlinkSync(tmpPath);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* leftover tmp is unlinked on the next exclusive create */
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* True when the working tree has uncommitted changes that analyze would
|
|
3
|
-
* re-index, even at a matching HEAD. Excludes
|
|
4
|
-
* analyze
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* "dirty"
|
|
3
|
+
* re-index, even at a matching HEAD. Excludes GITNEXUS_MANAGED_PATHS so
|
|
4
|
+
* GitNexus's own analyze output never counts as dirty (regression vs PR #1233
|
|
5
|
+
* behavior); whole directory trees are excluded, not just their root entries,
|
|
6
|
+
* because the skill mirror writes across .agents/skills/ and deeper paths.
|
|
7
|
+
* Conservative on any git failure.
|
|
8
|
+
*
|
|
9
|
+
* This drives `analyze`'s up-to-date fast path. It is deliberately coarse:
|
|
10
|
+
* a false "dirty" here costs only a hash diff that finds nothing. `status`
|
|
11
|
+
* reaches for the per-file comparison in core/index-content-drift.ts instead,
|
|
12
|
+
* because there the same false positive is a verdict the user cannot clear
|
|
13
|
+
* (#3077), and falls back to this only when that comparison cannot run.
|
|
11
14
|
*/
|
|
12
15
|
export declare const isWorkingTreeDirty: (repoPath: string) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Repo-relative paths `git status` reports as dirty or untracked, using the
|
|
18
|
+
* same managed-path excludes as {@link isWorkingTreeDirty}, plus tracked paths
|
|
19
|
+
* whose assume-unchanged or skip-worktree bits can hide content changes from
|
|
20
|
+
* porcelain. `null` means either query failed — callers must not treat that as
|
|
21
|
+
* a clean tree.
|
|
22
|
+
*/
|
|
23
|
+
export declare const listWorkingTreeDirtyPaths: (repoPath: string) => string[] | null;
|
|
13
24
|
/**
|
|
14
25
|
* Snapshot, per candidate file, whether it is safe for `selfCommitContextFiles`
|
|
15
26
|
* to auto-commit — call this BEFORE `analyze` writes AGENTS.md/CLAUDE.md.
|
package/dist/storage/git.js
CHANGED
|
@@ -4,37 +4,27 @@ import path from 'path';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { logger } from '../core/logger.js';
|
|
6
6
|
import { toZeroBasedLine } from '../core/ingestion/utils/line-base.js';
|
|
7
|
+
import { GITNEXUS_MANAGED_PATH_EXCLUDES, isGitNexusManagedPath } from './gitnexus-managed-paths.js';
|
|
7
8
|
// Git utilities for repository detection, commit tracking, and diff analysis
|
|
8
9
|
const chompGitOutput = (value) => value.toString().replace(/\r?\n$/, '');
|
|
10
|
+
const GIT_PATH_LIST_MAX_BUFFER = 64 * 1024 * 1024;
|
|
9
11
|
/**
|
|
10
12
|
* True when the working tree has uncommitted changes that analyze would
|
|
11
|
-
* re-index, even at a matching HEAD. Excludes
|
|
12
|
-
* analyze
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* "dirty"
|
|
13
|
+
* re-index, even at a matching HEAD. Excludes GITNEXUS_MANAGED_PATHS so
|
|
14
|
+
* GitNexus's own analyze output never counts as dirty (regression vs PR #1233
|
|
15
|
+
* behavior); whole directory trees are excluded, not just their root entries,
|
|
16
|
+
* because the skill mirror writes across .agents/skills/ and deeper paths.
|
|
17
|
+
* Conservative on any git failure.
|
|
18
|
+
*
|
|
19
|
+
* This drives `analyze`'s up-to-date fast path. It is deliberately coarse:
|
|
20
|
+
* a false "dirty" here costs only a hash diff that finds nothing. `status`
|
|
21
|
+
* reaches for the per-file comparison in core/index-content-drift.ts instead,
|
|
22
|
+
* because there the same false positive is a verdict the user cannot clear
|
|
23
|
+
* (#3077), and falls back to this only when that comparison cannot run.
|
|
19
24
|
*/
|
|
20
25
|
export const isWorkingTreeDirty = (repoPath) => {
|
|
21
26
|
try {
|
|
22
|
-
const out = execFileSync('git', [
|
|
23
|
-
'status',
|
|
24
|
-
'--porcelain',
|
|
25
|
-
'--',
|
|
26
|
-
'.',
|
|
27
|
-
':(exclude).gitnexus',
|
|
28
|
-
':(exclude).gitnexus/**',
|
|
29
|
-
':(exclude).claude',
|
|
30
|
-
':(exclude).claude/**',
|
|
31
|
-
':(exclude).cursor',
|
|
32
|
-
':(exclude).cursor/**',
|
|
33
|
-
':(exclude)AGENTS.md',
|
|
34
|
-
':(exclude)CLAUDE.md',
|
|
35
|
-
':(exclude).agents',
|
|
36
|
-
':(exclude).agents/**',
|
|
37
|
-
], {
|
|
27
|
+
const out = execFileSync('git', ['status', '--porcelain', '--', '.', ...GITNEXUS_MANAGED_PATH_EXCLUDES], {
|
|
38
28
|
cwd: repoPath,
|
|
39
29
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
40
30
|
windowsHide: true,
|
|
@@ -46,6 +36,75 @@ export const isWorkingTreeDirty = (repoPath) => {
|
|
|
46
36
|
return true; // conservative on git failure
|
|
47
37
|
}
|
|
48
38
|
};
|
|
39
|
+
const parsePorcelainPaths = (porcelain) => {
|
|
40
|
+
const paths = new Set();
|
|
41
|
+
const records = porcelain.split('\0');
|
|
42
|
+
for (let i = 0; i < records.length; i++) {
|
|
43
|
+
const record = records[i];
|
|
44
|
+
if (record.length < 4)
|
|
45
|
+
continue;
|
|
46
|
+
const status = record.slice(0, 2);
|
|
47
|
+
paths.add(record.slice(3));
|
|
48
|
+
// In porcelain v1 `-z` mode, rename/copy source and destination paths are
|
|
49
|
+
// separate NUL records (with no human-facing ` -> ` delimiter). Keep both:
|
|
50
|
+
// either side may be present in the previous coverage set.
|
|
51
|
+
if (status.includes('R') || status.includes('C')) {
|
|
52
|
+
const pairedPath = records[++i];
|
|
53
|
+
if (pairedPath)
|
|
54
|
+
paths.add(pairedPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...paths];
|
|
58
|
+
};
|
|
59
|
+
const gitPathListExec = {
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
maxBuffer: GIT_PATH_LIST_MAX_BUFFER,
|
|
63
|
+
};
|
|
64
|
+
const listHiddenIndexPaths = (repoPath) => {
|
|
65
|
+
const out = execFileSync('git', ['ls-files', '-v', '-z', '--'], {
|
|
66
|
+
cwd: repoPath,
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
...gitPathListExec,
|
|
69
|
+
});
|
|
70
|
+
const paths = [];
|
|
71
|
+
for (const record of out.split('\0')) {
|
|
72
|
+
if (record.length < 3 || record[1] !== ' ')
|
|
73
|
+
continue;
|
|
74
|
+
const tag = record[0];
|
|
75
|
+
// `S` marks skip-worktree. With `-v`, an assume-unchanged entry's
|
|
76
|
+
// ordinary tag is lower-cased (`H` -> `h`, `S` -> `s`, etc.).
|
|
77
|
+
if (tag === 'S' || (tag >= 'a' && tag <= 'z'))
|
|
78
|
+
paths.push(record.slice(2));
|
|
79
|
+
}
|
|
80
|
+
return paths;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Repo-relative paths `git status` reports as dirty or untracked, using the
|
|
84
|
+
* same managed-path excludes as {@link isWorkingTreeDirty}, plus tracked paths
|
|
85
|
+
* whose assume-unchanged or skip-worktree bits can hide content changes from
|
|
86
|
+
* porcelain. `null` means either query failed — callers must not treat that as
|
|
87
|
+
* a clean tree.
|
|
88
|
+
*/
|
|
89
|
+
export const listWorkingTreeDirtyPaths = (repoPath) => {
|
|
90
|
+
try {
|
|
91
|
+
const out = execFileSync('git', [
|
|
92
|
+
'status',
|
|
93
|
+
'--porcelain=v1',
|
|
94
|
+
'-z',
|
|
95
|
+
'--untracked-files=all',
|
|
96
|
+
'--',
|
|
97
|
+
'.',
|
|
98
|
+
...GITNEXUS_MANAGED_PATH_EXCLUDES,
|
|
99
|
+
], { cwd: repoPath, windowsHide: true, ...gitPathListExec });
|
|
100
|
+
return [
|
|
101
|
+
...new Set([...parsePorcelainPaths(out), ...listHiddenIndexPaths(repoPath)].filter((rel) => !isGitNexusManagedPath(rel))),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
49
108
|
/**
|
|
50
109
|
* Snapshot, per candidate file, whether it is safe for `selfCommitContextFiles`
|
|
51
110
|
* to auto-commit — call this BEFORE `analyze` writes AGENTS.md/CLAUDE.md.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths GitNexus itself writes during `analyze`.
|
|
3
|
+
*
|
|
4
|
+
* `analyze` rewrites the stats blocks in AGENTS.md/CLAUDE.md and refreshes the
|
|
5
|
+
* agent skill mirrors as its final step — after it has recorded the per-file
|
|
6
|
+
* hashes for the run. Counting its own output as a repository change makes
|
|
7
|
+
* every completed run look immediately out of date, which is the regression
|
|
8
|
+
* PR #1233 introduced and #1233's fix excluded these paths to prevent.
|
|
9
|
+
*
|
|
10
|
+
* Two freshness checks depend on this list agreeing: `isWorkingTreeDirty`
|
|
11
|
+
* (analyze's up-to-date fast-path gate) and the per-file comparison behind
|
|
12
|
+
* `status`. They used to hold separate copies of it, so a path added to one
|
|
13
|
+
* silently became a permanent "stale" verdict in the other. One list, imported
|
|
14
|
+
* by both.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Repository-root-relative. A directory entry covers everything beneath it;
|
|
18
|
+
* a file entry matches only itself. Prefix collisions are NOT matches —
|
|
19
|
+
* `.agentsrc` is an ordinary file, not part of the `.agents` tree.
|
|
20
|
+
*/
|
|
21
|
+
export declare const GITNEXUS_MANAGED_PATHS: readonly [".gitnexus", ".claude", ".cursor", ".agents", "AGENTS.md", "CLAUDE.md"];
|
|
22
|
+
/**
|
|
23
|
+
* Git pathspecs excluding {@link GITNEXUS_MANAGED_PATHS} from a `git status`
|
|
24
|
+
* run rooted at the repository. Patterns include `./` so they match only at
|
|
25
|
+
* the repo root: a slash-free `:(exclude)AGENTS.md` would also drop
|
|
26
|
+
* `docs/AGENTS.md`, which {@link isGitNexusManagedPath} does not treat as
|
|
27
|
+
* managed. Both forms are emitted per entry: the root path itself, and `/**`
|
|
28
|
+
* for directory contents.
|
|
29
|
+
*/
|
|
30
|
+
export declare const GITNEXUS_MANAGED_PATH_EXCLUDES: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* True when a repository-relative path is GitNexus's own output. Mirrors the
|
|
33
|
+
* pathspec semantics above: root-relative, whole path segments only, so
|
|
34
|
+
* neither `.agentsrc` nor a nested `subdir/.agents/` is treated as managed.
|
|
35
|
+
*/
|
|
36
|
+
export declare const isGitNexusManagedPath: (relPath: string) => boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths GitNexus itself writes during `analyze`.
|
|
3
|
+
*
|
|
4
|
+
* `analyze` rewrites the stats blocks in AGENTS.md/CLAUDE.md and refreshes the
|
|
5
|
+
* agent skill mirrors as its final step — after it has recorded the per-file
|
|
6
|
+
* hashes for the run. Counting its own output as a repository change makes
|
|
7
|
+
* every completed run look immediately out of date, which is the regression
|
|
8
|
+
* PR #1233 introduced and #1233's fix excluded these paths to prevent.
|
|
9
|
+
*
|
|
10
|
+
* Two freshness checks depend on this list agreeing: `isWorkingTreeDirty`
|
|
11
|
+
* (analyze's up-to-date fast-path gate) and the per-file comparison behind
|
|
12
|
+
* `status`. They used to hold separate copies of it, so a path added to one
|
|
13
|
+
* silently became a permanent "stale" verdict in the other. One list, imported
|
|
14
|
+
* by both.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Repository-root-relative. A directory entry covers everything beneath it;
|
|
18
|
+
* a file entry matches only itself. Prefix collisions are NOT matches —
|
|
19
|
+
* `.agentsrc` is an ordinary file, not part of the `.agents` tree.
|
|
20
|
+
*/
|
|
21
|
+
export const GITNEXUS_MANAGED_PATHS = [
|
|
22
|
+
'.gitnexus',
|
|
23
|
+
'.claude',
|
|
24
|
+
'.cursor',
|
|
25
|
+
'.agents',
|
|
26
|
+
'AGENTS.md',
|
|
27
|
+
'CLAUDE.md',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Git pathspecs excluding {@link GITNEXUS_MANAGED_PATHS} from a `git status`
|
|
31
|
+
* run rooted at the repository. Patterns include `./` so they match only at
|
|
32
|
+
* the repo root: a slash-free `:(exclude)AGENTS.md` would also drop
|
|
33
|
+
* `docs/AGENTS.md`, which {@link isGitNexusManagedPath} does not treat as
|
|
34
|
+
* managed. Both forms are emitted per entry: the root path itself, and `/**`
|
|
35
|
+
* for directory contents.
|
|
36
|
+
*/
|
|
37
|
+
export const GITNEXUS_MANAGED_PATH_EXCLUDES = GITNEXUS_MANAGED_PATHS.flatMap((managed) => [`:(exclude,glob)./${managed}`, `:(exclude,glob)./${managed}/**`]);
|
|
38
|
+
/**
|
|
39
|
+
* True when a repository-relative path is GitNexus's own output. Mirrors the
|
|
40
|
+
* pathspec semantics above: root-relative, whole path segments only, so
|
|
41
|
+
* neither `.agentsrc` nor a nested `subdir/.agents/` is treated as managed.
|
|
42
|
+
*/
|
|
43
|
+
export const isGitNexusManagedPath = (relPath) => {
|
|
44
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
45
|
+
return GITNEXUS_MANAGED_PATHS.some((managed) => normalized === managed || normalized.startsWith(`${managed}/`));
|
|
46
|
+
};
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* does is skip the tree-sitter worker dispatch when a chunk's contents
|
|
7
7
|
* haven't changed since the last run.
|
|
8
8
|
*
|
|
9
|
-
* Granularity: chunk-level.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Granularity: chunk-level. Files are assigned to a stable
|
|
10
|
+
* `(language, hash(path) mod 128)` bucket, then packed to a 2 MiB (or
|
|
11
|
+
* operator) byte budget *inside* that bucket. Membership does not depend
|
|
12
|
+
* on worker count. The cache key is `sha256(joined(filePath:contentHash
|
|
13
|
+
* for each file in the chunk, sorted))`. A content edit invalidates only
|
|
14
|
+
* that file's pack; add/delete/rename only the affected bucket.
|
|
13
15
|
*
|
|
14
16
|
* Why not per-file:
|
|
15
17
|
* - Workers process sub-batches and emit aggregated `ParseWorkerResult`s.
|
|
@@ -23,6 +25,22 @@
|
|
|
23
25
|
*/
|
|
24
26
|
import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.js';
|
|
25
27
|
export declare const PARSE_CACHE_VERSION: string;
|
|
28
|
+
/** Stable parse-cache bucket count (#3088). Changing this requires SCHEMA_BUMP. */
|
|
29
|
+
export declare const PARSE_CACHE_BUCKET_COUNT = 128;
|
|
30
|
+
/** Bucket id for cache membership: `sha256(path) mod N` without IEEE-754 truncation. */
|
|
31
|
+
export declare const parseCacheBucketId: (filePath: string) => number;
|
|
32
|
+
export type ParseCachePackFile = {
|
|
33
|
+
path: string;
|
|
34
|
+
size: number;
|
|
35
|
+
language: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Pack files into parse-cache chunks: group by (language, bucket id), sort
|
|
39
|
+
* paths inside the group, then cut at `byteBudget`. Bucket visit order is
|
|
40
|
+
* the lexicographic order of `${language}\\0${bucketId}` keys (deterministic,
|
|
41
|
+
* independent of scan order and worker count).
|
|
42
|
+
*/
|
|
43
|
+
export declare const packParseCacheChunks: (files: readonly ParseCachePackFile[], byteBudget: number) => string[][];
|
|
26
44
|
/** Runtime view: keyed Map for fast lookup; mutated in place during a run. */
|
|
27
45
|
export interface ParseCache {
|
|
28
46
|
version: string;
|