gitnexus 1.6.11-rc.21 → 1.6.11-rc.22
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/cross-impact.d.ts +3 -0
- package/dist/core/group/cross-impact.js +26 -4
- package/dist/core/group/extractors/http-patterns/java.js +18 -4
- package/dist/core/group/extractors/http-patterns/kotlin.js +2 -0
- package/dist/core/group/normalization.d.ts +38 -1
- package/dist/core/group/normalization.js +59 -3
- package/dist/core/group/service.d.ts +11 -0
- package/dist/core/group/service.js +8 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +15 -3
- package/dist/core/group/types.d.ts +13 -0
- package/dist/core/ingestion/language-provider.d.ts +15 -0
- package/dist/core/ingestion/language-provider.js +22 -0
- package/dist/core/ingestion/languages/java.js +6 -7
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +6 -1
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +16 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +10 -0
- package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +27 -1
- package/dist/core/ingestion/route-extractors/java-const-resolver.js +161 -13
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.d.ts +7 -8
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.js +209 -23
- package/dist/core/ingestion/workers/parse-worker.js +4 -1
- package/dist/mcp/local/local-backend.js +11 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/storage/parse-cache.js +8 -1
- package/package.json +1 -1
|
@@ -62,6 +62,9 @@ export declare function validateGroupImpactParams(params: Record<string, unknown
|
|
|
62
62
|
name: string;
|
|
63
63
|
repoPath: string;
|
|
64
64
|
target: string;
|
|
65
|
+
target_uid?: string;
|
|
66
|
+
file_path?: string;
|
|
67
|
+
kind?: string;
|
|
65
68
|
direction: 'upstream' | 'downstream';
|
|
66
69
|
maxDepth: number;
|
|
67
70
|
crossDepth: number;
|
|
@@ -103,13 +103,21 @@ export function clampTimeout(timeoutMs) {
|
|
|
103
103
|
export function validateGroupImpactParams(params) {
|
|
104
104
|
const name = String(params.name ?? '').trim();
|
|
105
105
|
const repoPath = String(params.repo ?? '').trim();
|
|
106
|
-
|
|
106
|
+
// Optional string, same helper shape as cross-trace's `str()`: empty/blank
|
|
107
|
+
// counts as absent so `target_uid: ''` degrades to the name lookup rather
|
|
108
|
+
// than a zero-ambiguity lookup of the empty uid. Parsed before the required
|
|
109
|
+
// check so UID-only callers (MCP impact schema requires `direction`, not
|
|
110
|
+
// `target`) are accepted.
|
|
111
|
+
const str = (v) => typeof v === 'string' && v.trim() !== '' ? v : undefined;
|
|
112
|
+
const targetName = String(params.target ?? '').trim();
|
|
113
|
+
const target_uidEarly = str(params.target_uid);
|
|
107
114
|
if (!name)
|
|
108
115
|
return { ok: false, error: 'name is required' };
|
|
109
116
|
if (!repoPath)
|
|
110
117
|
return { ok: false, error: 'repo is required (group repo path, e.g. app/backend)' };
|
|
111
|
-
if (!
|
|
112
|
-
return { ok: false, error: 'target is required' };
|
|
118
|
+
if (!targetName && !target_uidEarly)
|
|
119
|
+
return { ok: false, error: 'target or target_uid is required' };
|
|
120
|
+
const target = targetName || target_uidEarly;
|
|
113
121
|
if (params.service !== undefined &&
|
|
114
122
|
params.service !== null &&
|
|
115
123
|
String(params.service).trim() === '') {
|
|
@@ -133,6 +141,9 @@ export function validateGroupImpactParams(params) {
|
|
|
133
141
|
minConfidence = 1;
|
|
134
142
|
const service = normalizeServicePrefix(params.service);
|
|
135
143
|
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
144
|
+
const target_uid = target_uidEarly;
|
|
145
|
+
const file_path = str(params.file_path);
|
|
146
|
+
const kind = str(params.kind);
|
|
136
147
|
// Clamp at the validate boundary so the downstream `deadline` (line
|
|
137
148
|
// ~366) and `safeLocalImpact`'s `setTimeout` both see a single
|
|
138
149
|
// bounded value. Without this, the outer deadline budgeted Phase-2
|
|
@@ -150,6 +161,9 @@ export function validateGroupImpactParams(params) {
|
|
|
150
161
|
name,
|
|
151
162
|
repoPath,
|
|
152
163
|
target,
|
|
164
|
+
target_uid,
|
|
165
|
+
file_path,
|
|
166
|
+
kind,
|
|
153
167
|
direction,
|
|
154
168
|
maxDepth,
|
|
155
169
|
crossDepth,
|
|
@@ -448,7 +462,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
448
462
|
const parsed = validateGroupImpactParams(params);
|
|
449
463
|
if (parsed.ok === false)
|
|
450
464
|
return { error: parsed.error };
|
|
451
|
-
const { name, repoPath, target, direction, maxDepth, crossDepth: _crossDepth, crossDepthWarning, relationTypes, includeTests, minConfidence, service: servicePrefix, subgroup, timeoutMs, } = parsed;
|
|
465
|
+
const { name, repoPath, target, target_uid, file_path, kind, direction, maxDepth, crossDepth: _crossDepth, crossDepthWarning, relationTypes, includeTests, minConfidence, service: servicePrefix, subgroup, timeoutMs, } = parsed;
|
|
452
466
|
const groupDir = getGroupDir(deps.gitnexusDir, name);
|
|
453
467
|
let config;
|
|
454
468
|
try {
|
|
@@ -464,6 +478,14 @@ export async function runGroupImpact(deps, params) {
|
|
|
464
478
|
return { error: resolved.error };
|
|
465
479
|
const impactParams = {
|
|
466
480
|
target,
|
|
481
|
+
// Selector params pass through to the member repo's impact (the port
|
|
482
|
+
// contract in service.ts documents them), so the single-repo tool's
|
|
483
|
+
// "re-call with target_uid to disambiguate" loop works unchanged in
|
|
484
|
+
// group mode. `undefined` keeps the call shape flat — same convention
|
|
485
|
+
// as the relationTypes line below.
|
|
486
|
+
target_uid,
|
|
487
|
+
file_path,
|
|
488
|
+
kind,
|
|
467
489
|
direction,
|
|
468
490
|
maxDepth,
|
|
469
491
|
relationTypes: relationTypes && relationTypes.length > 0 ? relationTypes : undefined,
|
|
@@ -2,7 +2,7 @@ import Java from 'tree-sitter-java';
|
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
3
|
import { springAnnotationHttpMethods, intersectSpringHttpMethods, isRouteMemberKey, findEnclosingClass, joinPath, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
4
|
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
|
-
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, parseJavaConstOperands, } from '../../../ingestion/route-extractors/java-const-resolver.js';
|
|
5
|
+
import { expandJavaWildcardStaticImports, extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, parseJavaConstOperands, prepareJavaRouteConstants, } from '../../../ingestion/route-extractors/java-const-resolver.js';
|
|
6
6
|
import { extractStaticPathExpression, inferOkHttpMethod, inferHttpClientMethod, okHttpUrlRootsAtBuilder, httpClientUriRootsAtNewBuilder, httpClientChainHasUriCall, } from './java-static-path.js';
|
|
7
7
|
/**
|
|
8
8
|
* Java HTTP plugin. Handles:
|
|
@@ -789,7 +789,10 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
789
789
|
if (!tree)
|
|
790
790
|
continue;
|
|
791
791
|
const mc = extractJavaModuleConstants(tree);
|
|
792
|
-
if (mc.literals.size > 0 ||
|
|
792
|
+
if (mc.literals.size > 0 ||
|
|
793
|
+
mc.exprs.size > 0 ||
|
|
794
|
+
mc.imports.size > 0 ||
|
|
795
|
+
(mc.wildcardImports?.length ?? 0) > 0) {
|
|
793
796
|
constants.set(rel, mc);
|
|
794
797
|
}
|
|
795
798
|
}
|
|
@@ -800,7 +803,14 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
800
803
|
continue;
|
|
801
804
|
}
|
|
802
805
|
}
|
|
803
|
-
|
|
806
|
+
// On-demand static imports (`import static a.b.C.*`) were recorded as
|
|
807
|
+
// pending class FQNs during extraction; materialize their bare-name
|
|
808
|
+
// bindings now that the whole map exists. A wildcard's target is itself
|
|
809
|
+
// a constants file, so it is necessarily a map entry — anything else
|
|
810
|
+
// degrades to the fold's skip floor. In-place: each entry is owned by
|
|
811
|
+
// this map, and every file is expanded exactly once.
|
|
812
|
+
const constantIndex = prepareJavaRouteConstants(constants);
|
|
813
|
+
return { constants, constantIndex };
|
|
804
814
|
},
|
|
805
815
|
scan(tree, repoContext, fileRel) {
|
|
806
816
|
const out = [];
|
|
@@ -832,8 +842,12 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
832
842
|
return foldConstants;
|
|
833
843
|
try {
|
|
834
844
|
const mc = extractJavaModuleConstants(tree);
|
|
835
|
-
|
|
845
|
+
// A file carrying ONLY wildcard static imports has an empty import
|
|
846
|
+
// table pre-expansion — overlay it too, then materialize the promised
|
|
847
|
+
// bindings against the repo map before it becomes a fold target.
|
|
848
|
+
if (mc.imports.size > 0 || (mc.wildcardImports?.length ?? 0) > 0) {
|
|
836
849
|
const merged = new Map(javaCtx.constants);
|
|
850
|
+
expandJavaWildcardStaticImports(mc, fileRel, merged, javaCtx.constantIndex);
|
|
837
851
|
merged.set(fileRel, mc);
|
|
838
852
|
foldConstants = merged;
|
|
839
853
|
}
|
|
@@ -1260,6 +1260,7 @@ function buildKotlinPlugin(language) {
|
|
|
1260
1260
|
if (mc.literals.size > 0 ||
|
|
1261
1261
|
mc.exprs.size > 0 ||
|
|
1262
1262
|
mc.imports.size > 0 ||
|
|
1263
|
+
(mc.wildcardImports?.length ?? 0) > 0 ||
|
|
1263
1264
|
unfoldableDeclarationsOf(mc).size > 0) {
|
|
1264
1265
|
// POSIX key (see `normalizeRel`); `readFile` above got the raw `rel`.
|
|
1265
1266
|
constants.set(normalizeRel(rel), mc);
|
|
@@ -1303,6 +1304,7 @@ function buildKotlinPlugin(language) {
|
|
|
1303
1304
|
if (mc.literals.size > 0 ||
|
|
1304
1305
|
mc.exprs.size > 0 ||
|
|
1305
1306
|
mc.imports.size > 0 ||
|
|
1307
|
+
(mc.wildcardImports?.length ?? 0) > 0 ||
|
|
1306
1308
|
unfoldableDeclarationsOf(mc).size > 0) {
|
|
1307
1309
|
foldIndex = overlayKotlinConstantIndex(kotlinCtx.index, fileKey, mc);
|
|
1308
1310
|
}
|
|
@@ -1,3 +1,40 @@
|
|
|
1
|
-
import type { CrossLink, StoredContract } from './types.js';
|
|
1
|
+
import type { CrossLink, CrossLinkEndpoint, StoredContract } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* True when a link endpoint carries no resolved graph symbol — empty
|
|
4
|
+
* `symbolUid` or a missing/empty `symbolRef`.
|
|
5
|
+
*
|
|
6
|
+
* Sync marks a cross-link `degraded: true` when this holds for the PROVIDER
|
|
7
|
+
* endpoint (`to`): the contract boundary is proven, but the empty uid can
|
|
8
|
+
* never match a Phase-1 impact symbol id, so cross-repo fan-out across the
|
|
9
|
+
* link silently yields nothing (the classic case is a provider whose handler
|
|
10
|
+
* failed to resolve, leaving `symbolName` degraded to the file name with one
|
|
11
|
+
* pseudo-symbol carrying every route in that file). Consumer-side (`from`)
|
|
12
|
+
* emptiness is deliberately NOT degraded — several extractors (topics, grpc)
|
|
13
|
+
* legitimately emit consumer contracts without a per-call symbol, and the
|
|
14
|
+
* anchor that matters for far-side fan-out is the provider's.
|
|
15
|
+
*
|
|
16
|
+
* Kept next to the endpoint merge logic because `dedupeCrossLinks` must
|
|
17
|
+
* re-derive the flag after a merge: `mergeEndpoints` backfills `symbolUid`
|
|
18
|
+
* from the losing twin, which can invalidate a flag carried in from the winner.
|
|
19
|
+
*
|
|
20
|
+
* NOT unresolved: a deterministic `manifest::<repo>::<contractId>` synthetic
|
|
21
|
+
* uid (see `manifestSymbolUid`). Manifest endpoints fall back to it precisely
|
|
22
|
+
* when the graph has no symbol for them — its empty `symbolRef.filePath` would
|
|
23
|
+
* otherwise trip the check below — yet cross-impact anchors those links by
|
|
24
|
+
* design (#2722: the crossing is preserved with `fanout_status:
|
|
25
|
+
* 'not_attempted'` instead of silently yielding cross=0). The prefix is the
|
|
26
|
+
* canonical discriminator — real indexer uids never start with `manifest::`
|
|
27
|
+
* — and `cross-impact.ts` branches on the same test. Encoding the exemption
|
|
28
|
+
* HERE (not at the sync marking call site) keeps marking and the post-merge
|
|
29
|
+
* re-derivation from drifting apart, and keeps the flag's meaning exactly what
|
|
30
|
+
* `types.ts` documents: "distinct from manifest::… synthetic UIDs".
|
|
31
|
+
*/
|
|
32
|
+
export declare function isUnresolvedEndpoint(endpoint: CrossLinkEndpoint): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Derive `degraded` from the provider endpoint. Present (`true`) only when
|
|
35
|
+
* unresolved; deleted otherwise so contracts.json stays "carried only when
|
|
36
|
+
* meaningful" (`'degraded' in link === false` for anchored links).
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyDegradedFlag(link: CrossLink): CrossLink;
|
|
2
39
|
export declare function dedupeContracts(items: StoredContract[]): StoredContract[];
|
|
3
40
|
export declare function dedupeCrossLinks(items: CrossLink[]): CrossLink[];
|
|
@@ -83,6 +83,59 @@ function crossLinkKey(link) {
|
|
|
83
83
|
endpointKey(link.to),
|
|
84
84
|
].join('\0');
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* True when a link endpoint carries no resolved graph symbol — empty
|
|
88
|
+
* `symbolUid` or a missing/empty `symbolRef`.
|
|
89
|
+
*
|
|
90
|
+
* Sync marks a cross-link `degraded: true` when this holds for the PROVIDER
|
|
91
|
+
* endpoint (`to`): the contract boundary is proven, but the empty uid can
|
|
92
|
+
* never match a Phase-1 impact symbol id, so cross-repo fan-out across the
|
|
93
|
+
* link silently yields nothing (the classic case is a provider whose handler
|
|
94
|
+
* failed to resolve, leaving `symbolName` degraded to the file name with one
|
|
95
|
+
* pseudo-symbol carrying every route in that file). Consumer-side (`from`)
|
|
96
|
+
* emptiness is deliberately NOT degraded — several extractors (topics, grpc)
|
|
97
|
+
* legitimately emit consumer contracts without a per-call symbol, and the
|
|
98
|
+
* anchor that matters for far-side fan-out is the provider's.
|
|
99
|
+
*
|
|
100
|
+
* Kept next to the endpoint merge logic because `dedupeCrossLinks` must
|
|
101
|
+
* re-derive the flag after a merge: `mergeEndpoints` backfills `symbolUid`
|
|
102
|
+
* from the losing twin, which can invalidate a flag carried in from the winner.
|
|
103
|
+
*
|
|
104
|
+
* NOT unresolved: a deterministic `manifest::<repo>::<contractId>` synthetic
|
|
105
|
+
* uid (see `manifestSymbolUid`). Manifest endpoints fall back to it precisely
|
|
106
|
+
* when the graph has no symbol for them — its empty `symbolRef.filePath` would
|
|
107
|
+
* otherwise trip the check below — yet cross-impact anchors those links by
|
|
108
|
+
* design (#2722: the crossing is preserved with `fanout_status:
|
|
109
|
+
* 'not_attempted'` instead of silently yielding cross=0). The prefix is the
|
|
110
|
+
* canonical discriminator — real indexer uids never start with `manifest::`
|
|
111
|
+
* — and `cross-impact.ts` branches on the same test. Encoding the exemption
|
|
112
|
+
* HERE (not at the sync marking call site) keeps marking and the post-merge
|
|
113
|
+
* re-derivation from drifting apart, and keeps the flag's meaning exactly what
|
|
114
|
+
* `types.ts` documents: "distinct from manifest::… synthetic UIDs".
|
|
115
|
+
*/
|
|
116
|
+
export function isUnresolvedEndpoint(endpoint) {
|
|
117
|
+
if (endpoint.symbolUid.startsWith('manifest::'))
|
|
118
|
+
return false;
|
|
119
|
+
return (!endpoint.symbolUid ||
|
|
120
|
+
!endpoint.symbolRef ||
|
|
121
|
+
!endpoint.symbolRef.filePath ||
|
|
122
|
+
!endpoint.symbolRef.name);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Derive `degraded` from the provider endpoint. Present (`true`) only when
|
|
126
|
+
* unresolved; deleted otherwise so contracts.json stays "carried only when
|
|
127
|
+
* meaningful" (`'degraded' in link === false` for anchored links).
|
|
128
|
+
*/
|
|
129
|
+
export function applyDegradedFlag(link) {
|
|
130
|
+
const next = { ...link };
|
|
131
|
+
if (isUnresolvedEndpoint(next.to)) {
|
|
132
|
+
next.degraded = true;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
delete next.degraded;
|
|
136
|
+
}
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
86
139
|
export function dedupeContracts(items) {
|
|
87
140
|
const deduped = new Map();
|
|
88
141
|
for (const contract of items) {
|
|
@@ -104,12 +157,15 @@ export function dedupeCrossLinks(items) {
|
|
|
104
157
|
const keepIncoming = link.confidence > existing.confidence;
|
|
105
158
|
const primary = keepIncoming ? link : existing;
|
|
106
159
|
const secondary = keepIncoming ? existing : link;
|
|
107
|
-
|
|
160
|
+
const merged = {
|
|
108
161
|
...primary,
|
|
109
162
|
confidence: Math.max(existing.confidence, link.confidence),
|
|
110
163
|
from: mergeEndpoints(primary.from, secondary.from),
|
|
111
164
|
to: mergeEndpoints(primary.to, secondary.to),
|
|
112
|
-
}
|
|
165
|
+
};
|
|
166
|
+
// Re-derive after mergeEndpoints: a richer twin can backfill `to.symbolUid`
|
|
167
|
+
// and must not leave a stale `degraded` flag on an now-anchored link.
|
|
168
|
+
deduped.set(key, applyDegradedFlag(merged));
|
|
113
169
|
}
|
|
114
|
-
return [...deduped.values()];
|
|
170
|
+
return [...deduped.values()].map(applyDegradedFlag);
|
|
115
171
|
}
|
|
@@ -15,6 +15,17 @@ export interface GroupToolPort {
|
|
|
15
15
|
resolveRepo(repoParam?: string): Promise<GroupRepoHandle>;
|
|
16
16
|
impact(repo: GroupRepoHandle, params: {
|
|
17
17
|
target: string;
|
|
18
|
+
/**
|
|
19
|
+
* Target-selector params, same semantics as the single-repo `impact`
|
|
20
|
+
* tool: `target_uid` is the zero-ambiguity lookup (it wins over the
|
|
21
|
+
* name), `file_path`/`kind` narrow a name shared by several symbols
|
|
22
|
+
* (e.g. same-named Api/Impl/Controller layers). The port implementation
|
|
23
|
+
* consumes them directly; the Phase-1 caller in cross-impact.ts is
|
|
24
|
+
* responsible for threading them from the MCP `impact` args.
|
|
25
|
+
*/
|
|
26
|
+
target_uid?: string;
|
|
27
|
+
file_path?: string;
|
|
28
|
+
kind?: string;
|
|
18
29
|
direction: 'upstream' | 'downstream';
|
|
19
30
|
maxDepth?: number;
|
|
20
31
|
relationTypes?: string[];
|
|
@@ -341,6 +341,14 @@ export class GroupService {
|
|
|
341
341
|
// can otherwise see contract counts that disagree with this payload, with
|
|
342
342
|
// nothing here explaining why the write was skipped.
|
|
343
343
|
registryOutcome: result.registryOutcome,
|
|
344
|
+
// Data-quality signals surfaced from the sync run: links whose provider
|
|
345
|
+
// endpoint never resolved to a graph symbol, per-repo extraction
|
|
346
|
+
// failures with reasons, and operator warnings (e.g. bridge.lbug write
|
|
347
|
+
// failed after contracts.json was written). Always present so MCP
|
|
348
|
+
// consumers can branch on them without existence checks.
|
|
349
|
+
degradedLinks: result.degradedLinks,
|
|
350
|
+
failedRepos: result.failedRepos,
|
|
351
|
+
warnings: result.warnings,
|
|
344
352
|
};
|
|
345
353
|
}
|
|
346
354
|
async groupContracts(params) {
|
|
@@ -47,6 +47,27 @@ export interface SyncResult {
|
|
|
47
47
|
* none of that repo's contracts are in `contracts`.
|
|
48
48
|
*/
|
|
49
49
|
unreadableRepos: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Cross-links whose provider endpoint has no resolved graph symbol
|
|
52
|
+
* (`degraded: true` on the link — see `isUnresolvedEndpoint`). The boundary
|
|
53
|
+
* is proven but cross-impact fan-out cannot anchor it; the usual remedy is
|
|
54
|
+
* re-analyzing the provider repo so its handlers resolve.
|
|
55
|
+
*/
|
|
56
|
+
degradedLinks: number;
|
|
57
|
+
/**
|
|
58
|
+
* Repos whose per-repo extraction threw (init, an extractor, or the
|
|
59
|
+
* snapshot read). Each still lands in `unreadableRepos` (group path) —
|
|
60
|
+
* unchanged downstream semantics — but carries its failure reason here: the
|
|
61
|
+
* catch used to swallow the exception, leaving contracts already pushed by
|
|
62
|
+
* earlier extractors in this iteration as silent half-repo data. `repo` is
|
|
63
|
+
* that same group path (e.g. `app/backend`), not the registry display name.
|
|
64
|
+
*/
|
|
65
|
+
failedRepos: Array<{
|
|
66
|
+
repo: string;
|
|
67
|
+
reason: string;
|
|
68
|
+
}>;
|
|
69
|
+
/** Operator-facing run warnings (e.g. bridge.lbug write failed after contracts.json was written). */
|
|
70
|
+
warnings: string[];
|
|
50
71
|
repoSnapshots: Record<string, RepoSnapshot>;
|
|
51
72
|
/**
|
|
52
73
|
* Matching stages this run was asked to skip. Populated on EVERY outcome,
|
package/dist/core/group/sync.js
CHANGED
|
@@ -13,6 +13,7 @@ import { ManifestExtractor } from './extractors/manifest-extractor.js';
|
|
|
13
13
|
import { discoverWorkspaceLinks } from './extractors/workspace-extractor.js';
|
|
14
14
|
import { buildProviderIndex, runExactMatch, runWildcardMatch } from './matching.js';
|
|
15
15
|
import { detectServiceBoundaries, assignService } from './service-boundary-detector.js';
|
|
16
|
+
import { applyDegradedFlag } from './normalization.js';
|
|
16
17
|
import { getContractRegistryPath, readContractRegistry, writeContractRegistry } from './storage.js';
|
|
17
18
|
import { markBridgeProvenanceUnknown, refreshPreservedBridgeMeta, writeBridgeUnlocked, } from './bridge-db.js';
|
|
18
19
|
import { withGroupSyncLock } from './group-lock.js';
|
|
@@ -127,6 +128,8 @@ export function partitionManifestWindows(links, knownRepos, maxResident) {
|
|
|
127
128
|
}
|
|
128
129
|
export async function syncGroup(config, opts) {
|
|
129
130
|
const missingRepos = [];
|
|
131
|
+
const failedRepos = [];
|
|
132
|
+
const warnings = [];
|
|
130
133
|
// Repos that ARE registered but that we could not extract from — the index
|
|
131
134
|
// would not open, or an extractor threw partway and the repo's staged
|
|
132
135
|
// contracts were dropped. Kept separate from `missingRepos` because the two
|
|
@@ -309,6 +312,10 @@ export async function syncGroup(config, opts) {
|
|
|
309
312
|
// stack before logging it defeats the point.
|
|
310
313
|
logger.warn({ err, repo: regName, groupPath, lbugPath }, "⚠️ Could not read this repo's index; its contracts are omitted from this sync.");
|
|
311
314
|
unreadableRepos.push(groupPath);
|
|
315
|
+
failedRepos.push({
|
|
316
|
+
repo: groupPath,
|
|
317
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
318
|
+
});
|
|
312
319
|
// Forget the handle recorded above (present only if the failure came
|
|
313
320
|
// after initLbug). Deferred manifest resolution derives its known-repo
|
|
314
321
|
// set from this map, so leaving the entry here re-opens a repo this
|
|
@@ -459,7 +466,7 @@ export async function syncGroup(config, opts) {
|
|
|
459
466
|
// manifest-declared link can also emit a matchType:'exact' CrossLink with the
|
|
460
467
|
// same endpoints. Prefer the manifest version — it reflects operator intent
|
|
461
468
|
// and carries matchType:'manifest' which downstream consumers may rely on.
|
|
462
|
-
const crossLinks = dedupeCrossLinks([...manifestCrossLinks, ...matched, ...wildcard.matched]);
|
|
469
|
+
const crossLinks = dedupeCrossLinks([...manifestCrossLinks, ...matched, ...wildcard.matched]).map(applyDegradedFlag);
|
|
463
470
|
const allContracts = autoContracts;
|
|
464
471
|
const registry = {
|
|
465
472
|
version: 1,
|
|
@@ -671,10 +678,12 @@ export async function syncGroup(config, opts) {
|
|
|
671
678
|
'a lower bound rather than as complete.'
|
|
672
679
|
: 'Its metadata could NOT be marked provenance-unknown, so those answers may still ' +
|
|
673
680
|
'report as complete despite describing an older sync.';
|
|
674
|
-
|
|
681
|
+
const writeBridgeWarn = '⚠️ writeBridge failed; contracts.json is intact and is the canonical copy, ' +
|
|
675
682
|
'but bridge.lbug was not replaced: cross-repo queries may still answer from ' +
|
|
676
683
|
`the previous sync's contracts. ${provenanceNote} ` +
|
|
677
|
-
'Re-run `gitnexus group sync` to retry.'
|
|
684
|
+
'Re-run `gitnexus group sync` to retry.';
|
|
685
|
+
logger.warn({ err: msg, groupDir, bridgeProvenanceWithdrawn: withdrawn }, writeBridgeWarn);
|
|
686
|
+
warnings.push(writeBridgeWarn);
|
|
678
687
|
}
|
|
679
688
|
}
|
|
680
689
|
});
|
|
@@ -686,6 +695,9 @@ export async function syncGroup(config, opts) {
|
|
|
686
695
|
unmatched: wildcard.remaining,
|
|
687
696
|
missingRepos,
|
|
688
697
|
unreadableRepos,
|
|
698
|
+
failedRepos,
|
|
699
|
+
warnings,
|
|
700
|
+
degradedLinks: crossLinks.filter((l) => l.degraded === true).length,
|
|
689
701
|
repoSnapshots,
|
|
690
702
|
registryOutcome,
|
|
691
703
|
};
|
|
@@ -80,6 +80,19 @@ export interface CrossLink {
|
|
|
80
80
|
contractId: string;
|
|
81
81
|
matchType: MatchType;
|
|
82
82
|
confidence: number;
|
|
83
|
+
/**
|
|
84
|
+
* `true` when the PROVIDER endpoint (`to`) has no resolved graph symbol —
|
|
85
|
+
* empty `symbolUid` / `symbolRef` at sync time (e.g. the handler failed to
|
|
86
|
+
* resolve and `symbolName` degraded to the file name). The contract boundary
|
|
87
|
+
* is still proven, but the link cannot anchor a cross-impact fan-out: an
|
|
88
|
+
* empty provider uid never matches a Phase-1 symbol id, and a downstream
|
|
89
|
+
* fan-out into it has no neighbor symbol to resolve. Derived once at the
|
|
90
|
+
* sync persistence boundary (`isUnresolvedEndpoint` in normalization.ts) and
|
|
91
|
+
* re-derived by `dedupeCrossLinks` when a merge backfills the uid. Absent on
|
|
92
|
+
* fully-anchored links. Distinct from manifest `manifest::…` synthetic UIDs,
|
|
93
|
+
* which have their own `fanout_status: 'not_attempted'` channel downstream.
|
|
94
|
+
*/
|
|
95
|
+
degraded?: boolean;
|
|
83
96
|
}
|
|
84
97
|
export interface RepoSnapshot {
|
|
85
98
|
indexedAt: string;
|
|
@@ -372,6 +372,15 @@ interface LanguageProviderConfig {
|
|
|
372
372
|
* {@link extractModuleConstants} accepts.
|
|
373
373
|
*/
|
|
374
374
|
readonly moduleConstantHeuristic?: (content: string) => boolean;
|
|
375
|
+
/**
|
|
376
|
+
* Prepare this language's harvested constants once the complete repo map is
|
|
377
|
+
* available and before route operands are folded. The parse phase passes only
|
|
378
|
+
* entries owned by this provider, so implementations can build one reusable
|
|
379
|
+
* language-specific index and may materialize deferred bindings in place.
|
|
380
|
+
*
|
|
381
|
+
* Default: undefined (the harvested constants are already fold-ready).
|
|
382
|
+
*/
|
|
383
|
+
readonly prepareRouteConstants?: (repo: RepoConstants) => void;
|
|
375
384
|
/**
|
|
376
385
|
* Fold one file's non-literal route-path operand list
|
|
377
386
|
* (`routePathExpr`/`routePathOperands` of an `ExtractedDecoratorRoute`)
|
|
@@ -723,6 +732,12 @@ export interface LanguageProvider extends Omit<LanguageProviderConfig, 'mroStrat
|
|
|
723
732
|
/** Check if a name is a built-in/stdlib function that should be filtered from the call graph. */
|
|
724
733
|
readonly isBuiltInName: (name: string) => boolean;
|
|
725
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Run each provider's repo-constant preparation hook once over only the files
|
|
737
|
+
* that provider owns. Values are shared with `repo`, so in-place preparation
|
|
738
|
+
* is visible to the subsequent fold without copying the complete map.
|
|
739
|
+
*/
|
|
740
|
+
export declare function prepareRouteConstantsByProvider(repo: RepoConstants, providerForFile: (filePath: string) => Pick<LanguageProvider, 'prepareRouteConstants'> | null): void;
|
|
726
741
|
/** Define a language provider — required fields must be supplied, optional fields get sensible defaults. */
|
|
727
742
|
export declare function defineLanguage(config: LanguageProviderConfig): LanguageProvider;
|
|
728
743
|
export {};
|
|
@@ -45,6 +45,28 @@ export function shouldHarvestModuleConstants(provider, content) {
|
|
|
45
45
|
return false;
|
|
46
46
|
return !provider.moduleConstantHeuristic || provider.moduleConstantHeuristic(content);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Run each provider's repo-constant preparation hook once over only the files
|
|
50
|
+
* that provider owns. Values are shared with `repo`, so in-place preparation
|
|
51
|
+
* is visible to the subsequent fold without copying the complete map.
|
|
52
|
+
*/
|
|
53
|
+
export function prepareRouteConstantsByProvider(repo, providerForFile) {
|
|
54
|
+
const slices = new Map();
|
|
55
|
+
for (const [filePath, constants] of repo) {
|
|
56
|
+
const provider = providerForFile(filePath);
|
|
57
|
+
if (!provider?.prepareRouteConstants)
|
|
58
|
+
continue;
|
|
59
|
+
let slice = slices.get(provider);
|
|
60
|
+
if (!slice) {
|
|
61
|
+
slice = new Map();
|
|
62
|
+
slices.set(provider, slice);
|
|
63
|
+
}
|
|
64
|
+
slice.set(filePath, constants);
|
|
65
|
+
}
|
|
66
|
+
for (const [provider, slice] of slices) {
|
|
67
|
+
provider.prepareRouteConstants?.(slice);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
48
70
|
const DEFAULTS = {
|
|
49
71
|
mroStrategy: 'first-wins',
|
|
50
72
|
};
|
|
@@ -13,7 +13,7 @@ import { defineLanguage } from '../language-provider.js';
|
|
|
13
13
|
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
14
14
|
import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|
15
15
|
import { extractSpringRoutes, extractSpringTypes } from '../route-extractors/spring.js';
|
|
16
|
-
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, } from '../route-extractors/java-const-resolver.js';
|
|
16
|
+
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, prepareJavaRouteConstants, } from '../route-extractors/java-const-resolver.js';
|
|
17
17
|
import { javaExportChecker } from '../export-detection.js';
|
|
18
18
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
19
19
|
import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
|
|
@@ -202,11 +202,10 @@ export const javaProvider = defineLanguage({
|
|
|
202
202
|
// INTERFACES on this side only, which cost the graph its Route nodes while
|
|
203
203
|
// the group still published the contract).
|
|
204
204
|
moduleConstantHeuristic: (content) => isJavaConstantFile(content) ||
|
|
205
|
-
//
|
|
206
|
-
// constant ref
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/\bimport\s+(?:static\s+)?[\w.]+\s*;/.test(content),
|
|
205
|
+
// Class imports and static (including on-demand) imports can bind a
|
|
206
|
+
// constant ref. Ordinary `import a.b.*;` is not a Java type import and is
|
|
207
|
+
// not expanded by extractJavaModuleConstants, so it must not harvest.
|
|
208
|
+
/\bimport\s+(?:static\s+[\w.]+(?:\.\*)?|[\w.]+)\s*;/.test(content),
|
|
209
|
+
prepareRouteConstants: prepareJavaRouteConstants,
|
|
211
210
|
foldRoutePathOperands: foldJavaOperands,
|
|
212
211
|
});
|
|
@@ -30,7 +30,8 @@ import { SCOPE_RESOLVERS } from '../scope-resolution/pipeline/registry.js';
|
|
|
30
30
|
import { DATA_ROUTE_TABLE_SOURCE } from '../route-extractors/data-route-table.js';
|
|
31
31
|
import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
|
|
32
32
|
import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
|
|
33
|
-
import { resolveOperands
|
|
33
|
+
import { resolveOperands } from '../route-extractors/python-const-resolver.js';
|
|
34
|
+
import { prepareRouteConstantsByProvider } from '../language-provider.js';
|
|
34
35
|
import { resolveInheritedSpringRoutes, } from '../route-extractors/spring-shared.js';
|
|
35
36
|
import fs from 'node:fs';
|
|
36
37
|
import { effectiveRamBytes, memoryAutopilotDisabled } from '../utils/effective-ram.js';
|
|
@@ -1022,6 +1023,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1022
1023
|
for (const { filePath, constants } of allModuleConstants) {
|
|
1023
1024
|
repoConstants.set(filePath, constants);
|
|
1024
1025
|
}
|
|
1026
|
+
// Let each language prepare only its own constants slice before folding.
|
|
1027
|
+
// This is where deferred wildcard bindings can be materialized once per
|
|
1028
|
+
// provider without naming a language in the shared parse phase.
|
|
1029
|
+
prepareRouteConstantsByProvider(repoConstants, getProviderForFile);
|
|
1025
1030
|
const resolvedRoutes = [];
|
|
1026
1031
|
let skipped = 0;
|
|
1027
1032
|
for (const dr of allDecoratorRoutes) {
|
|
@@ -62,7 +62,23 @@ export interface ModuleConstants {
|
|
|
62
62
|
readonly literals: Map<string, string>;
|
|
63
63
|
readonly exprs: Map<string, readonly Operand[]>;
|
|
64
64
|
readonly imports: Map<string, ImportBinding>;
|
|
65
|
+
/**
|
|
66
|
+
* On-demand (wildcard) import specifiers whose bound member names could not
|
|
67
|
+
* be enumerated at extract time — Java `import static a.b.C.*;`, Python
|
|
68
|
+
* `from m import *`. The agnostic fold never reads this (it has no way to
|
|
69
|
+
* enumerate a target module's exports); a language binding materializes the
|
|
70
|
+
* promised bindings from a repo-wide map after extraction — see
|
|
71
|
+
* `expandJavaWildcardStaticImports` in the Java binding — so they resolve
|
|
72
|
+
* through the plain `imports` path with no special cases in the fold.
|
|
73
|
+
*/
|
|
74
|
+
readonly wildcardImports?: readonly string[];
|
|
65
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Declaration keys a language extractor found but could not fold. Java and
|
|
78
|
+
* Kotlin both use this metadata to keep lower-priority imports from replacing
|
|
79
|
+
* a real local declaration; other producers simply return the empty set.
|
|
80
|
+
*/
|
|
81
|
+
export declare function unfoldableDeclarationsOf(mc: ModuleConstants | undefined): ReadonlySet<string>;
|
|
66
82
|
/** Repo-wide map: unique file key (e.g. `app/constants.py`) → that file's
|
|
67
83
|
* {@link ModuleConstants}. */
|
|
68
84
|
export type RepoConstants = ReadonlyMap<string, ModuleConstants>;
|
|
@@ -32,6 +32,16 @@ const MAX_RESOLVE_DEPTH = 8;
|
|
|
32
32
|
* so we floor to `null` (skip) instead (#2393). The depth cap bounds recursion but
|
|
33
33
|
* NOT output size, which grows multiplicatively; this bounds the output. */
|
|
34
34
|
export const MAX_FOLD_LENGTH = 8192;
|
|
35
|
+
const NO_UNFOLDABLE_DECLARATIONS = new Set();
|
|
36
|
+
/**
|
|
37
|
+
* Declaration keys a language extractor found but could not fold. Java and
|
|
38
|
+
* Kotlin both use this metadata to keep lower-priority imports from replacing
|
|
39
|
+
* a real local declaration; other producers simply return the empty set.
|
|
40
|
+
*/
|
|
41
|
+
export function unfoldableDeclarationsOf(mc) {
|
|
42
|
+
const declarations = mc?.unfoldableDeclarations;
|
|
43
|
+
return declarations instanceof Set ? declarations : NO_UNFOLDABLE_DECLARATIONS;
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* Fold an operand list to its concatenated literal, or `null` if any operand is
|
|
37
47
|
* unresolvable (an unknown name, a non-string term, a cycle, or a depth overrun).
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
import type Parser from 'tree-sitter';
|
|
43
43
|
import { type ImportResolver, type ModuleConstants, type Operand, type RepoConstants } from './constant-resolver.js';
|
|
44
44
|
export type { ImportBinding, ModuleConstants, Operand, RepoConstants, } from './constant-resolver.js';
|
|
45
|
+
export interface JavaModuleConstants extends ModuleConstants {
|
|
46
|
+
/** Declaration keys whose initializer exists but cannot be folded. */
|
|
47
|
+
readonly unfoldableDeclarations: ReadonlySet<string>;
|
|
48
|
+
}
|
|
45
49
|
export declare function isJavaConstantFile(source: string): boolean;
|
|
46
50
|
/**
|
|
47
51
|
* The Java {@link ImportResolver}: map a fully-qualified import specifier to
|
|
@@ -96,7 +100,7 @@ export declare function parseJavaConstOperands(node: Parser.SyntaxNode | null |
|
|
|
96
100
|
* Last-wins in source order; a non-foldable rebind (`X = compute()`) drops X
|
|
97
101
|
* to unresolvable rather than keeping a stale literal.
|
|
98
102
|
*/
|
|
99
|
-
export declare function extractJavaModuleConstants(tree: Parser.Tree):
|
|
103
|
+
export declare function extractJavaModuleConstants(tree: Parser.Tree): JavaModuleConstants;
|
|
100
104
|
/**
|
|
101
105
|
* Resolve a single Java constant referenced in `fileKey` to its literal string
|
|
102
106
|
* value, folding `+` concatenation and following import chains via
|
|
@@ -112,3 +116,25 @@ export declare function resolveJavaConstant(fileKey: string, name: string, repo:
|
|
|
112
116
|
* `fileKey`, or null when any piece is unresolvable (skip floor).
|
|
113
117
|
*/
|
|
114
118
|
export declare function foldJavaOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants): string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Constant-defining file keys used by Java import resolution.
|
|
121
|
+
*
|
|
122
|
+
* Build once per repo pass. Recomputing this set for every wildcard-importing
|
|
123
|
+
* controller makes expansion quadratic in controller count.
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildJavaConstantKeys(repo: RepoConstants): ReadonlySet<string>;
|
|
126
|
+
export interface JavaConstantIndex {
|
|
127
|
+
readonly keys: ReadonlySet<string>;
|
|
128
|
+
/** Every resolvable path suffix as a dotted module name; null means ambiguous. */
|
|
129
|
+
readonly byModule: ReadonlyMap<string, string | null>;
|
|
130
|
+
/** Direct members by constant-defining file, built once for all importers. */
|
|
131
|
+
readonly membersByFile: ReadonlyMap<string, ReadonlySet<string>>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build all Java import suffixes once, turning repeated wildcard target lookup
|
|
135
|
+
* from O(importers × constant files) into O(path segments + importers).
|
|
136
|
+
*/
|
|
137
|
+
export declare function buildJavaConstantIndex(repo: RepoConstants): JavaConstantIndex;
|
|
138
|
+
export declare function expandJavaWildcardStaticImports(mc: ModuleConstants, _fileKey: string, repo: RepoConstants, index?: JavaConstantIndex): ModuleConstants;
|
|
139
|
+
/** Prepare every Java constants entry with one shared suffix index. */
|
|
140
|
+
export declare function prepareJavaRouteConstants(repo: RepoConstants): JavaConstantIndex;
|