gitnexus 1.6.12-rc.17 → 1.6.12-rc.19
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/dist/cli/doctor.js +6 -4
- package/dist/cli/i18n/en.d.ts +4 -2
- package/dist/cli/i18n/en.js +4 -2
- package/dist/cli/i18n/resources.d.ts +6 -2
- package/dist/cli/i18n/zh-CN.d.ts +2 -0
- package/dist/cli/i18n/zh-CN.js +4 -2
- package/dist/core/git-staleness.js +14 -0
- package/dist/server/api.js +15 -15
- package/dist/server/repo-projection.d.ts +88 -0
- package/dist/server/repo-projection.js +53 -0
- package/package.json +1 -1
- package/web/assets/{index-BtIqUO-E.js → index-CHJoW_P1.js} +1 -1
- package/web/index.html +1 -1
package/dist/cli/doctor.js
CHANGED
|
@@ -237,10 +237,12 @@ export const doctorCommand = async () => {
|
|
|
237
237
|
console.log(` ${padDisplayEnd('', 18)}${remedy}`);
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
console.log(` ${label('doctor.labels.semanticMode', 18)}${vectorProbe.loaded
|
|
240
|
+
// Doctor has no repository target, so this probe can report only whether the
|
|
241
|
+
// runtime can load VECTOR. It cannot claim that a repository has built the
|
|
242
|
+
// named HNSW index. Keep the capability and repository state distinct.
|
|
243
|
+
console.log(` ${label('doctor.labels.semanticMode', 18)}${t(vectorProbe.loaded
|
|
244
|
+
? 'doctor.vectorCapability.indexUnverified'
|
|
245
|
+
: 'doctor.vectorCapability.exactScanOnly')}`);
|
|
244
246
|
// Surface the optional-extension install policy so offline users can see
|
|
245
247
|
// whether analyze/query will reach the network (extension.ladybugdb.com).
|
|
246
248
|
// Literal label (like the 'native' line) to avoid adding i18n keys.
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -102,8 +102,10 @@ export declare const en: {
|
|
|
102
102
|
readonly 'doctor.labels.onnx': "ONNX:";
|
|
103
103
|
readonly 'doctor.labels.graphStore': "Graph store:";
|
|
104
104
|
readonly 'doctor.labels.fullTextSearch': "Full-text search:";
|
|
105
|
-
readonly 'doctor.labels.vectorIndex': "VECTOR
|
|
106
|
-
readonly 'doctor.labels.semanticMode': "Semantic
|
|
105
|
+
readonly 'doctor.labels.vectorIndex': "VECTOR extension:";
|
|
106
|
+
readonly 'doctor.labels.semanticMode': "Semantic support:";
|
|
107
|
+
readonly 'doctor.vectorCapability.indexUnverified': "vector-index capable (repository index not checked)";
|
|
108
|
+
readonly 'doctor.vectorCapability.exactScanOnly': "exact-scan only (VECTOR extension unavailable)";
|
|
107
109
|
readonly 'doctor.labels.exactScanLimit': "Exact scan limit:";
|
|
108
110
|
readonly 'doctor.labels.note': "Note:";
|
|
109
111
|
readonly 'doctor.labels.backend': "Backend:";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -104,8 +104,10 @@ export const en = {
|
|
|
104
104
|
'doctor.labels.onnx': 'ONNX:',
|
|
105
105
|
'doctor.labels.graphStore': 'Graph store:',
|
|
106
106
|
'doctor.labels.fullTextSearch': 'Full-text search:',
|
|
107
|
-
'doctor.labels.vectorIndex': 'VECTOR
|
|
108
|
-
'doctor.labels.semanticMode': 'Semantic
|
|
107
|
+
'doctor.labels.vectorIndex': 'VECTOR extension:',
|
|
108
|
+
'doctor.labels.semanticMode': 'Semantic support:',
|
|
109
|
+
'doctor.vectorCapability.indexUnverified': 'vector-index capable (repository index not checked)',
|
|
110
|
+
'doctor.vectorCapability.exactScanOnly': 'exact-scan only (VECTOR extension unavailable)',
|
|
109
111
|
'doctor.labels.exactScanLimit': 'Exact scan limit:',
|
|
110
112
|
'doctor.labels.note': 'Note:',
|
|
111
113
|
'doctor.labels.backend': 'Backend:',
|
|
@@ -103,8 +103,10 @@ export declare const cliResources: {
|
|
|
103
103
|
readonly 'doctor.labels.onnx': "ONNX:";
|
|
104
104
|
readonly 'doctor.labels.graphStore': "Graph store:";
|
|
105
105
|
readonly 'doctor.labels.fullTextSearch': "Full-text search:";
|
|
106
|
-
readonly 'doctor.labels.vectorIndex': "VECTOR
|
|
107
|
-
readonly 'doctor.labels.semanticMode': "Semantic
|
|
106
|
+
readonly 'doctor.labels.vectorIndex': "VECTOR extension:";
|
|
107
|
+
readonly 'doctor.labels.semanticMode': "Semantic support:";
|
|
108
|
+
readonly 'doctor.vectorCapability.indexUnverified': "vector-index capable (repository index not checked)";
|
|
109
|
+
readonly 'doctor.vectorCapability.exactScanOnly': "exact-scan only (VECTOR extension unavailable)";
|
|
108
110
|
readonly 'doctor.labels.exactScanLimit': "Exact scan limit:";
|
|
109
111
|
readonly 'doctor.labels.note': "Note:";
|
|
110
112
|
readonly 'doctor.labels.backend': "Backend:";
|
|
@@ -384,6 +386,8 @@ export declare const cliResources: {
|
|
|
384
386
|
'doctor.labels.fullTextSearch': string;
|
|
385
387
|
'doctor.labels.vectorIndex': string;
|
|
386
388
|
'doctor.labels.semanticMode': string;
|
|
389
|
+
'doctor.vectorCapability.indexUnverified': string;
|
|
390
|
+
'doctor.vectorCapability.exactScanOnly': string;
|
|
387
391
|
'doctor.labels.exactScanLimit': string;
|
|
388
392
|
'doctor.labels.note': string;
|
|
389
393
|
'doctor.labels.backend': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -104,6 +104,8 @@ export declare const zhCN: {
|
|
|
104
104
|
'doctor.labels.fullTextSearch': string;
|
|
105
105
|
'doctor.labels.vectorIndex': string;
|
|
106
106
|
'doctor.labels.semanticMode': string;
|
|
107
|
+
'doctor.vectorCapability.indexUnverified': string;
|
|
108
|
+
'doctor.vectorCapability.exactScanOnly': string;
|
|
107
109
|
'doctor.labels.exactScanLimit': string;
|
|
108
110
|
'doctor.labels.note': string;
|
|
109
111
|
'doctor.labels.backend': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -102,8 +102,10 @@ export const zhCN = {
|
|
|
102
102
|
'doctor.labels.onnx': 'ONNX:',
|
|
103
103
|
'doctor.labels.graphStore': '图存储:',
|
|
104
104
|
'doctor.labels.fullTextSearch': '全文搜索:',
|
|
105
|
-
'doctor.labels.vectorIndex': '
|
|
106
|
-
'doctor.labels.semanticMode': '
|
|
105
|
+
'doctor.labels.vectorIndex': 'VECTOR 扩展:',
|
|
106
|
+
'doctor.labels.semanticMode': '语义支持:',
|
|
107
|
+
'doctor.vectorCapability.indexUnverified': '支持向量索引(未检查仓库索引)',
|
|
108
|
+
'doctor.vectorCapability.exactScanOnly': '仅精确扫描(VECTOR 扩展不可用)',
|
|
107
109
|
'doctor.labels.exactScanLimit': '精确扫描上限:',
|
|
108
110
|
'doctor.labels.note': '说明:',
|
|
109
111
|
'doctor.labels.backend': '后端:',
|
|
@@ -8,6 +8,12 @@ import path from 'path';
|
|
|
8
8
|
import { readRegistry } from '../storage/repo-manager.js';
|
|
9
9
|
import { findGitRootByDotGit, getCurrentCommit, getRemoteUrl } from '../storage/git.js';
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
+
/**
|
|
12
|
+
* Ceiling for one `git rev-list` staleness probe. Generous for the local
|
|
13
|
+
* history walk this is, and short enough that an unresponsive working tree
|
|
14
|
+
* degrades to "not stale" quickly rather than holding a request open.
|
|
15
|
+
*/
|
|
16
|
+
const STALENESS_TIMEOUT_MS = 5_000;
|
|
11
17
|
/**
|
|
12
18
|
* Check how many commits the index is behind HEAD (synchronous; uses git CLI).
|
|
13
19
|
*/
|
|
@@ -46,6 +52,14 @@ export async function checkStalenessAsync(repoPath, lastCommit) {
|
|
|
46
52
|
cwd: repoPath,
|
|
47
53
|
encoding: 'utf-8',
|
|
48
54
|
windowsHide: true,
|
|
55
|
+
// The catch below fails closed on every git ERROR, but a hang is not an
|
|
56
|
+
// error — it is silence, and without a bound this await never settles.
|
|
57
|
+
// A working tree on a disconnected network mount or behind a stuck lock
|
|
58
|
+
// does exactly that, and `/api/repos` fans this out once per registered
|
|
59
|
+
// repo, so one unreachable mount could hold the whole listing open
|
|
60
|
+
// (#3232 review). The timeout kills the child and rejects, which routes
|
|
61
|
+
// a hang into the same fail-closed "not stale" answer as a bad SHA.
|
|
62
|
+
timeout: STALENESS_TIMEOUT_MS,
|
|
49
63
|
});
|
|
50
64
|
const commitsBehind = parseInt(stdout.trim(), 10) || 0;
|
|
51
65
|
if (commitsBehind > 0) {
|
package/dist/server/api.js
CHANGED
|
@@ -31,6 +31,8 @@ import { parseGrepQuery, GREP_TIME_BUDGET_MS } from './grep-params.js';
|
|
|
31
31
|
import { runGrepScanInWorker } from './grep-scan.js';
|
|
32
32
|
import { analyzeCloneOptions, extractWebRepoName, getCloneDir, cloneOrPull, warnIfInsecureAzureConfig, GITHUB_TOKEN_HOSTS, } from './git-clone.js';
|
|
33
33
|
import { createAnalyzeUploadHandler } from './analyze-upload.js';
|
|
34
|
+
import { checkStalenessAsync } from '../core/git-staleness.js';
|
|
35
|
+
import { projectRepoDetail, projectRepoListEntry, resolveLastCommit } from './repo-projection.js';
|
|
34
36
|
// Shared with the CLI's `--branch` (via the analyze-config wrapper) so both
|
|
35
37
|
// entry points accept the same refs. Imported from core — not cli/ — so
|
|
36
38
|
// createServer does not close a cycle with cli/serve.ts.
|
|
@@ -809,17 +811,19 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
809
811
|
res.json(buildServerInfo(updateController.snapshot()));
|
|
810
812
|
});
|
|
811
813
|
// List all registered repos
|
|
812
|
-
|
|
814
|
+
// Rate-limited (CodeQL js/missing-rate-limiting) because this route now spawns
|
|
815
|
+
// one `git rev-list` per registered repo to answer freshness: an unauthenticated
|
|
816
|
+
// GET that costs N subprocesses is worth the same 60 rpm/IP ceiling `/api/repo`
|
|
817
|
+
// already carries. Web callers hit this on connect/switch, never in a loop.
|
|
818
|
+
app.get('/api/repos', createRouteLimiter(), async (_req, res) => {
|
|
813
819
|
try {
|
|
814
820
|
const repos = await listRegisteredRepos();
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
stats: r.stats,
|
|
822
|
-
})));
|
|
821
|
+
// Checked in parallel, for the reason `list_repos` already does it that
|
|
822
|
+
// way: each check spawns an async `git rev-list`, and the sequential
|
|
823
|
+
// variant took ~50s across 200 repos (#1363). Projecting inside the map
|
|
824
|
+
// keeps the entry and its own check together — an index-matched second
|
|
825
|
+
// array is the shape that silently mispairs them if either is reordered.
|
|
826
|
+
res.json(await Promise.all(repos.map(async (r) => projectRepoListEntry(r, await checkStalenessAsync(r.path, r.lastCommit)))));
|
|
823
827
|
}
|
|
824
828
|
catch (err) {
|
|
825
829
|
res.status(500).json({ error: err.message || 'Failed to list repos' });
|
|
@@ -845,12 +849,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
845
849
|
return;
|
|
846
850
|
}
|
|
847
851
|
const meta = await loadMeta(entry.storagePath);
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
repoPath: entry.path,
|
|
851
|
-
indexedAt: meta?.indexedAt ?? entry.indexedAt,
|
|
852
|
-
stats: meta?.stats ?? entry.stats ?? {},
|
|
853
|
-
});
|
|
852
|
+
const staleness = await checkStalenessAsync(entry.path, resolveLastCommit(entry, meta));
|
|
853
|
+
res.json(projectRepoDetail(entry, meta, staleness));
|
|
854
854
|
}
|
|
855
855
|
catch (err) {
|
|
856
856
|
res.status(500).json({ error: err.message || 'Failed to get repo info' });
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response projections for the `serve` repo routes.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the route bodies so the field list is assertable. Inline, the
|
|
5
|
+
* projections were only reachable by booting a server and indexing a real
|
|
6
|
+
* repository, which is how `branch` came to sit on `RegistryEntry` unexposed
|
|
7
|
+
* over HTTP while `gitnexus list` printed it, with no test to notice (#3226).
|
|
8
|
+
*
|
|
9
|
+
* These are pure: the caller resolves the registry entry, the on-disk metadata
|
|
10
|
+
* and the staleness check, and passes the results in.
|
|
11
|
+
*/
|
|
12
|
+
import type { StalenessInfo } from '../core/git-staleness.js';
|
|
13
|
+
import type { RegistryEntry } from '../storage/repo-manager.js';
|
|
14
|
+
import type { RepoMeta } from '../storage/repo-meta.js';
|
|
15
|
+
/**
|
|
16
|
+
* Staleness in the shape MCP `list_repos` already returns
|
|
17
|
+
* (`mcp/local/local-backend.ts`): the key is present only when the index is
|
|
18
|
+
* actually behind, so "fresh" stays the absence of a field rather than a second
|
|
19
|
+
* thing for a client to interpret. Deliberately identical across the two
|
|
20
|
+
* surfaces — the same fact should not have two shapes.
|
|
21
|
+
*
|
|
22
|
+
* `checkStalenessAsync` self-catches and reports 0 commits behind when the
|
|
23
|
+
* commit cannot be resolved, so an unanswerable check degrades to "not stale"
|
|
24
|
+
* rather than failing the request that carries it.
|
|
25
|
+
*
|
|
26
|
+
* That makes the field meaningful mainly for `path`-registered repositories,
|
|
27
|
+
* where an operator commits into the working tree the index was built from.
|
|
28
|
+
* A `url` repository is cloned `--depth 1` and is re-analyzed by the same run
|
|
29
|
+
* that pulls it, so its recorded commit is HEAD; and were the two ever to
|
|
30
|
+
* diverge, `git rev-list <old>..HEAD` cannot walk a shallow history and fails
|
|
31
|
+
* closed to "not stale". Reporting fresh there is not a claim that the remote
|
|
32
|
+
* has not moved — this measures the index against the local working tree, the
|
|
33
|
+
* same thing `gitnexus status` and MCP `list_repos` measure.
|
|
34
|
+
*/
|
|
35
|
+
export declare const stalenessField: (info: StalenessInfo) => {
|
|
36
|
+
staleness?: {
|
|
37
|
+
commitsBehind: number;
|
|
38
|
+
hint?: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/** One entry of `GET /api/repos`. */
|
|
42
|
+
export declare const projectRepoListEntry: (entry: RegistryEntry, staleness: StalenessInfo) => {
|
|
43
|
+
staleness?: {
|
|
44
|
+
commitsBehind: number;
|
|
45
|
+
hint?: string;
|
|
46
|
+
};
|
|
47
|
+
name: string;
|
|
48
|
+
path: string;
|
|
49
|
+
repoPath: string;
|
|
50
|
+
indexedAt: string;
|
|
51
|
+
lastCommit: string;
|
|
52
|
+
stats: {
|
|
53
|
+
files?: number;
|
|
54
|
+
nodes?: number;
|
|
55
|
+
edges?: number;
|
|
56
|
+
communities?: number;
|
|
57
|
+
processes?: number;
|
|
58
|
+
embeddings?: number;
|
|
59
|
+
};
|
|
60
|
+
branch: string;
|
|
61
|
+
branches: import("../storage/branch-index.js").BranchSummary[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* `GET /api/repo`. `meta ?? entry` throughout, for the reason `indexedAt`
|
|
65
|
+
* already did it: the on-disk metadata is the fresher record when the two
|
|
66
|
+
* disagree, and the entry is the fallback for a repo whose meta cannot be read.
|
|
67
|
+
*/
|
|
68
|
+
export declare const projectRepoDetail: (entry: RegistryEntry, meta: RepoMeta | null | undefined, staleness: StalenessInfo) => {
|
|
69
|
+
staleness?: {
|
|
70
|
+
commitsBehind: number;
|
|
71
|
+
hint?: string;
|
|
72
|
+
};
|
|
73
|
+
name: string;
|
|
74
|
+
repoPath: string;
|
|
75
|
+
indexedAt: string;
|
|
76
|
+
stats: {
|
|
77
|
+
files?: number;
|
|
78
|
+
nodes?: number;
|
|
79
|
+
edges?: number;
|
|
80
|
+
communities?: number;
|
|
81
|
+
processes?: number;
|
|
82
|
+
embeddings?: number;
|
|
83
|
+
};
|
|
84
|
+
lastCommit: string;
|
|
85
|
+
branch: string;
|
|
86
|
+
};
|
|
87
|
+
/** The commit a `/api/repo` staleness check should be measured against. */
|
|
88
|
+
export declare const resolveLastCommit: (entry: RegistryEntry, meta: RepoMeta | null | undefined) => string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staleness in the shape MCP `list_repos` already returns
|
|
3
|
+
* (`mcp/local/local-backend.ts`): the key is present only when the index is
|
|
4
|
+
* actually behind, so "fresh" stays the absence of a field rather than a second
|
|
5
|
+
* thing for a client to interpret. Deliberately identical across the two
|
|
6
|
+
* surfaces — the same fact should not have two shapes.
|
|
7
|
+
*
|
|
8
|
+
* `checkStalenessAsync` self-catches and reports 0 commits behind when the
|
|
9
|
+
* commit cannot be resolved, so an unanswerable check degrades to "not stale"
|
|
10
|
+
* rather than failing the request that carries it.
|
|
11
|
+
*
|
|
12
|
+
* That makes the field meaningful mainly for `path`-registered repositories,
|
|
13
|
+
* where an operator commits into the working tree the index was built from.
|
|
14
|
+
* A `url` repository is cloned `--depth 1` and is re-analyzed by the same run
|
|
15
|
+
* that pulls it, so its recorded commit is HEAD; and were the two ever to
|
|
16
|
+
* diverge, `git rev-list <old>..HEAD` cannot walk a shallow history and fails
|
|
17
|
+
* closed to "not stale". Reporting fresh there is not a claim that the remote
|
|
18
|
+
* has not moved — this measures the index against the local working tree, the
|
|
19
|
+
* same thing `gitnexus status` and MCP `list_repos` measure.
|
|
20
|
+
*/
|
|
21
|
+
export const stalenessField = (info) => info.isStale ? { staleness: { commitsBehind: info.commitsBehind, hint: info.hint } } : {};
|
|
22
|
+
/** One entry of `GET /api/repos`. */
|
|
23
|
+
export const projectRepoListEntry = (entry, staleness) => ({
|
|
24
|
+
name: entry.name,
|
|
25
|
+
path: entry.path,
|
|
26
|
+
repoPath: entry.path,
|
|
27
|
+
indexedAt: entry.indexedAt,
|
|
28
|
+
lastCommit: entry.lastCommit,
|
|
29
|
+
stats: entry.stats,
|
|
30
|
+
// The registry has carried these since #2106; #3199 made them load-bearing
|
|
31
|
+
// over HTTP, because a branch-pinned analyze now gets its own entry and the
|
|
32
|
+
// only other way to tell two entries apart is to parse the clone-directory
|
|
33
|
+
// slug — a layout detail, not an API contract.
|
|
34
|
+
branch: entry.branch,
|
|
35
|
+
branches: entry.branches,
|
|
36
|
+
...stalenessField(staleness),
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* `GET /api/repo`. `meta ?? entry` throughout, for the reason `indexedAt`
|
|
40
|
+
* already did it: the on-disk metadata is the fresher record when the two
|
|
41
|
+
* disagree, and the entry is the fallback for a repo whose meta cannot be read.
|
|
42
|
+
*/
|
|
43
|
+
export const projectRepoDetail = (entry, meta, staleness) => ({
|
|
44
|
+
name: entry.name,
|
|
45
|
+
repoPath: entry.path,
|
|
46
|
+
indexedAt: meta?.indexedAt ?? entry.indexedAt,
|
|
47
|
+
stats: meta?.stats ?? entry.stats ?? {},
|
|
48
|
+
lastCommit: meta?.lastCommit ?? entry.lastCommit,
|
|
49
|
+
branch: meta?.branch ?? entry.branch,
|
|
50
|
+
...stalenessField(staleness),
|
|
51
|
+
});
|
|
52
|
+
/** The commit a `/api/repo` staleness check should be measured against. */
|
|
53
|
+
export const resolveLastCommit = (entry, meta) => meta?.lastCommit ?? entry.lastCommit;
|
package/package.json
CHANGED