gitnexus 1.6.9-rc.8 → 1.6.9-rc.9
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/core/group/bridge-db.js +24 -6
- package/dist/core/group/cross-impact.d.ts +34 -1
- package/dist/core/group/cross-impact.js +34 -10
- package/dist/core/group/cross-trace.d.ts +135 -0
- package/dist/core/group/cross-trace.js +930 -0
- package/dist/core/group/extractors/http-patterns/go.js +3 -0
- package/dist/core/group/extractors/http-patterns/java.js +8 -0
- package/dist/core/group/extractors/http-patterns/kotlin.js +6 -0
- package/dist/core/group/extractors/http-patterns/node.js +16 -2
- package/dist/core/group/extractors/http-patterns/php.js +3 -0
- package/dist/core/group/extractors/http-patterns/python.js +7 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +9 -0
- package/dist/core/group/extractors/http-route-extractor.js +186 -92
- package/dist/core/group/service.d.ts +56 -0
- package/dist/core/group/service.js +4 -0
- package/dist/core/group/types.d.ts +7 -0
- package/dist/mcp/local/local-backend.d.ts +24 -0
- package/dist/mcp/local/local-backend.js +166 -2
- package/dist/mcp/tools.js +29 -3
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +4 -0
|
@@ -171,11 +171,19 @@ export async function closeBridgeDb(handle) {
|
|
|
171
171
|
// pending on disk, which makes a subsequent read-side open either race
|
|
172
172
|
// with the WAL replay or trip the database-id check on the sidecars.
|
|
173
173
|
// CHECKPOINT is a no-op when there's nothing pending, so it's cheap.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
//
|
|
175
|
+
// ONLY on a writable handle. A read-only connection has nothing to flush,
|
|
176
|
+
// and issuing CHECKPOINT on it leaves a WAL/shadow lock artifact that makes
|
|
177
|
+
// the very next read-only open of the same path fail in-process — which broke
|
|
178
|
+
// repeated `@group` impact/trace calls in a long-lived MCP server (the read
|
|
179
|
+
// path opens read-only, queries, and closes per call).
|
|
180
|
+
if (!handle._readOnly) {
|
|
181
|
+
try {
|
|
182
|
+
await handle._conn.query('CHECKPOINT');
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
/* ignore — older LadybugDB or schemaless DB may not accept it */
|
|
186
|
+
}
|
|
179
187
|
}
|
|
180
188
|
try {
|
|
181
189
|
await handle._conn.close();
|
|
@@ -189,6 +197,16 @@ export async function closeBridgeDb(handle) {
|
|
|
189
197
|
catch {
|
|
190
198
|
/* ignore */
|
|
191
199
|
}
|
|
200
|
+
// NOTE: Windows in-process write→read reopen of the SAME bridge.lbug is still a
|
|
201
|
+
// known limitation (the writable close's OS file handle is not released before
|
|
202
|
+
// the read open races; the existing open-side LBUG_OPEN_RETRY only retries
|
|
203
|
+
// lock-pattern errors, not the post-rename sidecar database-id mismatch). The
|
|
204
|
+
// bridge's close-then-reopen tests stay Windows-skipped. A close-side
|
|
205
|
+
// waitForWindowsHandleRelease + finalizeLbugSidecarsAfterClose probe (mirroring
|
|
206
|
+
// safeClose) was tried and did NOT close that gap on Windows CI, so it was
|
|
207
|
+
// removed rather than carry latency/duplication for no Windows benefit. The
|
|
208
|
+
// read-only CHECKPOINT skip above is the load-bearing fix and works on
|
|
209
|
+
// Linux/macOS (the platforms where in-process reopen is supported).
|
|
192
210
|
}
|
|
193
211
|
/* ------------------------------------------------------------------ */
|
|
194
212
|
/* retryRename — handles transient EBUSY/EPERM/EACCES on Windows */
|
|
@@ -577,7 +595,7 @@ export async function openBridgeDbReadOnly(groupDir) {
|
|
|
577
595
|
// (where we can retry) instead of on the first user query.
|
|
578
596
|
await handle.db.init();
|
|
579
597
|
await handle.conn.init();
|
|
580
|
-
return { _db: handle.db, _conn: handle.conn, groupDir };
|
|
598
|
+
return { _db: handle.db, _conn: handle.conn, groupDir, _readOnly: true };
|
|
581
599
|
}
|
|
582
600
|
catch (err) {
|
|
583
601
|
lastErr = err;
|
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
* Cross-repo impact (Phase 1 local walk + Phase 2 bridge fan-out).
|
|
3
3
|
* All bridge Cypher for this feature lives in this module.
|
|
4
4
|
*/
|
|
5
|
-
import type { CrossRepoImpact, GroupImpactResult } from './types.js';
|
|
5
|
+
import type { BridgeHandle, CrossRepoImpact, GroupImpactResult } from './types.js';
|
|
6
6
|
import type { GroupToolPort } from './service.js';
|
|
7
7
|
/** Cross-boundary hops beyond this value are clamped (multi-hop reserved for future work). */
|
|
8
8
|
export declare const MAX_SUPPORTED_CROSS_DEPTH = 1;
|
|
9
9
|
/** Default wall-clock budget for the Phase 1 `impact` leg when callers omit `timeoutMs`. */
|
|
10
10
|
export declare const DEFAULT_LOCAL_IMPACT_TIMEOUT_MS = 30000;
|
|
11
|
+
export type BridgeNeighborRow = {
|
|
12
|
+
neighborRepo: string;
|
|
13
|
+
neighborUid: string;
|
|
14
|
+
neighborFilePath?: string;
|
|
15
|
+
matchType: string;
|
|
16
|
+
confidence: number;
|
|
17
|
+
contractId: string;
|
|
18
|
+
contractType: string;
|
|
19
|
+
};
|
|
11
20
|
export interface RunGroupImpactDeps {
|
|
12
21
|
port: GroupToolPort;
|
|
13
22
|
gitnexusDir: string;
|
|
@@ -80,6 +89,30 @@ export declare function collectImpactSymbolUids(local: unknown, servicePrefix: s
|
|
|
80
89
|
targetFilePath?: string;
|
|
81
90
|
};
|
|
82
91
|
export declare function mergeRisk(localRisk: string, cross: CrossRepoImpact[]): string;
|
|
92
|
+
export declare function ensureBridgeReady(groupDir: string): Promise<{
|
|
93
|
+
handle: BridgeHandle;
|
|
94
|
+
} | {
|
|
95
|
+
error: string;
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Resolve cross-repo neighbors over `ContractLink` for a set of local symbol
|
|
99
|
+
* UIDs, in a single direction, sorted by descending confidence.
|
|
100
|
+
*
|
|
101
|
+
* This is the one shared consumer↔provider bridge join. `runGroupImpact`'s
|
|
102
|
+
* Phase-2 fan-out uses it directly; the cross-repo trace path (`cross-trace.ts`)
|
|
103
|
+
* reuses the same `queryBridge` + row-normalization primitives but issues a
|
|
104
|
+
* distinct *pair* query, because a trace must keep BOTH endpoints of a crossing
|
|
105
|
+
* (this neighbor join intentionally returns only the far side, which is lossy
|
|
106
|
+
* for stitching a path). Keeping this helper as the single uid-filtered join
|
|
107
|
+
* means impact never forks its own copy of the neighbor Cypher.
|
|
108
|
+
*
|
|
109
|
+
* Returns `[]` for an empty `uids` set without touching the DB.
|
|
110
|
+
*/
|
|
111
|
+
export declare function resolveBridgeNeighbors(handle: BridgeHandle, opts: {
|
|
112
|
+
localRepo: string;
|
|
113
|
+
uids: string[];
|
|
114
|
+
direction: 'upstream' | 'downstream';
|
|
115
|
+
}): Promise<BridgeNeighborRow[]>;
|
|
83
116
|
export declare function runGroupImpact(deps: RunGroupImpactDeps, params: Record<string, unknown>): Promise<GroupImpactResult | {
|
|
84
117
|
error: string;
|
|
85
118
|
}>;
|
|
@@ -267,7 +267,7 @@ export function mergeRisk(localRisk, cross) {
|
|
|
267
267
|
return 'MEDIUM';
|
|
268
268
|
return localRisk;
|
|
269
269
|
}
|
|
270
|
-
async function ensureBridgeReady(groupDir) {
|
|
270
|
+
export async function ensureBridgeReady(groupDir) {
|
|
271
271
|
const meta = await readBridgeMeta(groupDir);
|
|
272
272
|
if (meta.version > 0 && meta.version !== BRIDGE_SCHEMA_VERSION) {
|
|
273
273
|
return {
|
|
@@ -306,6 +306,37 @@ function rowToNeighbor(r) {
|
|
|
306
306
|
contractType: String(r.contractType ?? r[6] ?? 'custom'),
|
|
307
307
|
};
|
|
308
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Resolve cross-repo neighbors over `ContractLink` for a set of local symbol
|
|
311
|
+
* UIDs, in a single direction, sorted by descending confidence.
|
|
312
|
+
*
|
|
313
|
+
* This is the one shared consumer↔provider bridge join. `runGroupImpact`'s
|
|
314
|
+
* Phase-2 fan-out uses it directly; the cross-repo trace path (`cross-trace.ts`)
|
|
315
|
+
* reuses the same `queryBridge` + row-normalization primitives but issues a
|
|
316
|
+
* distinct *pair* query, because a trace must keep BOTH endpoints of a crossing
|
|
317
|
+
* (this neighbor join intentionally returns only the far side, which is lossy
|
|
318
|
+
* for stitching a path). Keeping this helper as the single uid-filtered join
|
|
319
|
+
* means impact never forks its own copy of the neighbor Cypher.
|
|
320
|
+
*
|
|
321
|
+
* Returns `[]` for an empty `uids` set without touching the DB.
|
|
322
|
+
*/
|
|
323
|
+
export async function resolveBridgeNeighbors(handle, opts) {
|
|
324
|
+
if (opts.uids.length === 0)
|
|
325
|
+
return [];
|
|
326
|
+
const cypher = opts.direction === 'upstream' ? CY_NEIGHBORS_UPSTREAM : CY_NEIGHBORS_DOWNSTREAM;
|
|
327
|
+
const rows = await queryBridge(handle, cypher, {
|
|
328
|
+
localRepo: opts.localRepo,
|
|
329
|
+
uids: opts.uids,
|
|
330
|
+
});
|
|
331
|
+
const neighbors = [];
|
|
332
|
+
for (const raw of rows) {
|
|
333
|
+
const n = rowToNeighbor(raw);
|
|
334
|
+
if (n)
|
|
335
|
+
neighbors.push(n);
|
|
336
|
+
}
|
|
337
|
+
neighbors.sort((a, b) => b.confidence - a.confidence);
|
|
338
|
+
return neighbors;
|
|
339
|
+
}
|
|
309
340
|
export async function runGroupImpact(deps, params) {
|
|
310
341
|
const parsed = validateGroupImpactParams(params);
|
|
311
342
|
if (parsed.ok === false)
|
|
@@ -418,18 +449,11 @@ export async function runGroupImpact(deps, params) {
|
|
|
418
449
|
const outOfScope = [];
|
|
419
450
|
const truncatedRepos = [];
|
|
420
451
|
try {
|
|
421
|
-
const
|
|
422
|
-
const rows = await queryBridge(handle, cypher, {
|
|
452
|
+
const neighbors = await resolveBridgeNeighbors(handle, {
|
|
423
453
|
localRepo: repoPath,
|
|
424
454
|
uids,
|
|
455
|
+
direction,
|
|
425
456
|
});
|
|
426
|
-
const neighbors = [];
|
|
427
|
-
for (const raw of rows) {
|
|
428
|
-
const n = rowToNeighbor(raw);
|
|
429
|
-
if (n)
|
|
430
|
-
neighbors.push(n);
|
|
431
|
-
}
|
|
432
|
-
neighbors.sort((a, b) => b.confidence - a.confidence);
|
|
433
457
|
const seen = new Set();
|
|
434
458
|
for (const n of neighbors) {
|
|
435
459
|
if (servicePrefix && !fileMatchesServicePrefix(n.neighborFilePath, servicePrefix)) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-repo call trace.
|
|
3
|
+
*
|
|
4
|
+
* Stitches per-repo directed-path segments (CALLS + HAS_METHOD, via the
|
|
5
|
+
* existing single-repo `trace`) across a single `ContractLink` boundary in the
|
|
6
|
+
* group bridge, and — when a `--pdg` layer is present and the caller opts in —
|
|
7
|
+
* enriches the boundary-adjacent segments with their intra-procedural
|
|
8
|
+
* REACHING_DEF data-flow.
|
|
9
|
+
*
|
|
10
|
+
* Design notes:
|
|
11
|
+
* - The crossing is a single bridge hop joined on `symbolUid` (the symbol node
|
|
12
|
+
* id is the same value the bridge stores as `Contract.symbolUid`). This
|
|
13
|
+
* mirrors `cross-impact.ts` and is clamped to one boundary
|
|
14
|
+
* (`MAX_SUPPORTED_CROSS_DEPTH`); multi-hop is deferred.
|
|
15
|
+
* - All trace-specific bridge Cypher lives in THIS module (mirroring the
|
|
16
|
+
* "all bridge Cypher for this feature lives here" convention in
|
|
17
|
+
* cross-impact.ts). The trace needs BOTH endpoints of a crossing to stitch a
|
|
18
|
+
* path, so it issues its own pair query rather than the lossy uid-filtered
|
|
19
|
+
* neighbor join exported by cross-impact (`resolveBridgeNeighbors`), which
|
|
20
|
+
* intentionally returns only the far side.
|
|
21
|
+
* - PDG is enrichment only: data flow never crosses the repo boundary. Full
|
|
22
|
+
* cross-program (SDG-like) data flow is deferred — see
|
|
23
|
+
* docs/plans/2026-06-18-002-feat-unified-pdg-impact-evaluation-plan.md.
|
|
24
|
+
*/
|
|
25
|
+
import type { GroupPdgFlowHop, GroupToolPort } from './service.js';
|
|
26
|
+
export interface TraceHop {
|
|
27
|
+
name: string;
|
|
28
|
+
filePath: string;
|
|
29
|
+
startLine: number;
|
|
30
|
+
/** The member repo path (group.yaml key) this hop belongs to. */
|
|
31
|
+
repo: string;
|
|
32
|
+
}
|
|
33
|
+
export interface TraceEdge {
|
|
34
|
+
relType: string;
|
|
35
|
+
confidence: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SegmentDataFlow {
|
|
38
|
+
/** Member repo path of the enriched segment. */
|
|
39
|
+
repo: string;
|
|
40
|
+
/** The boundary-adjacent symbol the flow was anchored on. */
|
|
41
|
+
anchor: string;
|
|
42
|
+
variable?: string;
|
|
43
|
+
hops: GroupPdgFlowHop[];
|
|
44
|
+
truncated?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface BridgeCrossing {
|
|
47
|
+
fromRepo: string;
|
|
48
|
+
toRepo: string;
|
|
49
|
+
contractId: string;
|
|
50
|
+
contractType: string;
|
|
51
|
+
matchType: string;
|
|
52
|
+
confidence: number;
|
|
53
|
+
}
|
|
54
|
+
export interface GroupTraceEndpoint {
|
|
55
|
+
name: string;
|
|
56
|
+
filePath: string;
|
|
57
|
+
startLine: number;
|
|
58
|
+
repo: string;
|
|
59
|
+
}
|
|
60
|
+
export interface GroupTraceOkResult {
|
|
61
|
+
status: 'ok';
|
|
62
|
+
group: string;
|
|
63
|
+
from: GroupTraceEndpoint;
|
|
64
|
+
to: GroupTraceEndpoint;
|
|
65
|
+
/** 0 for a same-repo trace, 1 for a single boundary crossing. */
|
|
66
|
+
crossings: BridgeCrossing[];
|
|
67
|
+
hopCount: number;
|
|
68
|
+
hops: TraceHop[];
|
|
69
|
+
edges: TraceEdge[];
|
|
70
|
+
/** Present only when PDG enrichment ran for at least one segment. */
|
|
71
|
+
dataFlow?: SegmentDataFlow[];
|
|
72
|
+
truncated?: boolean;
|
|
73
|
+
notes: string[];
|
|
74
|
+
}
|
|
75
|
+
export interface GroupTraceCandidate {
|
|
76
|
+
repo: string;
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
filePath: string;
|
|
80
|
+
startLine: number;
|
|
81
|
+
}
|
|
82
|
+
export interface GroupTraceNotFoundResult {
|
|
83
|
+
status: 'not_found';
|
|
84
|
+
group: string;
|
|
85
|
+
role?: 'from' | 'to';
|
|
86
|
+
query?: string;
|
|
87
|
+
/**
|
|
88
|
+
* True when the answer is NOT authoritative: the crossing cap
|
|
89
|
+
* (`MAX_CROSSINGS_TO_TRY`) was hit, so a connecting ContractLink ranked beyond
|
|
90
|
+
* the cap may have been skipped. A consumer should treat this as "unknown",
|
|
91
|
+
* not "no path exists".
|
|
92
|
+
*/
|
|
93
|
+
truncated?: boolean;
|
|
94
|
+
notes: string[];
|
|
95
|
+
suggestion?: string;
|
|
96
|
+
}
|
|
97
|
+
export interface GroupTraceAmbiguousResult {
|
|
98
|
+
status: 'ambiguous';
|
|
99
|
+
group: string;
|
|
100
|
+
role: 'from' | 'to';
|
|
101
|
+
candidates: GroupTraceCandidate[];
|
|
102
|
+
notes: string[];
|
|
103
|
+
}
|
|
104
|
+
export interface GroupTraceErrorResult {
|
|
105
|
+
status: 'error';
|
|
106
|
+
group: string;
|
|
107
|
+
error: string;
|
|
108
|
+
notes: string[];
|
|
109
|
+
}
|
|
110
|
+
export type GroupTraceResult = GroupTraceOkResult | GroupTraceNotFoundResult | GroupTraceAmbiguousResult | GroupTraceErrorResult;
|
|
111
|
+
/**
|
|
112
|
+
* Centralized degraded-state messages so wording stays consistent and
|
|
113
|
+
* testable. Kept as named constants/builders rather than inline strings.
|
|
114
|
+
*/
|
|
115
|
+
export declare const TRACE_NOTES: {
|
|
116
|
+
readonly noBridgeLink: string;
|
|
117
|
+
readonly crossDepthClamped: string;
|
|
118
|
+
readonly noLocalPath: (repo: string) => string;
|
|
119
|
+
readonly noPdgLayer: (repo: string) => string;
|
|
120
|
+
readonly pdgRequested: string;
|
|
121
|
+
readonly pdgSameRepoNoop: string;
|
|
122
|
+
readonly crossingsCapped: (cap: number) => string;
|
|
123
|
+
readonly degradedMembers: (repos: string[]) => string;
|
|
124
|
+
readonly fileBoundaryFallback: string;
|
|
125
|
+
readonly anonymousHandler: (route: string, location: string) => string;
|
|
126
|
+
readonly destinationNoLink: string;
|
|
127
|
+
readonly destinationNoReach: string;
|
|
128
|
+
readonly destinationMultiple: string;
|
|
129
|
+
readonly destinationAmbiguousFile: string;
|
|
130
|
+
};
|
|
131
|
+
export interface RunGroupTraceDeps {
|
|
132
|
+
port: GroupToolPort;
|
|
133
|
+
gitnexusDir: string;
|
|
134
|
+
}
|
|
135
|
+
export declare function runGroupTrace(deps: RunGroupTraceDeps, params: Record<string, unknown>): Promise<GroupTraceResult>;
|