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.
@@ -237,10 +237,12 @@ export const doctorCommand = async () => {
237
237
  console.log(` ${padDisplayEnd('', 18)}${remedy}`);
238
238
  }
239
239
  }
240
- // Semantic mode follows the probe, not the platform: without a loadable
241
- // VECTOR extension the index can be neither built nor queried, so search is
242
- // really on exact scan no matter what the platform would allow.
243
- console.log(` ${label('doctor.labels.semanticMode', 18)}${vectorProbe.loaded ? capabilities.semanticMode : 'exact-scan'}`);
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.
@@ -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 index:";
106
- readonly 'doctor.labels.semanticMode': "Semantic mode:";
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:";
@@ -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 index:',
108
- 'doctor.labels.semanticMode': 'Semantic mode:',
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 index:";
107
- readonly 'doctor.labels.semanticMode': "Semantic mode:";
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;
@@ -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;
@@ -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) {
@@ -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
- app.get('/api/repos', async (_req, res) => {
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
- res.json(repos.map((r) => ({
816
- name: r.name,
817
- path: r.path,
818
- repoPath: r.path,
819
- indexedAt: r.indexedAt,
820
- lastCommit: r.lastCommit,
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
- res.json({
849
- name: entry.name,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.12-rc.17",
3
+ "version": "1.6.12-rc.19",
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",