gitnexus 1.6.9-rc.26 → 1.6.9-rc.28
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 +4 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +1 -0
- package/dist/core/group/extractors/http-route-extractor.js +9 -4
- package/dist/core/lbug/lbug-adapter.d.ts +6 -0
- package/dist/core/lbug/lbug-adapter.js +8 -2
- package/dist/core/run-analyze.js +5 -1
- package/dist/core/search/fts-indexes.d.ts +14 -0
- package/dist/core/search/fts-indexes.js +67 -2
- package/dist/storage/git.js +6 -0
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +1 -0
package/README.md
CHANGED
|
@@ -363,6 +363,7 @@ Configure the behavior with two environment variables:
|
|
|
363
363
|
| -------------------------------------------- | ---------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
364
364
|
| `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded INSTALL if LOAD fails. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. |
|
|
365
365
|
| `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process `INSTALL` child before it is killed. |
|
|
366
|
+
| `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. |
|
|
366
367
|
| `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
|
|
367
368
|
|
|
368
369
|
```bash
|
|
@@ -371,6 +372,9 @@ GITNEXUS_LBUG_EXTENSION_INSTALL=load-only npx gitnexus analyze
|
|
|
371
372
|
|
|
372
373
|
# Slow network: give extension downloads more time
|
|
373
374
|
GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS=30000 npx gitnexus analyze
|
|
375
|
+
|
|
376
|
+
# CJK-heavy codebase: rebuild keyword indexes without English stemming
|
|
377
|
+
GITNEXUS_FTS_STEMMER=none npx gitnexus analyze --repair-fts
|
|
374
378
|
```
|
|
375
379
|
|
|
376
380
|
### Analysis runs out of memory
|
|
@@ -32,6 +32,7 @@ export declare const HANDLES_ROUTE_QUERY = "\nMATCH (handlerFile:File)-[r:CodeRe
|
|
|
32
32
|
* - collapse `:id`, `{id}`, `[id]` path params into a single `{param}`
|
|
33
33
|
*/
|
|
34
34
|
export declare function normalizeHttpPath(p: string): string;
|
|
35
|
+
export declare function normalizeRepoRelPath(filePath: string): string;
|
|
35
36
|
export declare class HttpRouteExtractor implements ContractExtractor {
|
|
36
37
|
type: "http";
|
|
37
38
|
canExtract(_repo: RepoHandle): Promise<boolean>;
|
|
@@ -230,6 +230,9 @@ function normalizeConsumerPath(url) {
|
|
|
230
230
|
function contractIdFor(method, pathNorm) {
|
|
231
231
|
return `http::${method.toUpperCase()}::${pathNorm}`;
|
|
232
232
|
}
|
|
233
|
+
export function normalizeRepoRelPath(filePath) {
|
|
234
|
+
return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
235
|
+
}
|
|
233
236
|
// ─── Graph row helpers ───────────────────────────────────────────────
|
|
234
237
|
function methodFromRouteReason(reason) {
|
|
235
238
|
const r = reason || '';
|
|
@@ -696,6 +699,7 @@ export class HttpRouteExtractor {
|
|
|
696
699
|
const out = [];
|
|
697
700
|
for (const rel of files) {
|
|
698
701
|
const detections = await getDetections(rel);
|
|
702
|
+
const filePath = normalizeRepoRelPath(rel);
|
|
699
703
|
for (const d of detections) {
|
|
700
704
|
if (d.role !== 'provider')
|
|
701
705
|
continue;
|
|
@@ -703,14 +707,14 @@ export class HttpRouteExtractor {
|
|
|
703
707
|
// Resolve the handler to a real symbol (named handler, or the inline
|
|
704
708
|
// arrow that encloses the registration line) so the contract carries a
|
|
705
709
|
// real symbolUid; fall back to the file + detection name otherwise.
|
|
706
|
-
const resolved = await resolveSymbol(
|
|
710
|
+
const resolved = await resolveSymbol(filePath, d);
|
|
707
711
|
out.push({
|
|
708
712
|
contractId: contractIdFor(d.method, pathNorm),
|
|
709
713
|
type: 'http',
|
|
710
714
|
role: 'provider',
|
|
711
715
|
symbolUid: resolved?.uid ?? '',
|
|
712
716
|
symbolRef: {
|
|
713
|
-
filePath: resolved?.filePath ||
|
|
717
|
+
filePath: resolved?.filePath || filePath,
|
|
714
718
|
name: resolved?.name ?? d.name ?? 'handler',
|
|
715
719
|
},
|
|
716
720
|
symbolName: resolved?.name ?? d.name ?? 'handler',
|
|
@@ -797,6 +801,7 @@ export class HttpRouteExtractor {
|
|
|
797
801
|
const out = [];
|
|
798
802
|
for (const rel of files) {
|
|
799
803
|
const detections = await getDetections(rel);
|
|
804
|
+
const filePath = normalizeRepoRelPath(rel);
|
|
800
805
|
for (const d of detections) {
|
|
801
806
|
if (d.role !== 'consumer')
|
|
802
807
|
continue;
|
|
@@ -804,13 +809,13 @@ export class HttpRouteExtractor {
|
|
|
804
809
|
// Resolve the function CONTAINING the fetch/axios call so the consumer
|
|
805
810
|
// contract carries a real symbolUid (was always '' — the gap that left
|
|
806
811
|
// cross-repo trace/impact unable to traverse HTTP links).
|
|
807
|
-
const resolved = await resolveSymbol(
|
|
812
|
+
const resolved = await resolveSymbol(filePath, d);
|
|
808
813
|
out.push({
|
|
809
814
|
contractId: contractIdFor(d.method, pathNorm),
|
|
810
815
|
type: 'http',
|
|
811
816
|
role: 'consumer',
|
|
812
817
|
symbolUid: resolved?.uid ?? '',
|
|
813
|
-
symbolRef: { filePath: resolved?.filePath ||
|
|
818
|
+
symbolRef: { filePath: resolved?.filePath || filePath, name: resolved?.name ?? 'fetch' },
|
|
814
819
|
symbolName: resolved?.name ?? 'fetch',
|
|
815
820
|
confidence: d.confidence,
|
|
816
821
|
meta: {
|
|
@@ -286,6 +286,12 @@ export declare const loadFTSExtension: (targetConn?: lbug.Connection, opts?: Ext
|
|
|
286
286
|
* unavailable so semantic search can fall back to exact scan.
|
|
287
287
|
*/
|
|
288
288
|
export declare const loadVectorExtension: (targetConn?: lbug.Connection, opts?: ExtensionEnsureOptions) => Promise<boolean>;
|
|
289
|
+
/**
|
|
290
|
+
* Default stemmer for FTS indexes. Single source so the analyze path
|
|
291
|
+
* (`getSearchFTSStemmer`) and the read-only `createFTSIndex`/`ensureFTSIndex`
|
|
292
|
+
* defaults can never silently diverge.
|
|
293
|
+
*/
|
|
294
|
+
export declare const DEFAULT_FTS_STEMMER = "porter";
|
|
289
295
|
/**
|
|
290
296
|
* Create a full-text search index on a table
|
|
291
297
|
* @param tableName - The node table name (e.g., 'File', 'CodeSymbol')
|
|
@@ -2081,6 +2081,12 @@ export const loadVectorExtension = async (targetConn, opts = {}) => {
|
|
|
2081
2081
|
vectorExtensionLoaded = true;
|
|
2082
2082
|
return loaded;
|
|
2083
2083
|
};
|
|
2084
|
+
/**
|
|
2085
|
+
* Default stemmer for FTS indexes. Single source so the analyze path
|
|
2086
|
+
* (`getSearchFTSStemmer`) and the read-only `createFTSIndex`/`ensureFTSIndex`
|
|
2087
|
+
* defaults can never silently diverge.
|
|
2088
|
+
*/
|
|
2089
|
+
export const DEFAULT_FTS_STEMMER = 'porter';
|
|
2084
2090
|
/**
|
|
2085
2091
|
* Create a full-text search index on a table
|
|
2086
2092
|
* @param tableName - The node table name (e.g., 'File', 'CodeSymbol')
|
|
@@ -2088,7 +2094,7 @@ export const loadVectorExtension = async (targetConn, opts = {}) => {
|
|
|
2088
2094
|
* @param properties - List of properties to index (e.g., ['name', 'code'])
|
|
2089
2095
|
* @param stemmer - Stemming algorithm (default: 'porter')
|
|
2090
2096
|
*/
|
|
2091
|
-
export const createFTSIndex = async (tableName, indexName, properties, stemmer =
|
|
2097
|
+
export const createFTSIndex = async (tableName, indexName, properties, stemmer = DEFAULT_FTS_STEMMER) => {
|
|
2092
2098
|
if (!conn) {
|
|
2093
2099
|
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2094
2100
|
}
|
|
@@ -2176,7 +2182,7 @@ export const createVectorIndex = async () => {
|
|
|
2176
2182
|
* the index is owned by `gitnexus analyze` (writable) and either already
|
|
2177
2183
|
* exists or will be created on the next analyze.
|
|
2178
2184
|
*/
|
|
2179
|
-
export const ensureFTSIndex = async (tableName, indexName, properties, stemmer =
|
|
2185
|
+
export const ensureFTSIndex = async (tableName, indexName, properties, stemmer = DEFAULT_FTS_STEMMER) => {
|
|
2180
2186
|
const key = ftsIndexKey(tableName, indexName);
|
|
2181
2187
|
if (ensuredFTSIndexes.has(key))
|
|
2182
2188
|
return;
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -14,7 +14,7 @@ import { execFileSync } from 'child_process';
|
|
|
14
14
|
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
15
15
|
import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
16
16
|
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFile, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, queryImporters, loadFTSExtension, } from './lbug/lbug-adapter.js';
|
|
17
|
-
import { createSearchFTSIndexes, verifySearchFTSIndexes } from './search/fts-indexes.js';
|
|
17
|
+
import { createSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
18
18
|
import { resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
19
19
|
import { startWalCheckpointDriver, } from './lbug/wal-checkpoint-driver.js';
|
|
20
20
|
import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, isRepoRegistered, cleanupOldKuzuFiles, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
|
|
@@ -255,6 +255,10 @@ export const pdgModeMismatch = (recorded, options) => {
|
|
|
255
255
|
export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
256
256
|
const log = (msg) => callbacks.onLog?.(msg);
|
|
257
257
|
const progress = (phase, percent, message) => callbacks.onProgress(phase, percent, message);
|
|
258
|
+
// Resolve + validate operator-provided FTS config once, before the expensive
|
|
259
|
+
// parse/load phases. A typo fails here in ms; createSearchFTSIndexes reuses
|
|
260
|
+
// the cached value via getSearchFTSStemmer.
|
|
261
|
+
initialiseSearchFTSStemmer();
|
|
258
262
|
// Scope the degraded-parse log throttle to this run. On a reused process
|
|
259
263
|
// (e.g. tests, or any host that calls runFullAnalysis more than once) the
|
|
260
264
|
// module-level counter would otherwise stay saturated and suppress every
|
|
@@ -2,5 +2,19 @@ export interface CreateSearchFTSIndexesOptions {
|
|
|
2
2
|
onIndexStart?: (table: string, indexName: string) => void;
|
|
3
3
|
onIndexReady?: (table: string, indexName: string) => void;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Resolve + validate `GITNEXUS_FTS_STEMMER` once, up front at analyze startup,
|
|
7
|
+
* and cache it. An invalid value throws here — in milliseconds — instead of
|
|
8
|
+
* ~85% into a run (after the expensive parse/scope-resolution work). The cached
|
|
9
|
+
* value is what {@link getSearchFTSStemmer} returns for the rest of the run, so
|
|
10
|
+
* config is read and validated in exactly one place.
|
|
11
|
+
*/
|
|
12
|
+
export declare function initialiseSearchFTSStemmer(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Return the stemmer resolved by {@link initialiseSearchFTSStemmer}. Falls back
|
|
15
|
+
* to resolving on demand when init was never called (read-only hosts, unit
|
|
16
|
+
* tests) so validation always applies.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getSearchFTSStemmer(): string;
|
|
5
19
|
export declare function createSearchFTSIndexes(options?: CreateSearchFTSIndexesOptions): Promise<void>;
|
|
6
20
|
export declare function verifySearchFTSIndexes(executeQuery: (cypher: string) => Promise<unknown[]>): Promise<string[]>;
|
|
@@ -1,6 +1,71 @@
|
|
|
1
|
-
import { createFTSIndex, dropFTSIndex } from '../lbug/lbug-adapter.js';
|
|
1
|
+
import { createFTSIndex, dropFTSIndex, DEFAULT_FTS_STEMMER } from '../lbug/lbug-adapter.js';
|
|
2
2
|
import { FTS_INDEXES } from './fts-schema.js';
|
|
3
|
+
// Stemmers shipped by the LadybugDB FTS extension. Mirrors the lowercase token
|
|
4
|
+
// set in the extension bundled with @ladybugdb/core 0.17.x (see package.json).
|
|
5
|
+
// Keep in sync on a LadybugDB minor bump — a value here that the installed
|
|
6
|
+
// extension rejects would pass validation but fail at CREATE_FTS_INDEX.
|
|
7
|
+
const SUPPORTED_FTS_STEMMERS = new Set([
|
|
8
|
+
'arabic',
|
|
9
|
+
'basque',
|
|
10
|
+
'catalan',
|
|
11
|
+
'danish',
|
|
12
|
+
'dutch',
|
|
13
|
+
'english',
|
|
14
|
+
'finnish',
|
|
15
|
+
'french',
|
|
16
|
+
'german',
|
|
17
|
+
'greek',
|
|
18
|
+
'hindi',
|
|
19
|
+
'hungarian',
|
|
20
|
+
'indonesian',
|
|
21
|
+
'irish',
|
|
22
|
+
'italian',
|
|
23
|
+
'lithuanian',
|
|
24
|
+
'nepali',
|
|
25
|
+
'norwegian',
|
|
26
|
+
'none',
|
|
27
|
+
'porter',
|
|
28
|
+
'portuguese',
|
|
29
|
+
'romanian',
|
|
30
|
+
'russian',
|
|
31
|
+
'serbian',
|
|
32
|
+
'spanish',
|
|
33
|
+
'swedish',
|
|
34
|
+
'tamil',
|
|
35
|
+
'turkish',
|
|
36
|
+
]);
|
|
37
|
+
let resolvedStemmer;
|
|
38
|
+
/** Read + validate `GITNEXUS_FTS_STEMMER`. Throws on an unsupported value. */
|
|
39
|
+
function resolveFTSStemmer() {
|
|
40
|
+
const raw = process.env.GITNEXUS_FTS_STEMMER?.trim().toLowerCase();
|
|
41
|
+
if (!raw)
|
|
42
|
+
return DEFAULT_FTS_STEMMER;
|
|
43
|
+
if (SUPPORTED_FTS_STEMMERS.has(raw))
|
|
44
|
+
return raw;
|
|
45
|
+
throw new Error(`Invalid GITNEXUS_FTS_STEMMER "${process.env.GITNEXUS_FTS_STEMMER}". ` +
|
|
46
|
+
`Expected one of: ${[...SUPPORTED_FTS_STEMMERS].sort().join(', ')}.`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve + validate `GITNEXUS_FTS_STEMMER` once, up front at analyze startup,
|
|
50
|
+
* and cache it. An invalid value throws here — in milliseconds — instead of
|
|
51
|
+
* ~85% into a run (after the expensive parse/scope-resolution work). The cached
|
|
52
|
+
* value is what {@link getSearchFTSStemmer} returns for the rest of the run, so
|
|
53
|
+
* config is read and validated in exactly one place.
|
|
54
|
+
*/
|
|
55
|
+
export function initialiseSearchFTSStemmer() {
|
|
56
|
+
resolvedStemmer = resolveFTSStemmer();
|
|
57
|
+
return resolvedStemmer;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Return the stemmer resolved by {@link initialiseSearchFTSStemmer}. Falls back
|
|
61
|
+
* to resolving on demand when init was never called (read-only hosts, unit
|
|
62
|
+
* tests) so validation always applies.
|
|
63
|
+
*/
|
|
64
|
+
export function getSearchFTSStemmer() {
|
|
65
|
+
return resolvedStemmer ?? resolveFTSStemmer();
|
|
66
|
+
}
|
|
3
67
|
export async function createSearchFTSIndexes(options) {
|
|
68
|
+
const stemmer = getSearchFTSStemmer();
|
|
4
69
|
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
5
70
|
options?.onIndexStart?.(table, indexName);
|
|
6
71
|
// Drop first so the live `properties` always win. `createFTSIndex` is
|
|
@@ -15,7 +80,7 @@ export async function createSearchFTSIndexes(options) {
|
|
|
15
80
|
// runs inside the existing FTS phase. Gate on a stored schema fingerprint if
|
|
16
81
|
// this rebuild cost ever shows up in analyze profiles.
|
|
17
82
|
await dropFTSIndex(table, indexName);
|
|
18
|
-
await createFTSIndex(table, indexName, [...properties]);
|
|
83
|
+
await createFTSIndex(table, indexName, [...properties], stemmer);
|
|
19
84
|
options?.onIndexReady?.(table, indexName);
|
|
20
85
|
}
|
|
21
86
|
}
|
package/dist/storage/git.js
CHANGED
|
@@ -99,6 +99,12 @@ export const getRemoteUrl = (repoPath) => {
|
|
|
99
99
|
* Find the git repository root from any path inside the repo
|
|
100
100
|
*/
|
|
101
101
|
export const getGitRoot = (fromPath) => {
|
|
102
|
+
const resolved = path.resolve(fromPath);
|
|
103
|
+
// Avoid git rev-parse --show-toplevel trimming trailing spaces from the
|
|
104
|
+
// repository root on Windows; callers that need identity keys canonicalize
|
|
105
|
+
// this value with realpath before comparing it.
|
|
106
|
+
if (hasGitDir(resolved))
|
|
107
|
+
return resolved;
|
|
102
108
|
try {
|
|
103
109
|
const raw = chompGitOutput(execSync('git rev-parse --show-toplevel', {
|
|
104
110
|
cwd: fromPath,
|
package/package.json
CHANGED
|
@@ -36,6 +36,7 @@ const PLATFORM_LOGIC = [
|
|
|
36
36
|
'test/unit/lbug-pool-fts-load.test.ts',
|
|
37
37
|
'test/unit/repo-manager.test.ts',
|
|
38
38
|
'test/unit/repo-manager-finalize-invariant.test.ts',
|
|
39
|
+
'test/unit/git-utils.test.ts',
|
|
39
40
|
'test/unit/hooks.test.ts',
|
|
40
41
|
'test/unit/hook-db-lock-probe.test.ts',
|
|
41
42
|
'test/unit/cursor-hook.test.ts',
|