gitnexus 1.6.13-rc.61 → 1.6.13-rc.63
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/detect-changes-format.js +6 -0
- package/dist/cli/format-path.d.ts +2 -0
- package/dist/cli/format-path.js +4 -0
- package/dist/cli/i18n/en.d.ts +2 -1
- package/dist/cli/i18n/en.js +2 -1
- package/dist/cli/i18n/resources.d.ts +3 -1
- package/dist/cli/i18n/zh-CN.d.ts +1 -0
- package/dist/cli/i18n/zh-CN.js +2 -1
- package/dist/cli/status.js +1 -2
- package/dist/core/incremental/write-reconciliation.d.ts +23 -0
- package/dist/core/incremental/write-reconciliation.js +80 -0
- package/dist/core/ingestion/cobol/file-types.d.ts +6 -0
- package/dist/core/ingestion/cobol/file-types.js +12 -0
- package/dist/core/ingestion/cobol-processor.d.ts +1 -5
- package/dist/core/ingestion/cobol-processor.js +2 -10
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +1 -1
- package/dist/core/ingestion/pipeline-phases/routes.js +2 -9
- package/dist/core/ingestion/utils/template-file.d.ts +2 -0
- package/dist/core/ingestion/utils/template-file.js +10 -0
- package/dist/core/lbug/csv-generator.js +6 -1
- package/dist/core/lbug/lbug-adapter.js +46 -10
- package/dist/core/run-analyze.js +114 -61
- package/dist/mcp/local/local-backend.js +25 -3
- package/dist/mcp/tools.js +1 -1
- package/dist/server/api.js +1 -1
- package/dist/storage/git.d.ts +2 -0
- package/dist/storage/git.js +6 -1
- package/package.json +2 -2
- package/vendor/lbug-fts/manifest.json +2 -2
- package/vendor/lbug-fts/prebuilds/SHA256SUMS +5 -5
- package/vendor/lbug-fts/prebuilds/darwin-arm64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/darwin-x64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/linux-arm64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/linux-x64/libfts.lbug_extension +0 -0
- package/vendor/lbug-fts/prebuilds/win32-x64/libfts.lbug_extension +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { t } from './i18n/index.js';
|
|
2
2
|
import { formatSymbolLine } from './format-symbol.js';
|
|
3
|
+
import { formatPathForTerminal } from './format-path.js';
|
|
3
4
|
export function formatDetectChangesResult(result) {
|
|
4
5
|
const payload = (result ?? {});
|
|
5
6
|
if (payload.error)
|
|
@@ -14,6 +15,11 @@ export function formatDetectChangesResult(result) {
|
|
|
14
15
|
const notes = [];
|
|
15
16
|
if (payload.partial)
|
|
16
17
|
notes.push(t('tool.detectChanges.partial'));
|
|
18
|
+
if (payload.unmapped_files?.length) {
|
|
19
|
+
notes.push(t('tool.detectChanges.unmappedSource', {
|
|
20
|
+
files: payload.unmapped_files.map(formatPathForTerminal).join(', '),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
17
23
|
// The plain truncation note reassures that the counts are whole. That is only
|
|
18
24
|
// true when the run did NOT also degrade — `changed_count` sums the batches
|
|
19
25
|
// that succeeded — so the two flags together get a different sentence.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Quote control-bearing paths for terminal output; structured results retain raw paths. */
|
|
2
|
+
export const formatPathForTerminal = (filePath) => /[\u0000-\u001f\u007f-\u009f]/.test(filePath)
|
|
3
|
+
? JSON.stringify(filePath).replace(/[\u007f-\u009f]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)
|
|
4
|
+
: filePath;
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -106,7 +106,8 @@ export declare const en: {
|
|
|
106
106
|
readonly 'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.";
|
|
107
107
|
readonly 'tool.detectChanges.noChanges': 'No changes detected.';
|
|
108
108
|
readonly 'tool.detectChanges.noOverlappingSymbols': 'Diff touched {{files}} file(s) but no indexed symbols overlap those hunks — not a clean tree.';
|
|
109
|
-
readonly 'tool.detectChanges.partial': 'PARTIAL RESULT:
|
|
109
|
+
readonly 'tool.detectChanges.partial': 'PARTIAL RESULT: changed-symbol or process mapping is incomplete. Do not read this as a clean pre-commit check.';
|
|
110
|
+
readonly 'tool.detectChanges.unmappedSource': 'No symbols mapped for changed source files: {{files}}. Rebuild the index and inspect the diff; retry alone may not resolve missing or out-of-range symbols.';
|
|
110
111
|
readonly 'tool.detectChanges.truncated': 'LISTING CAPPED: the changed-symbol list was capped, so it does not name every changed symbol. The counts and risk level still cover all of them.';
|
|
111
112
|
readonly 'tool.detectChanges.truncatedDegraded': 'LISTING CAPPED: the changed-symbol list was capped. The run also degraded, so the counts are a lower bound, not a total.';
|
|
112
113
|
readonly 'tool.detectChanges.changesSummary': 'Changes: {{files}} files, {{symbols}} symbols';
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -106,7 +106,8 @@ export const en = {
|
|
|
106
106
|
'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.",
|
|
107
107
|
'tool.detectChanges.noChanges': 'No changes detected.',
|
|
108
108
|
'tool.detectChanges.noOverlappingSymbols': 'Diff touched {{files}} file(s) but no indexed symbols overlap those hunks — not a clean tree.',
|
|
109
|
-
'tool.detectChanges.partial': 'PARTIAL RESULT:
|
|
109
|
+
'tool.detectChanges.partial': 'PARTIAL RESULT: changed-symbol or process mapping is incomplete. Do not read this as a clean pre-commit check.',
|
|
110
|
+
'tool.detectChanges.unmappedSource': 'No symbols mapped for changed source files: {{files}}. Rebuild the index and inspect the diff; retry alone may not resolve missing or out-of-range symbols.',
|
|
110
111
|
'tool.detectChanges.truncated': 'LISTING CAPPED: the changed-symbol list was capped, so it does not name every changed symbol. The counts and risk level still cover all of them.',
|
|
111
112
|
// The reassurance above is only true on its own. When the run also degraded,
|
|
112
113
|
// `changed_count` was summed from the batches that SUCCEEDED, so it is a floor.
|
|
@@ -107,7 +107,8 @@ export declare const cliResources: {
|
|
|
107
107
|
readonly 'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.";
|
|
108
108
|
readonly 'tool.detectChanges.noChanges': 'No changes detected.';
|
|
109
109
|
readonly 'tool.detectChanges.noOverlappingSymbols': 'Diff touched {{files}} file(s) but no indexed symbols overlap those hunks — not a clean tree.';
|
|
110
|
-
readonly 'tool.detectChanges.partial': 'PARTIAL RESULT:
|
|
110
|
+
readonly 'tool.detectChanges.partial': 'PARTIAL RESULT: changed-symbol or process mapping is incomplete. Do not read this as a clean pre-commit check.';
|
|
111
|
+
readonly 'tool.detectChanges.unmappedSource': 'No symbols mapped for changed source files: {{files}}. Rebuild the index and inspect the diff; retry alone may not resolve missing or out-of-range symbols.';
|
|
111
112
|
readonly 'tool.detectChanges.truncated': 'LISTING CAPPED: the changed-symbol list was capped, so it does not name every changed symbol. The counts and risk level still cover all of them.';
|
|
112
113
|
readonly 'tool.detectChanges.truncatedDegraded': 'LISTING CAPPED: the changed-symbol list was capped. The run also degraded, so the counts are a lower bound, not a total.';
|
|
113
114
|
readonly 'tool.detectChanges.changesSummary': 'Changes: {{files}} files, {{symbols}} symbols';
|
|
@@ -435,6 +436,7 @@ export declare const cliResources: {
|
|
|
435
436
|
'tool.detectChanges.noChanges': string;
|
|
436
437
|
'tool.detectChanges.noOverlappingSymbols': string;
|
|
437
438
|
'tool.detectChanges.partial': string;
|
|
439
|
+
'tool.detectChanges.unmappedSource': string;
|
|
438
440
|
'tool.detectChanges.truncated': string;
|
|
439
441
|
'tool.detectChanges.truncatedDegraded': string;
|
|
440
442
|
'tool.detectChanges.changesSummary': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -107,6 +107,7 @@ export declare const zhCN: {
|
|
|
107
107
|
'tool.detectChanges.noChanges': string;
|
|
108
108
|
'tool.detectChanges.noOverlappingSymbols': string;
|
|
109
109
|
'tool.detectChanges.partial': string;
|
|
110
|
+
'tool.detectChanges.unmappedSource': string;
|
|
110
111
|
'tool.detectChanges.truncated': string;
|
|
111
112
|
'tool.detectChanges.truncatedDegraded': string;
|
|
112
113
|
'tool.detectChanges.changesSummary': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -106,7 +106,8 @@ export const zhCN = {
|
|
|
106
106
|
'tool.warn.unknownKind': "--kind '{{kind}}' 不是已知的符号类型(如 Function、Class、Method),不会用于缩小结果范围。",
|
|
107
107
|
'tool.detectChanges.noChanges': '未检测到变更。',
|
|
108
108
|
'tool.detectChanges.noOverlappingSymbols': 'diff 触及 {{files}} 个文件,但没有索引符号与这些 hunk 重叠 — 并非干净工作区。',
|
|
109
|
-
'tool.detectChanges.partial': '
|
|
109
|
+
'tool.detectChanges.partial': '结果不完整:变更符号或流程映射不完整。请勿将其视为通过的提交前检查。',
|
|
110
|
+
'tool.detectChanges.unmappedSource': '变更源码文件未映射到符号:{{files}}。请重建索引并检查 diff;仅重试可能无法解决符号缺失或源码范围不匹配。',
|
|
110
111
|
'tool.detectChanges.truncated': '列表已截断:已变更符号列表被截断,未列出全部变更符号。计数与风险等级仍涵盖全部符号。',
|
|
111
112
|
'tool.detectChanges.truncatedDegraded': '列表已截断:已变更符号列表被截断。本次运行同时不完整,因此计数为下限而非总数。',
|
|
112
113
|
'tool.detectChanges.changesSummary': '变更:{{files}} 个文件,{{symbols}} 个符号',
|
package/dist/cli/status.js
CHANGED
|
@@ -16,6 +16,7 @@ import { getFtsDisabledReason, FTS_DISABLED_MESSAGE } from '../core/search/fts-p
|
|
|
16
16
|
import { detectIndexContentDrift } from '../core/index-content-drift.js';
|
|
17
17
|
import { checkoutIsDirectory, contentRetentionFromMeta, isFullSourceAvailable, } from '../core/content-retention.js';
|
|
18
18
|
import { t } from './i18n/index.js';
|
|
19
|
+
import { formatPathForTerminal as formatDriftPath } from './format-path.js';
|
|
19
20
|
/** How many drifted paths the report names before summarizing the rest. */
|
|
20
21
|
const DRIFT_SAMPLE_LIMIT = 10;
|
|
21
22
|
/**
|
|
@@ -50,8 +51,6 @@ const describeContentDrift = (drift) => {
|
|
|
50
51
|
},
|
|
51
52
|
};
|
|
52
53
|
};
|
|
53
|
-
/** Escape control characters in repo-relative paths before printing. */
|
|
54
|
-
const formatDriftPath = (rel) => /[\u0000-\u001f\u007f]/.test(rel) ? JSON.stringify(rel) : rel;
|
|
55
54
|
const printDriftDetail = (drift) => {
|
|
56
55
|
console.log(t('status.indexContentDrifted', {
|
|
57
56
|
changed: drift.changed.length,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { KnowledgeGraph } from '../graph/types.js';
|
|
2
|
+
interface IdentityRow {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string | null;
|
|
5
|
+
filePath: string | null;
|
|
6
|
+
startLine?: number | null;
|
|
7
|
+
endLine?: number | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Certify the identity fields used by context/impact/detect_changes, including
|
|
11
|
+
* retained rows. A write-set-only probe misses corruption of an unchanged row.
|
|
12
|
+
* Scan one label at a time, without path/range predicates: a corrupt field must
|
|
13
|
+
* not be able to hide its own row. No multi-label scans (#3139), N+1 ID lookups,
|
|
14
|
+
* source-content copies, or whole-graph row materialization.
|
|
15
|
+
*
|
|
16
|
+
* This certifies node identity, not relationships or native storage internals.
|
|
17
|
+
* Call after COPY/checkpoint and after FTS/embedding/checkpoint, before metadata.
|
|
18
|
+
*/
|
|
19
|
+
export declare function reconcileGraphNodeIdentities(graph: KnowledgeGraph, query: (cypher: string) => Promise<IdentityRow[]>, phase: string): Promise<{
|
|
20
|
+
nodes: number;
|
|
21
|
+
tables: number;
|
|
22
|
+
}>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { NODE_TABLES } from '../lbug/schema.js';
|
|
2
|
+
import { sanitizeUTF8 } from '../lbug/csv-generator.js';
|
|
3
|
+
// These layers need a different oracle: folders can outlive their last file,
|
|
4
|
+
// derived nodes may be preserved without recomputation, and PDG can be streamed.
|
|
5
|
+
const EXCLUDED = new Set(['Folder', 'Community', 'Process', 'BasicBlock']);
|
|
6
|
+
const WITHOUT_RANGE = new Set(['File', 'Route', 'Tool']);
|
|
7
|
+
const storedString = (value) => sanitizeUTF8(value || '');
|
|
8
|
+
const storedIdentityField = (node, field) => {
|
|
9
|
+
const value = node.properties[field];
|
|
10
|
+
if (field === 'name' || field === 'filePath') {
|
|
11
|
+
return storedString(value);
|
|
12
|
+
}
|
|
13
|
+
if (node.label === 'Destination') {
|
|
14
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
15
|
+
}
|
|
16
|
+
return value ?? -1;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Certify the identity fields used by context/impact/detect_changes, including
|
|
20
|
+
* retained rows. A write-set-only probe misses corruption of an unchanged row.
|
|
21
|
+
* Scan one label at a time, without path/range predicates: a corrupt field must
|
|
22
|
+
* not be able to hide its own row. No multi-label scans (#3139), N+1 ID lookups,
|
|
23
|
+
* source-content copies, or whole-graph row materialization.
|
|
24
|
+
*
|
|
25
|
+
* This certifies node identity, not relationships or native storage internals.
|
|
26
|
+
* Call after COPY/checkpoint and after FTS/embedding/checkpoint, before metadata.
|
|
27
|
+
*/
|
|
28
|
+
export async function reconcileGraphNodeIdentities(graph, query, phase) {
|
|
29
|
+
const expected = new Map();
|
|
30
|
+
for (const table of NODE_TABLES) {
|
|
31
|
+
if (!EXCLUDED.has(table))
|
|
32
|
+
expected.set(table, new Map());
|
|
33
|
+
}
|
|
34
|
+
for (const node of graph.iterNodes()) {
|
|
35
|
+
const table = expected.get(node.label);
|
|
36
|
+
if (!table)
|
|
37
|
+
continue;
|
|
38
|
+
const id = storedString(node.id);
|
|
39
|
+
if (!id || table.has(id)) {
|
|
40
|
+
throw new Error(`Graph identity reconciliation (${phase}): invalid or duplicate ID ${id}`);
|
|
41
|
+
}
|
|
42
|
+
table.set(id, node);
|
|
43
|
+
}
|
|
44
|
+
let nodes = 0;
|
|
45
|
+
for (const [table, remaining] of expected) {
|
|
46
|
+
const fields = WITHOUT_RANGE.has(table)
|
|
47
|
+
? ['id', 'name', 'filePath']
|
|
48
|
+
: ['id', 'name', 'filePath', 'startLine', 'endLine'];
|
|
49
|
+
const identityFields = fields.slice(1);
|
|
50
|
+
const fail = (detail, cause) => {
|
|
51
|
+
throw new Error(`Graph identity reconciliation failed (${phase}, ${table}): ${detail}. ` +
|
|
52
|
+
'Freshness was not advanced; run `gitnexus analyze --force` to rebuild the graph.', { cause });
|
|
53
|
+
};
|
|
54
|
+
let rows;
|
|
55
|
+
try {
|
|
56
|
+
rows = await query(`MATCH (n:\`${table}\`) RETURN ${fields.map((f) => `n.${f} AS ${f}`).join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
fail(`could not read identity fields: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
60
|
+
}
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
const node = remaining.get(row.id);
|
|
63
|
+
if (!node)
|
|
64
|
+
fail(`unexpected or duplicate ID ${JSON.stringify(row.id)}`);
|
|
65
|
+
for (const field of identityFields) {
|
|
66
|
+
const wanted = storedIdentityField(node, field);
|
|
67
|
+
const actual = field === 'name' || field === 'filePath' ? (row[field] ?? '') : row[field];
|
|
68
|
+
if (actual !== wanted) {
|
|
69
|
+
fail(`${row.id}.${field}: expected ${JSON.stringify(wanted)}, read ${JSON.stringify(actual)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
remaining.delete(row.id);
|
|
73
|
+
nodes++;
|
|
74
|
+
}
|
|
75
|
+
if (remaining.size) {
|
|
76
|
+
fail(`${remaining.size} missing ID(s), including ${JSON.stringify(remaining.keys().next().value)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { nodes, tables: expected.size };
|
|
80
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const COBOL_EXTENSIONS: Set<string>;
|
|
2
|
+
export declare const JCL_EXTENSIONS: Set<string>;
|
|
3
|
+
/** Includes COBOL programs and copybooks accepted by ingestion. */
|
|
4
|
+
export declare function isCobolFile(filePath: string): boolean;
|
|
5
|
+
/** Includes job and procedure aliases accepted by JCL ingestion. */
|
|
6
|
+
export declare function isJclFile(filePath: string): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
// Keep detection shared without loading the analyze-only language providers.
|
|
3
|
+
export const COBOL_EXTENSIONS = new Set(['.cob', '.cbl', '.cobol', '.cpy', '.copybook']);
|
|
4
|
+
export const JCL_EXTENSIONS = new Set(['.jcl', '.job', '.proc']);
|
|
5
|
+
/** Includes COBOL programs and copybooks accepted by ingestion. */
|
|
6
|
+
export function isCobolFile(filePath) {
|
|
7
|
+
return COBOL_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
8
|
+
}
|
|
9
|
+
/** Includes job and procedure aliases accepted by JCL ingestion. */
|
|
10
|
+
export function isJclFile(filePath) {
|
|
11
|
+
return JCL_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
12
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 5. Optionally process JCL files for job-step cross-references
|
|
14
14
|
*/
|
|
15
15
|
import type { KnowledgeGraph } from '../graph/types.js';
|
|
16
|
+
export { isCobolFile, isJclFile } from './cobol/file-types.js';
|
|
16
17
|
interface CobolFile {
|
|
17
18
|
path: string;
|
|
18
19
|
content: string;
|
|
@@ -39,10 +40,6 @@ export interface CobolProcessResult {
|
|
|
39
40
|
initializes: number;
|
|
40
41
|
arithmeticOps: number;
|
|
41
42
|
}
|
|
42
|
-
/** Returns true if the file is a COBOL or copybook file. */
|
|
43
|
-
export declare function isCobolFile(filePath: string): boolean;
|
|
44
|
-
/** Returns true if the file is a JCL file. */
|
|
45
|
-
export declare function isJclFile(filePath: string): boolean;
|
|
46
43
|
/**
|
|
47
44
|
* Process COBOL and JCL files into the knowledge graph.
|
|
48
45
|
*
|
|
@@ -52,4 +49,3 @@ export declare function isJclFile(filePath: string): boolean;
|
|
|
52
49
|
* @returns Summary of what was extracted
|
|
53
50
|
*/
|
|
54
51
|
export declare const processCobol: (graph: KnowledgeGraph, files: CobolFile[], allPathSet: ReadonlySet<string>) => CobolProcessResult;
|
|
55
|
-
export {};
|
|
@@ -20,21 +20,13 @@ import { preprocessCobolSource, extractCobolSymbolsWithRegex, } from './cobol/co
|
|
|
20
20
|
import { expandCopies } from './cobol/cobol-copy-expander.js';
|
|
21
21
|
import { processJclFiles } from './cobol/jcl-processor.js';
|
|
22
22
|
import { resolveCobolCopyTarget } from './languages/cobol/copy-target.js';
|
|
23
|
+
import { COBOL_EXTENSIONS, JCL_EXTENSIONS } from './cobol/file-types.js';
|
|
24
|
+
export { isCobolFile, isJclFile } from './cobol/file-types.js';
|
|
23
25
|
import { logger } from '../logger.js';
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
// File detection
|
|
26
28
|
// ---------------------------------------------------------------------------
|
|
27
|
-
const COBOL_EXTENSIONS = new Set(['.cob', '.cbl', '.cobol', '.cpy', '.copybook']);
|
|
28
|
-
const JCL_EXTENSIONS = new Set(['.jcl', '.job', '.proc']);
|
|
29
29
|
const COPYBOOK_EXTENSIONS = new Set(['.cpy', '.copybook']);
|
|
30
|
-
/** Returns true if the file is a COBOL or copybook file. */
|
|
31
|
-
export function isCobolFile(filePath) {
|
|
32
|
-
return COBOL_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
33
|
-
}
|
|
34
|
-
/** Returns true if the file is a JCL file. */
|
|
35
|
-
export function isJclFile(filePath) {
|
|
36
|
-
return JCL_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
37
|
-
}
|
|
38
30
|
/** Returns true if the file is a COBOL copybook. */
|
|
39
31
|
function isCopybook(filePath) {
|
|
40
32
|
return COPYBOOK_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @output routeRegistry, handlerContents
|
|
12
12
|
*/
|
|
13
13
|
import type { PipelinePhase } from './types.js';
|
|
14
|
+
export { isTemplateRouteCandidate } from '../utils/template-file.js';
|
|
14
15
|
export interface RouteEntry {
|
|
15
16
|
filePath: string;
|
|
16
17
|
source: string;
|
|
@@ -39,6 +40,5 @@ export interface TemplateFetchCall {
|
|
|
39
40
|
fetchURL: string;
|
|
40
41
|
lineNumber: number;
|
|
41
42
|
}
|
|
42
|
-
export declare const isTemplateRouteCandidate: (filePath: string) => boolean;
|
|
43
43
|
export declare function extractTemplateStaticFetchCalls(filePath: string, content: string, namedRouteUrls?: ReadonlyMap<string, string>): TemplateFetchCall[];
|
|
44
44
|
export declare const routesPhase: PipelinePhase<RoutesOutput>;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @output routeRegistry, handlerContents
|
|
12
12
|
*/
|
|
13
13
|
import { getPhaseOutput } from './types.js';
|
|
14
|
-
import {
|
|
14
|
+
import { isTemplateRouteCandidate } from '../utils/template-file.js';
|
|
15
15
|
import { nextjsFileToRouteURL, normalizeFetchURL } from '../route-extractors/nextjs.js';
|
|
16
16
|
import { expoFileToRouteURL } from '../route-extractors/expo.js';
|
|
17
17
|
import { phpFileToRouteURL } from '../route-extractors/php.js';
|
|
@@ -25,6 +25,7 @@ import { generateId } from '../../../lib/utils.js';
|
|
|
25
25
|
import { readFileContents } from '../filesystem-walker.js';
|
|
26
26
|
import { isDev } from '../utils/env.js';
|
|
27
27
|
import { logger } from '../../logger.js';
|
|
28
|
+
export { isTemplateRouteCandidate } from '../utils/template-file.js';
|
|
28
29
|
const EXPO_NAV_PATTERNS = [
|
|
29
30
|
/router\.(push|replace|navigate)\(\s*['"`]([^'"`]+)['"`]/g,
|
|
30
31
|
/<Link\s+[^>]*href=\s*['"`]([^'"`]+)['"`]/g,
|
|
@@ -65,14 +66,6 @@ const TEMPLATE_NAMED_ROUTE_PATTERNS = [
|
|
|
65
66
|
function hasRouteParameters(routeUrl) {
|
|
66
67
|
return /\{[^}]+\}/.test(routeUrl);
|
|
67
68
|
}
|
|
68
|
-
export const isTemplateRouteCandidate = (filePath) => {
|
|
69
|
-
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
70
|
-
return (normalized.endsWith('.html') ||
|
|
71
|
-
normalized.endsWith('.htm') ||
|
|
72
|
-
normalized.endsWith('.ejs') ||
|
|
73
|
-
normalized.endsWith('.hbs') ||
|
|
74
|
-
isBladeTemplateFilename(normalized));
|
|
75
|
-
};
|
|
76
69
|
export function extractTemplateStaticFetchCalls(filePath, content, namedRouteUrls = new Map()) {
|
|
77
70
|
const calls = [];
|
|
78
71
|
const seen = new Set();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { isBladeTemplateFilename } from '../../../_shared/index.js';
|
|
2
|
+
/** Templates whose URLs and fetches can contribute route relationships to the graph. */
|
|
3
|
+
export const isTemplateRouteCandidate = (filePath) => {
|
|
4
|
+
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
5
|
+
return (normalized.endsWith('.html') ||
|
|
6
|
+
normalized.endsWith('.htm') ||
|
|
7
|
+
normalized.endsWith('.ejs') ||
|
|
8
|
+
normalized.endsWith('.hbs') ||
|
|
9
|
+
isBladeTemplateFilename(normalized));
|
|
10
|
+
};
|
|
@@ -91,9 +91,14 @@ export const sanitizeUTF8 = (str) => {
|
|
|
91
91
|
};
|
|
92
92
|
export const escapeCSVField = (value) => {
|
|
93
93
|
if (value === undefined || value === null)
|
|
94
|
-
return '
|
|
94
|
+
return '';
|
|
95
95
|
let str = String(value);
|
|
96
96
|
str = sanitizeUTF8(str);
|
|
97
|
+
// Preserve the NULL meaning of absent strings from the 0.18.3 writer.
|
|
98
|
+
// Newer LadybugDB readers retain a quoted empty field as a STRING value;
|
|
99
|
+
// it must stay unquoted or unresolved destination addresses can join.
|
|
100
|
+
if (str.length === 0)
|
|
101
|
+
return '';
|
|
97
102
|
return `"${str.replace(/"/g, '""')}"`;
|
|
98
103
|
};
|
|
99
104
|
export const escapeCSVNumber = (value, defaultValue = -1) => {
|
|
@@ -908,23 +908,59 @@ const doInitLbug = async (dbPath, options = {}) => {
|
|
|
908
908
|
};
|
|
909
909
|
/**
|
|
910
910
|
* Run a COPY, retrying once with IGNORE_ERRORS=true (which skips row-level
|
|
911
|
-
* errors) on first failure.
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
* throw to propagate the failure.
|
|
911
|
+
* errors) on first failure. Log the original failure and native COPY/warning
|
|
912
|
+
* receipts even when the retry succeeds. Node COPY requires every row; a
|
|
913
|
+
* skipped-node receipt is a failure. Call sites retain their own message
|
|
914
|
+
* limits and relationship fallback policy.
|
|
916
915
|
*/
|
|
917
|
-
const copyCsvWithRetry = async (targetConn, copyQuery, onError) => {
|
|
916
|
+
const copyCsvWithRetry = async (targetConn, copyQuery, onError, expectedRows) => {
|
|
918
917
|
try {
|
|
919
918
|
await queryAndDrain(targetConn, copyQuery);
|
|
920
919
|
}
|
|
921
|
-
catch {
|
|
920
|
+
catch (firstError) {
|
|
921
|
+
logger.warn({ err: firstError, copyQuery }, 'First COPY failure; retrying with IGNORE_ERRORS=true');
|
|
922
922
|
try {
|
|
923
923
|
const retryQuery = copyQuery.replace('auto_detect=false)', 'auto_detect=false, IGNORE_ERRORS=true)');
|
|
924
|
-
|
|
924
|
+
// Keep COPY and its connection-local warning receipt in one critical
|
|
925
|
+
// section. Only project diagnostics, never skipped_line_or_record: that
|
|
926
|
+
// column contains source text. Warnings are retained at a bounded native
|
|
927
|
+
// limit, so their count is a lower bound, not the exact skipped total.
|
|
928
|
+
const retry = async () => {
|
|
929
|
+
await drainQueryResult(await targetConn.query('CALL CLEAR_WARNINGS()'));
|
|
930
|
+
const result = await readQueryRows(await targetConn.query(retryQuery));
|
|
931
|
+
const warnings = await readQueryRows(await targetConn.query('CALL SHOW_WARNINGS() RETURN message, file_path, line_number'));
|
|
932
|
+
// The native warning limit can be zero; warning rows alone cannot
|
|
933
|
+
// prove that every CSV row landed. Node COPY exposes its copied count
|
|
934
|
+
// in the result receipt even when warning retention is disabled.
|
|
935
|
+
const countReceipt = result
|
|
936
|
+
.map((row) => String(row.result ?? ''))
|
|
937
|
+
.map((message) => /^(\d+) tuples have been copied/.exec(message))
|
|
938
|
+
.find(Boolean);
|
|
939
|
+
const copiedRows = countReceipt ? Number(countReceipt[1]) : undefined;
|
|
940
|
+
logger.warn({
|
|
941
|
+
copyQuery,
|
|
942
|
+
copyResult: result,
|
|
943
|
+
expectedRows,
|
|
944
|
+
copiedRows,
|
|
945
|
+
skippedRows: expectedRows !== undefined && copiedRows !== undefined
|
|
946
|
+
? Math.max(0, expectedRows - copiedRows)
|
|
947
|
+
: undefined,
|
|
948
|
+
retainedWarnings: warnings.length,
|
|
949
|
+
warningSamples: warnings.slice(0, 5),
|
|
950
|
+
}, 'COPY retry completed; retained warnings describe skipped rows (a lower bound)');
|
|
951
|
+
if (expectedRows !== undefined && (copiedRows !== expectedRows || warnings.length > 0)) {
|
|
952
|
+
throw new Error(`COPY retry skipped rows or could not verify a complete node load ` +
|
|
953
|
+
`(copied ${copiedRows ?? 'unknown'} of ${expectedRows}; ${warnings.length} retained warning(s))`);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
await (isSharedSingletonConn(targetConn) ? withConnLock(retry) : retry());
|
|
925
957
|
}
|
|
926
958
|
catch (retryErr) {
|
|
927
|
-
|
|
959
|
+
const firstMessage = firstError instanceof Error ? firstError.message : String(firstError);
|
|
960
|
+
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
961
|
+
onError(new Error(`COPY retry failed: ${retryMessage}; first failure: ${firstMessage}`, {
|
|
962
|
+
cause: retryErr,
|
|
963
|
+
}));
|
|
928
964
|
}
|
|
929
965
|
}
|
|
930
966
|
};
|
|
@@ -992,7 +1028,7 @@ const copyNodeCSVs = async (targetConn, nodeFileEntries, log, totalSteps) => {
|
|
|
992
1028
|
// sizing was calibrated for — name the knob and the mechanism.
|
|
993
1029
|
const remedy = bufferPoolExhaustionRemedy(retryMsg);
|
|
994
1030
|
throw new Error(`COPY failed for ${table}: ${retryMsg.slice(0, 200)}${remedy ? ` ${remedy}` : ''}`);
|
|
995
|
-
});
|
|
1031
|
+
}, rows);
|
|
996
1032
|
}
|
|
997
1033
|
};
|
|
998
1034
|
/**
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -24,6 +24,7 @@ import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
|
24
24
|
import { logUnresolvedReceiverFiles, summarizeUnresolvedReceivers, } from './ingestion/scope-resolution/unresolved-receivers.js';
|
|
25
25
|
import { summarizeUndecidedSatisfaction } from './ingestion/scope-resolution/undecided-satisfaction.js';
|
|
26
26
|
import { summarizeScopeExtractionFailures } from './ingestion/scope-resolution/scope-extraction-failures.js';
|
|
27
|
+
import { reconcileGraphNodeIdentities } from './incremental/write-reconciliation.js';
|
|
27
28
|
import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
28
29
|
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, nodeTablesWithRowsForFiles, snapshotDerivedRelsForFiles, restoreDerivedRels, ensureEmbeddingRowDmlSafe, ensureFtsRowDmlSafe, readIndexCatalogSnapshot, INDEX_CATALOG_UNREADABLE, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, deleteAllAdvisedBy, deleteAllDestinations, deleteSpringAopEvidenceNodes, deleteSpringAutoConfigurationDeclarations, deleteSpringAutoConfigurationSyntheticClasses, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
|
|
29
30
|
import { estimateBufferPool, setBufferPoolSizeHint, resolveNativeSafeStorageDir, } from './lbug/lbug-config.js';
|
|
@@ -36,7 +37,7 @@ import { cjkSegmentationModeMismatch, getSearchFTSCjkSegmentation, initialiseSea
|
|
|
36
37
|
import { getExtensionCapability, getExtensionCapabilities, getFtsCapability, resolveAnalyzeInstallPolicy, } from './lbug/extension-loader.js';
|
|
37
38
|
import { diagnoseExtensionLoad, extractExtensionPath, usesClassifiedLoadRemedy, } from './lbug/extension-load-error.js';
|
|
38
39
|
import { resolveFtsVersionPair } from './lbug/vendored-extension-path.js';
|
|
39
|
-
import { startWalCheckpointDriver, checkpointOnce, } from './lbug/wal-checkpoint-driver.js';
|
|
40
|
+
import { startWalCheckpointDriver, checkpointOnce, isManualCheckpointEnabled, } from './lbug/wal-checkpoint-driver.js';
|
|
40
41
|
import { ftsCrashParkFailureMessage, quarantineSidecarsForDirtyRecovery, inspectLbugSidecars, } from './lbug/sidecar-recovery.js';
|
|
41
42
|
import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, setShareOptOut, adoptFlatBranchLabel, isReadOnlyFilesystemError, isRepoRegistered, cleanupOldKuzuFiles, reconcileMetadataFiles, ensureStoragePathWritable, isMissingFilesystemError, INDEX_METADATA_FILE, CONTENT_RETENTION_SCHEMA_VERSION, } from '../storage/repo-manager.js';
|
|
42
43
|
import { ANALYZE_FORCE_STORAGE_REQUIREMENTS, ANALYZE_STORAGE_REQUIREMENTS, defaultStoragePath, requireRegisteredStoragePath, requireStoragePath, resolveStoragePath, } from '../storage/storage-resolver.js';
|
|
@@ -2046,7 +2047,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
2046
2047
|
// (Bugbot review on PR #1479: a prediction that flipped post-pipeline
|
|
2047
2048
|
// could skip the embedding cache load and then take the full-rebuild
|
|
2048
2049
|
// path, silently losing embeddings).
|
|
2049
|
-
const
|
|
2050
|
+
const incrementalEligible = !options.force &&
|
|
2050
2051
|
!!existingMeta &&
|
|
2051
2052
|
// Belt and braces, not a second gate: the guard above already set `force`
|
|
2052
2053
|
// on exactly this condition, and `!options.force` short-circuits before
|
|
@@ -2057,6 +2058,12 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
2057
2058
|
Object.keys(existingMeta.fileHashes).length > 0 &&
|
|
2058
2059
|
repoHasGit &&
|
|
2059
2060
|
allFilePaths.length > 0;
|
|
2061
|
+
// Select a full build before any selective mutation when the operator has
|
|
2062
|
+
// disabled the checkpoint needed to certify an incremental publication.
|
|
2063
|
+
const isIncremental = incrementalEligible && isManualCheckpointEnabled();
|
|
2064
|
+
if (incrementalEligible && !isIncremental) {
|
|
2065
|
+
log('Manual WAL checkpoints are disabled; switching to a full DB write before mutation.');
|
|
2066
|
+
}
|
|
2060
2067
|
const hashDiff = isIncremental
|
|
2061
2068
|
? diffFileHashes(newFileHashes, existingMeta.fileHashes)
|
|
2062
2069
|
: undefined;
|
|
@@ -2287,6 +2294,25 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
2287
2294
|
// collapse check compares the whole in-memory graph against the whole DB,
|
|
2288
2295
|
// which is only a like-for-like comparison on a full rebuild.
|
|
2289
2296
|
let wroteChangedSubgraphOnly = false;
|
|
2297
|
+
const markIncrementalGraphVerification = async () => {
|
|
2298
|
+
if (buildPath !== lbugPath)
|
|
2299
|
+
return;
|
|
2300
|
+
const latest = await loadMeta(metaDir);
|
|
2301
|
+
if (!latest?.incrementalInProgress) {
|
|
2302
|
+
throw new Error('Cannot certify incremental graph without its dirty metadata marker.');
|
|
2303
|
+
}
|
|
2304
|
+
// A failed or aborted identity scan is a graph failure. FTS-only repair
|
|
2305
|
+
// and FTS crash recovery must not clear it while retaining these rows.
|
|
2306
|
+
await saveMeta(metaDir, {
|
|
2307
|
+
...latest,
|
|
2308
|
+
incrementalInProgress: {
|
|
2309
|
+
...latest.incrementalInProgress,
|
|
2310
|
+
phase: 'graph-reconciliation',
|
|
2311
|
+
updatedAt: Date.now(),
|
|
2312
|
+
checkpointSucceeded: false,
|
|
2313
|
+
},
|
|
2314
|
+
});
|
|
2315
|
+
};
|
|
2290
2316
|
let incrementalFtsRebuildTables;
|
|
2291
2317
|
if (isIncremental && hashDiff) {
|
|
2292
2318
|
// ── Incremental DB writeback ───────────────────────────────────
|
|
@@ -2335,6 +2361,9 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
2335
2361
|
// strict-equality consumer of the diagnostics shape.
|
|
2336
2362
|
let droppedImporterChunks = 0;
|
|
2337
2363
|
const saveIncrementalDirtyState = async (phase, extra = {}) => {
|
|
2364
|
+
// Do not stamp live metadata while writing a staging database.
|
|
2365
|
+
if (buildPath !== lbugPath)
|
|
2366
|
+
return;
|
|
2338
2367
|
await saveMeta(metaDir, {
|
|
2339
2368
|
...existingMeta,
|
|
2340
2369
|
incrementalInProgress: {
|
|
@@ -2995,6 +3024,10 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
2995
3024
|
log(`Boundary WAL checkpoint failed on the in-place FTS path (best-effort): ${detail}. ` +
|
|
2996
3025
|
'Continuing; recovery will treat the graph-boundary checkpoint as unsuccessful.');
|
|
2997
3026
|
}
|
|
3027
|
+
if (wroteChangedSubgraphOnly) {
|
|
3028
|
+
await markIncrementalGraphVerification();
|
|
3029
|
+
await reconcileGraphNodeIdentities(pipelineResult.graph, executeQuery, 'post-COPY/checkpoint');
|
|
3030
|
+
}
|
|
2998
3031
|
if (shouldStampFtsDirtyPhase(ftsWritePlan)) {
|
|
2999
3032
|
// Lift the prior-meta precondition: a first-ever in-place run (Windows
|
|
3000
3033
|
// full rebuild, or any in-place incremental) must stamp too. Staging
|
|
@@ -3083,6 +3116,8 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
3083
3116
|
? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
|
|
3084
3117
|
: undefined,
|
|
3085
3118
|
});
|
|
3119
|
+
if (wroteChangedSubgraphOnly)
|
|
3120
|
+
await markIncrementalGraphVerification();
|
|
3086
3121
|
if (ftsResult.ok) {
|
|
3087
3122
|
progress('fts', 90, 'Search indexes ready');
|
|
3088
3123
|
}
|
|
@@ -3127,6 +3162,11 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
3127
3162
|
: FTS_UNAVAILABLE_MESSAGE);
|
|
3128
3163
|
progress('fts', 90, 'Search indexes skipped (FTS unavailable)');
|
|
3129
3164
|
}
|
|
3165
|
+
if (wroteChangedSubgraphOnly) {
|
|
3166
|
+
// FTS has returned. Later embedding/finalization failures must require
|
|
3167
|
+
// graph recovery, since post-FTS node identities are not yet certified.
|
|
3168
|
+
await markIncrementalGraphVerification();
|
|
3169
|
+
}
|
|
3130
3170
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
3131
3171
|
// Runs on BOTH the full-rebuild path and the incremental path:
|
|
3132
3172
|
// - Full rebuild / escalated write: DB was wiped, every cached row
|
|
@@ -3909,6 +3949,78 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
3909
3949
|
// object is fully computed at this point; only its write is deferred.
|
|
3910
3950
|
// Parse-cache publish waits until after that swap + saveMeta so a failed
|
|
3911
3951
|
// registerRepo / close / swap cannot replace live shards (#3153).
|
|
3952
|
+
if (wroteChangedSubgraphOnly) {
|
|
3953
|
+
// Registry freshness must not advance either. Include FTS, embedding
|
|
3954
|
+
// restoration and the final WAL drain in the certified boundary.
|
|
3955
|
+
await markIncrementalGraphVerification();
|
|
3956
|
+
await walCheckpointDriver.stop();
|
|
3957
|
+
if (!(await checkpointOnce())) {
|
|
3958
|
+
throw new Error('Graph identity reconciliation failed: final checkpoint could not be verified; run `gitnexus analyze --force`.');
|
|
3959
|
+
}
|
|
3960
|
+
await reconcileGraphNodeIdentities(pipelineResult.graph, executeQuery, 'pre-publish/checkpoint');
|
|
3961
|
+
}
|
|
3962
|
+
// Keep generated .gitnexus contents ignored without editing the user's root .gitignore.
|
|
3963
|
+
await ensureGitNexusIgnored(repoPath, storagePath);
|
|
3964
|
+
// ── Close LadybugDB ──────────────────────────────────────────────
|
|
3965
|
+
// Stop the manual checkpoint driver before closeLbug so its
|
|
3966
|
+
// in-flight CHECKPOINT cannot race the `safeClose` CHECKPOINT.
|
|
3967
|
+
await walCheckpointDriver.stop();
|
|
3968
|
+
// CLI callers (about to process.exit) skip the native close to dodge a
|
|
3969
|
+
// LadybugDB destructor double-free after --pdg writes — closeLbugBeforeExit
|
|
3970
|
+
// CHECKPOINTs for durability then leaves the handles for process exit to
|
|
3971
|
+
// reclaim (#2264). Long-lived callers close for real.
|
|
3972
|
+
//
|
|
3973
|
+
// On Windows a swap must release the build handle before the rename (a
|
|
3974
|
+
// same-process open file can't be renamed), so it forces a real close —
|
|
3975
|
+
// safe because windowsSwapOk excludes --pdg (the #2264 case). POSIX renames
|
|
3976
|
+
// an open file, so it keeps the skip-native-close there.
|
|
3977
|
+
const forceRealCloseForSwap = useAtomicSwap && process.platform === 'win32';
|
|
3978
|
+
await (options.skipNativeCloseOnExit && !forceRealCloseForSwap
|
|
3979
|
+
? closeLbugBeforeExit()
|
|
3980
|
+
: closeLbug());
|
|
3981
|
+
// #2 atomic publish: the fresh index was built at buildPath (a full rebuild,
|
|
3982
|
+
// or an opt-in atomic incremental that copied the live index in first). Swap
|
|
3983
|
+
// it over the live lbugPath in one rename so an MCP reader that opened
|
|
3984
|
+
// mid-build only ever saw the previous complete index — never a wiped/
|
|
3985
|
+
// half-built file. The close above checkpoint-consolidated buildPath to a
|
|
3986
|
+
// single file (no .wal), so the rename publishes a complete index; a reader
|
|
3987
|
+
// holding the old inode keeps a consistent stale snapshot until the pool
|
|
3988
|
+
// re-opens onto the new one (the pool staleness invalidation). Runs only on
|
|
3989
|
+
// success — a thrown error skips this, leaving the live index intact and the
|
|
3990
|
+
// temp build to be cleared by the next run's wipe.
|
|
3991
|
+
// Only publish if the build actually produced a DB at buildPath. A
|
|
3992
|
+
// degenerate run (empty repo, or a mocked pipeline that never opened the
|
|
3993
|
+
// store) leaves nothing to swap — skip rather than throw ENOENT.
|
|
3994
|
+
const builtDbExists = useAtomicSwap
|
|
3995
|
+
? await fs.stat(buildPath).then(() => true, () => false)
|
|
3996
|
+
: false;
|
|
3997
|
+
if (useAtomicSwap && builtDbExists) {
|
|
3998
|
+
await retryRename(buildPath, lbugPath);
|
|
3999
|
+
liveIndexMutationStarted = true;
|
|
4000
|
+
// Clear any sidecars orphaned beside the replaced file. A cleanly-closed
|
|
4001
|
+
// prior index has none; a crashed one could, and it would be replay
|
|
4002
|
+
// poison next to the freshly published index. Best-effort.
|
|
4003
|
+
for (const suffix of ['.wal', '.shadow', '.wal.checkpoint']) {
|
|
4004
|
+
await fs.rm(`${lbugPath}${suffix}`, { force: true }).catch(() => { });
|
|
4005
|
+
}
|
|
4006
|
+
// #2614 F4: if the final checkpoint silently failed, the build may still
|
|
4007
|
+
// carry a residual .wal/.shadow under the temp name. MOVE it beside the
|
|
4008
|
+
// published index (not orphan/delete it) so the next open replays the
|
|
4009
|
+
// delta, rather than leaving it under a name LadybugDB never reconciles.
|
|
4010
|
+
for (const suffix of ['.wal', '.shadow']) {
|
|
4011
|
+
await fs.rename(`${buildPath}${suffix}`, `${lbugPath}${suffix}`).catch(() => { });
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
// #2614 F1: stamp the freshness metadata now that the index is published.
|
|
4015
|
+
// When meta.indexedAt becomes visible, lbugPath already resolves to the new
|
|
4016
|
+
// inode, so a reader reiniting on the stamp opens the fresh graph rather
|
|
4017
|
+
// than latching on the old one. Leaving the dirty flag set across the swap
|
|
4018
|
+
// is a crash-safety improvement: a failed swap leaves the previous index
|
|
4019
|
+
// live and the next run recovers via the full-rebuild path.
|
|
4020
|
+
await saveMeta(metaDir, meta);
|
|
4021
|
+
// Registry freshness is published only after the graph and its metadata.
|
|
4022
|
+
// A failed close, swap, or metadata save must leave the previous registry
|
|
4023
|
+
// receipt intact, just as it leaves the cache unpublished.
|
|
3912
4024
|
// Forward the --name alias and the registry-collision bypass bit.
|
|
3913
4025
|
// `allowDuplicateName` is its own concern — independent from the
|
|
3914
4026
|
// pipeline `force` above. The CLI maps it from
|
|
@@ -3946,8 +4058,6 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
3946
4058
|
log(`Warning: could not sync the workspace branch label (${e.message}); continuing.`);
|
|
3947
4059
|
}
|
|
3948
4060
|
}
|
|
3949
|
-
// Keep generated .gitnexus contents ignored without editing the user's root .gitignore.
|
|
3950
|
-
await ensureGitNexusIgnored(repoPath, storagePath);
|
|
3951
4061
|
// ── Generate AI context files (best-effort) ───────────────────────
|
|
3952
4062
|
let aggregatedClusterCount = 0;
|
|
3953
4063
|
if (pipelineResult.communityResult?.communities) {
|
|
@@ -3984,63 +4094,6 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
|
|
|
3984
4094
|
// Best-effort — don't fail the entire analysis for context file issues
|
|
3985
4095
|
}
|
|
3986
4096
|
}
|
|
3987
|
-
// ── Close LadybugDB ──────────────────────────────────────────────
|
|
3988
|
-
// Stop the manual checkpoint driver before closeLbug so its
|
|
3989
|
-
// in-flight CHECKPOINT cannot race the `safeClose` CHECKPOINT.
|
|
3990
|
-
await walCheckpointDriver.stop();
|
|
3991
|
-
// CLI callers (about to process.exit) skip the native close to dodge a
|
|
3992
|
-
// LadybugDB destructor double-free after --pdg writes — closeLbugBeforeExit
|
|
3993
|
-
// CHECKPOINTs for durability then leaves the handles for process exit to
|
|
3994
|
-
// reclaim (#2264). Long-lived callers close for real.
|
|
3995
|
-
//
|
|
3996
|
-
// On Windows a swap must release the build handle before the rename (a
|
|
3997
|
-
// same-process open file can't be renamed), so it forces a real close —
|
|
3998
|
-
// safe because windowsSwapOk excludes --pdg (the #2264 case). POSIX renames
|
|
3999
|
-
// an open file, so it keeps the skip-native-close there.
|
|
4000
|
-
const forceRealCloseForSwap = useAtomicSwap && process.platform === 'win32';
|
|
4001
|
-
await (options.skipNativeCloseOnExit && !forceRealCloseForSwap
|
|
4002
|
-
? closeLbugBeforeExit()
|
|
4003
|
-
: closeLbug());
|
|
4004
|
-
// #2 atomic publish: the fresh index was built at buildPath (a full rebuild,
|
|
4005
|
-
// or an opt-in atomic incremental that copied the live index in first). Swap
|
|
4006
|
-
// it over the live lbugPath in one rename so an MCP reader that opened
|
|
4007
|
-
// mid-build only ever saw the previous complete index — never a wiped/
|
|
4008
|
-
// half-built file. The close above checkpoint-consolidated buildPath to a
|
|
4009
|
-
// single file (no .wal), so the rename publishes a complete index; a reader
|
|
4010
|
-
// holding the old inode keeps a consistent stale snapshot until the pool
|
|
4011
|
-
// re-opens onto the new one (the pool staleness invalidation). Runs only on
|
|
4012
|
-
// success — a thrown error skips this, leaving the live index intact and the
|
|
4013
|
-
// temp build to be cleared by the next run's wipe.
|
|
4014
|
-
// Only publish if the build actually produced a DB at buildPath. A
|
|
4015
|
-
// degenerate run (empty repo, or a mocked pipeline that never opened the
|
|
4016
|
-
// store) leaves nothing to swap — skip rather than throw ENOENT.
|
|
4017
|
-
const builtDbExists = useAtomicSwap
|
|
4018
|
-
? await fs.stat(buildPath).then(() => true, () => false)
|
|
4019
|
-
: false;
|
|
4020
|
-
if (useAtomicSwap && builtDbExists) {
|
|
4021
|
-
await retryRename(buildPath, lbugPath);
|
|
4022
|
-
liveIndexMutationStarted = true;
|
|
4023
|
-
// Clear any sidecars orphaned beside the replaced file. A cleanly-closed
|
|
4024
|
-
// prior index has none; a crashed one could, and it would be replay
|
|
4025
|
-
// poison next to the freshly published index. Best-effort.
|
|
4026
|
-
for (const suffix of ['.wal', '.shadow', '.wal.checkpoint']) {
|
|
4027
|
-
await fs.rm(`${lbugPath}${suffix}`, { force: true }).catch(() => { });
|
|
4028
|
-
}
|
|
4029
|
-
// #2614 F4: if the final checkpoint silently failed, the build may still
|
|
4030
|
-
// carry a residual .wal/.shadow under the temp name. MOVE it beside the
|
|
4031
|
-
// published index (not orphan/delete it) so the next open replays the
|
|
4032
|
-
// delta, rather than leaving it under a name LadybugDB never reconciles.
|
|
4033
|
-
for (const suffix of ['.wal', '.shadow']) {
|
|
4034
|
-
await fs.rename(`${buildPath}${suffix}`, `${lbugPath}${suffix}`).catch(() => { });
|
|
4035
|
-
}
|
|
4036
|
-
}
|
|
4037
|
-
// #2614 F1: stamp the freshness metadata now that the index is published.
|
|
4038
|
-
// When meta.indexedAt becomes visible, lbugPath already resolves to the new
|
|
4039
|
-
// inode, so a reader reiniting on the stamp opens the fresh graph rather
|
|
4040
|
-
// than latching on the old one. Leaving the dirty flag set across the swap
|
|
4041
|
-
// is a crash-safety improvement: a failed swap leaves the previous index
|
|
4042
|
-
// live and the next run recovers via the full-rebuild path.
|
|
4043
|
-
await saveMeta(metaDir, meta);
|
|
4044
4097
|
// Persist the incremental parse cache only after a successful graph
|
|
4045
4098
|
// publish (#3153). try/catch so a cache-write failure never breaks an
|
|
4046
4099
|
// otherwise successful indexing run. Prune stale chunk-hash entries first
|
|
@@ -9,7 +9,7 @@ import { resolveGraphPath } from '../../storage/shared-store.js';
|
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
12
|
-
import { scoreImpactRisk, unusedAxesForImpactWalk } from '../../_shared/index.js';
|
|
12
|
+
import { scoreImpactRisk, unusedAxesForImpactWalk, getLanguageFromFilename, } from '../../_shared/index.js';
|
|
13
13
|
import { initLbug, executeQuery, executeParameterized, ensureVectorExtension, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
|
|
14
14
|
import { queryClassBeanMetadata } from './bean-metadata.js';
|
|
15
15
|
import { querySpringAopMetadata } from './aop-metadata.js';
|
|
@@ -20,8 +20,10 @@ import { shapeQueryProcessAttaches } from './query-process-attaches.js';
|
|
|
20
20
|
import { LBUG_ID_PROBE_BATCH_SIZE, LBUG_QUERY_BATCH_SIZE } from '../../core/lbug/query-batch.js';
|
|
21
21
|
import { chunk, mapConcurrent } from '../../lib/utils.js';
|
|
22
22
|
import { pathSuffixOf } from './path-predicate.js';
|
|
23
|
+
import { isCobolFile, isJclFile } from '../../core/ingestion/cobol/file-types.js';
|
|
23
24
|
import { toOneBasedLine } from '../../core/ingestion/utils/line-base.js';
|
|
24
25
|
import { isTestFilePath } from '../../core/ingestion/utils/test-file-path.js';
|
|
26
|
+
import { isTemplateRouteCandidate } from '../../core/ingestion/utils/template-file.js';
|
|
25
27
|
import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/lbug-config.js';
|
|
26
28
|
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
27
29
|
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
@@ -5193,6 +5195,7 @@ export class LocalBackend {
|
|
|
5193
5195
|
// throwaway arrays the size of the row set (40k rows 11.4ms → 4.5ms, 200k
|
|
5194
5196
|
// rows 71.3ms → 26.6ms).
|
|
5195
5197
|
const exactlyMatchedPaths = new Set();
|
|
5198
|
+
const mappedPaths = new Set();
|
|
5196
5199
|
for (const row of symbolRows) {
|
|
5197
5200
|
if (row.filePath === row.diffPath)
|
|
5198
5201
|
exactlyMatchedPaths.add(row.diffPath);
|
|
@@ -5204,6 +5207,9 @@ export class LocalBackend {
|
|
|
5204
5207
|
const hunks = hunksByPath.get(diffPath) ?? [];
|
|
5205
5208
|
if (!hunksOverlapRange(hunks, sym.startLine, sym.endLine))
|
|
5206
5209
|
continue;
|
|
5210
|
+
// A suffix fallback is a hint, not proof that this is the changed file.
|
|
5211
|
+
if (sym.filePath === diffPath)
|
|
5212
|
+
mappedPaths.add(diffPath);
|
|
5207
5213
|
if (changedSymbols.has(sym.id))
|
|
5208
5214
|
continue;
|
|
5209
5215
|
changedSymbols.set(sym.id, {
|
|
@@ -5214,6 +5220,21 @@ export class LocalBackend {
|
|
|
5214
5220
|
change_type: 'touched',
|
|
5215
5221
|
});
|
|
5216
5222
|
}
|
|
5223
|
+
// An empty successful query cannot prove a source diff is safe: its rows
|
|
5224
|
+
// may be missing, outside indexed spans, or not yet indexed. Keep ordinary
|
|
5225
|
+
// docs/config diffs measurable, but withhold a ranked source-risk verdict.
|
|
5226
|
+
const isSourceFile = (file) => getLanguageFromFilename(file) !== null ||
|
|
5227
|
+
isCobolFile(file) ||
|
|
5228
|
+
isJclFile(file) ||
|
|
5229
|
+
isTemplateRouteCandidate(file);
|
|
5230
|
+
const unmappedFiles = [
|
|
5231
|
+
...new Set(fileDiffs
|
|
5232
|
+
.filter(({ filePath, oldFilePath }) => !mappedPaths.has(filePath) &&
|
|
5233
|
+
(isSourceFile(filePath) || (oldFilePath !== undefined && isSourceFile(oldFilePath))))
|
|
5234
|
+
.map(({ filePath }) => filePath)),
|
|
5235
|
+
];
|
|
5236
|
+
if (unmappedFiles.length > 0)
|
|
5237
|
+
queryDegraded = true;
|
|
5217
5238
|
// Find affected processes -- batched queries instead of N+1
|
|
5218
5239
|
const affectedProcesses = new Map();
|
|
5219
5240
|
if (changedSymbols.size > 0) {
|
|
@@ -5305,8 +5326,9 @@ export class LocalBackend {
|
|
|
5305
5326
|
},
|
|
5306
5327
|
changed_symbols: listedSymbols,
|
|
5307
5328
|
affected_processes: Array.from(affectedProcesses.values()),
|
|
5308
|
-
|
|
5309
|
-
//
|
|
5329
|
+
...(unmappedFiles.length > 0 && { unmapped_files: unmappedFiles }),
|
|
5330
|
+
// Failed queries or unmapped source files leave counts/risk incomplete;
|
|
5331
|
+
// the safety gate must not treat that as a clean result (#2283).
|
|
5310
5332
|
...(queryDegraded && { partial: true }),
|
|
5311
5333
|
...(listedSymbols.length < changedSymbols.size && { truncated: true }),
|
|
5312
5334
|
};
|
package/dist/mcp/tools.js
CHANGED
|
@@ -349,7 +349,7 @@ AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ
|
|
|
349
349
|
GIT WORKTREE SUPPORT: GitNexus automatically detects when the MCP server was launched from inside a linked git worktree and runs git diff against that worktree — no extra parameters needed in the common case. Pass "worktree" explicitly only when the server was started from a different directory than the worktree you are editing (e.g., the server runs from the canonical root but your changes are in a linked worktree at a different path).
|
|
350
350
|
|
|
351
351
|
Returns: changed symbols, affected processes, and a risk summary.
|
|
352
|
-
- partial: true —
|
|
352
|
+
- partial: true — mapping is incomplete, so risk_level is "unknown" instead of a ranked level. A failed symbol query (or an unparseable diff) degrades changed_symbols, both counts, and derived processes; a failed process lookup degrades only affected_processes and risk, leaving changed-symbol counts sound. unmapped_files lists changed supported source files with no mapped symbols, including source renames without hunks: their symbols may be missing, unindexed, or outside indexed ranges even when every query succeeded. Rebuild the index and inspect those diffs; retry alone may not resolve this state. changed_count:0 with partial:true is NOT a clean pre-commit check.
|
|
353
353
|
- truncated: true — the changed_symbols LISTING was capped for this response. summary.changed_count counts every symbol the run observed: the true total normally, a LOWER BOUND when partial:true. Compare it with the array length rather than trusting the array.`,
|
|
354
354
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
355
355
|
inputSchema: {
|
package/dist/server/api.js
CHANGED
|
@@ -424,7 +424,7 @@ const mapGraphRelationshipRow = (row) => ({
|
|
|
424
424
|
sourceId: row.sourceId,
|
|
425
425
|
targetId: row.targetId,
|
|
426
426
|
confidence: row.confidence,
|
|
427
|
-
reason: row.reason,
|
|
427
|
+
reason: row.reason ?? '',
|
|
428
428
|
step: row.step,
|
|
429
429
|
});
|
|
430
430
|
export const streamGraphNdjson = async (res, includeContent = false, signal) => {
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -285,6 +285,8 @@ export interface DiffHunk {
|
|
|
285
285
|
}
|
|
286
286
|
export interface FileDiff {
|
|
287
287
|
filePath: string;
|
|
288
|
+
/** Decoded pre-rename path; either side can identify a source-file change. */
|
|
289
|
+
oldFilePath?: string;
|
|
288
290
|
hunks: DiffHunk[];
|
|
289
291
|
}
|
|
290
292
|
/** `parseDiffHunks` plus how many `diff --git` headers could not be decoded. */
|
package/dist/storage/git.js
CHANGED
|
@@ -1005,7 +1005,12 @@ export function parseDiffHunksResult(diffOutput) {
|
|
|
1005
1005
|
unparsedGitHeaders++;
|
|
1006
1006
|
}
|
|
1007
1007
|
}
|
|
1008
|
-
else if (line.startsWith('rename
|
|
1008
|
+
else if (!inHunk && line.startsWith('rename from ')) {
|
|
1009
|
+
const oldFilePath = decodeGitPathToken(line.slice('rename from '.length));
|
|
1010
|
+
if (current && oldFilePath)
|
|
1011
|
+
current.oldFilePath = oldFilePath;
|
|
1012
|
+
}
|
|
1013
|
+
else if (!inHunk && line.startsWith('rename to ')) {
|
|
1009
1014
|
const filePath = pathFromRenameTo(line);
|
|
1010
1015
|
if (!filePath)
|
|
1011
1016
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.13-rc.
|
|
3
|
+
"version": "1.6.13-rc.63",
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"version": "node scripts/sync-plugin-manifests.mjs"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@ladybugdb/core": "0.
|
|
68
|
+
"@ladybugdb/core": "0.21.1",
|
|
69
69
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
70
70
|
"@scarf/scarf": "^1.4.0",
|
|
71
71
|
"busboy": "^1.6.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"coreVersion": "0.
|
|
3
|
-
"extensionVersion": "0.
|
|
2
|
+
"coreVersion": "0.21.1",
|
|
3
|
+
"extensionVersion": "0.21.0",
|
|
4
4
|
"filename": "libfts.lbug_extension",
|
|
5
5
|
"officialRepo": "https://extension.ladybugdb.com/",
|
|
6
6
|
"urlTemplate": "{officialRepo}v{extensionVersion}/{upstreamPlatform}/fts/{filename}",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
248332de781a7781f612a55b5397e88f58f1f1ba2a973836ada199eab0f6bf10 ./darwin-arm64/libfts.lbug_extension
|
|
2
|
+
742f2756c2f80bcd1886b6ee0446483038cff0ecf6cf47f698bc6fe855cbaef6 ./linux-x64/libfts.lbug_extension
|
|
3
|
+
7b6132ae6a554e3eeb3c4b0da7e2eb5c483b258b31e848a0e3d6000ac29885ee ./win32-x64/libfts.lbug_extension
|
|
4
|
+
b8675b086664e3d6742330a517c5b619f0ac10dd094d250f12bef9822a8d5750 ./linux-arm64/libfts.lbug_extension
|
|
5
|
+
c86f427503555a3ccdc55eb66a8d82793a206aac599a6092f7d1d4eb50c21403 ./darwin-x64/libfts.lbug_extension
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|