gitnexus 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/_shared/graph/types.d.ts +1 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +1 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +1 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +3 -1
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/mro-strategy.d.ts +19 -0
- package/dist/_shared/mro-strategy.d.ts.map +1 -0
- package/dist/_shared/mro-strategy.js +2 -0
- package/dist/_shared/mro-strategy.js.map +1 -0
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +28 -4
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +2 -1
- package/dist/cli/group.d.ts +2 -0
- package/dist/cli/group.js +233 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/serve.js +4 -1
- package/dist/cli/setup.js +34 -3
- package/dist/config/ignore-service.js +8 -3
- package/dist/core/augmentation/engine.js +1 -1
- package/dist/core/git-staleness.d.ts +13 -0
- package/dist/core/git-staleness.js +29 -0
- package/dist/core/group/bridge-db.d.ts +82 -0
- package/dist/core/group/bridge-db.js +460 -0
- package/dist/core/group/bridge-schema.d.ts +27 -0
- package/dist/core/group/bridge-schema.js +55 -0
- package/dist/core/group/config-parser.d.ts +3 -0
- package/dist/core/group/config-parser.js +83 -0
- package/dist/core/group/contract-extractor.d.ts +7 -0
- package/dist/core/group/contract-extractor.js +1 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +16 -0
- package/dist/core/group/extractors/grpc-extractor.js +264 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +428 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +9 -0
- package/dist/core/group/extractors/topic-extractor.js +234 -0
- package/dist/core/group/matching.d.ts +13 -0
- package/dist/core/group/matching.js +198 -0
- package/dist/core/group/normalization.d.ts +3 -0
- package/dist/core/group/normalization.js +115 -0
- package/dist/core/group/service-boundary-detector.d.ts +8 -0
- package/dist/core/group/service-boundary-detector.js +155 -0
- package/dist/core/group/service.d.ts +46 -0
- package/dist/core/group/service.js +160 -0
- package/dist/core/group/storage.d.ts +9 -0
- package/dist/core/group/storage.js +91 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +148 -0
- package/dist/core/group/types.d.ts +130 -0
- package/dist/core/group/types.js +1 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +207 -0
- package/dist/core/ingestion/binding-accumulator.js +332 -0
- package/dist/core/ingestion/call-processor.d.ts +155 -24
- package/dist/core/ingestion/call-processor.js +1129 -247
- package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/generic.js +135 -0
- package/dist/core/ingestion/class-types.d.ts +34 -0
- package/dist/core/ingestion/class-types.js +1 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
- package/dist/core/ingestion/entry-point-scoring.js +1 -0
- package/dist/core/ingestion/field-types.d.ts +2 -2
- package/dist/core/ingestion/filesystem-walker.js +8 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +1 -0
- package/dist/core/ingestion/heritage-processor.d.ts +8 -15
- package/dist/core/ingestion/heritage-processor.js +15 -28
- package/dist/core/ingestion/import-processor.d.ts +1 -11
- package/dist/core/ingestion/import-processor.js +0 -12
- package/dist/core/ingestion/import-resolvers/utils.js +1 -0
- package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/vue.js +9 -0
- package/dist/core/ingestion/language-provider.d.ts +6 -3
- package/dist/core/ingestion/languages/c-cpp.js +168 -1
- package/dist/core/ingestion/languages/csharp.js +20 -0
- package/dist/core/ingestion/languages/dart.js +26 -4
- package/dist/core/ingestion/languages/go.js +22 -0
- package/dist/core/ingestion/languages/index.d.ts +1 -0
- package/dist/core/ingestion/languages/index.js +2 -0
- package/dist/core/ingestion/languages/java.js +17 -0
- package/dist/core/ingestion/languages/kotlin.js +24 -1
- package/dist/core/ingestion/languages/php.js +23 -11
- package/dist/core/ingestion/languages/python.js +9 -0
- package/dist/core/ingestion/languages/ruby.js +28 -0
- package/dist/core/ingestion/languages/rust.js +38 -0
- package/dist/core/ingestion/languages/swift.js +31 -0
- package/dist/core/ingestion/languages/typescript.d.ts +1 -0
- package/dist/core/ingestion/languages/typescript.js +52 -3
- package/dist/core/ingestion/languages/vue.d.ts +13 -0
- package/dist/core/ingestion/languages/vue.js +81 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
- package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
- package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +13 -4
- package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
- package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +285 -0
- package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
- package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +85 -8
- package/dist/core/ingestion/method-extractors/generic.js +38 -15
- package/dist/core/ingestion/method-types.d.ts +25 -0
- package/dist/core/ingestion/model/field-registry.d.ts +18 -0
- package/dist/core/ingestion/model/field-registry.js +22 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
- package/dist/core/ingestion/model/heritage-map.js +159 -0
- package/dist/core/ingestion/model/index.d.ts +20 -0
- package/dist/core/ingestion/model/index.js +41 -0
- package/dist/core/ingestion/model/method-registry.d.ts +62 -0
- package/dist/core/ingestion/model/method-registry.js +130 -0
- package/dist/core/ingestion/model/registration-table.d.ts +139 -0
- package/dist/core/ingestion/model/registration-table.js +224 -0
- package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
- package/dist/core/ingestion/model/resolution-context.js +337 -0
- package/dist/core/ingestion/model/resolve.d.ts +56 -0
- package/dist/core/ingestion/model/resolve.js +242 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
- package/dist/core/ingestion/model/semantic-model.js +120 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
- package/dist/core/ingestion/model/symbol-table.js +206 -0
- package/dist/core/ingestion/model/type-registry.d.ts +39 -0
- package/dist/core/ingestion/model/type-registry.js +62 -0
- package/dist/core/ingestion/mro-processor.d.ts +4 -3
- package/dist/core/ingestion/mro-processor.js +310 -106
- package/dist/core/ingestion/parsing-processor.d.ts +5 -4
- package/dist/core/ingestion/parsing-processor.js +210 -85
- package/dist/core/ingestion/pipeline.d.ts +2 -0
- package/dist/core/ingestion/pipeline.js +192 -68
- package/dist/core/ingestion/tree-sitter-queries.d.ts +5 -5
- package/dist/core/ingestion/tree-sitter-queries.js +21 -0
- package/dist/core/ingestion/type-env.d.ts +15 -2
- package/dist/core/ingestion/type-env.js +163 -102
- package/dist/core/ingestion/type-extractors/csharp.js +17 -0
- package/dist/core/ingestion/type-extractors/jvm.js +11 -0
- package/dist/core/ingestion/type-extractors/php.js +0 -55
- package/dist/core/ingestion/type-extractors/ruby.js +0 -32
- package/dist/core/ingestion/type-extractors/swift.js +13 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
- package/dist/core/ingestion/type-extractors/typescript.js +66 -69
- package/dist/core/ingestion/utils/ast-helpers.d.ts +33 -43
- package/dist/core/ingestion/utils/ast-helpers.js +129 -572
- package/dist/core/ingestion/utils/method-props.d.ts +32 -0
- package/dist/core/ingestion/utils/method-props.js +147 -0
- package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
- package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
- package/dist/core/ingestion/workers/parse-worker.js +463 -198
- package/dist/core/lbug/lbug-adapter.d.ts +6 -0
- package/dist/core/lbug/lbug-adapter.js +68 -3
- package/dist/core/lbug/pool-adapter.d.ts +76 -0
- package/dist/core/lbug/pool-adapter.js +522 -0
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +1 -1
- package/dist/core/search/bm25-index.js +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -0
- package/dist/core/wiki/graph-queries.js +1 -1
- package/dist/mcp/core/embedder.js +6 -5
- package/dist/mcp/core/lbug-adapter.d.ts +3 -63
- package/dist/mcp/core/lbug-adapter.js +3 -484
- package/dist/mcp/local/local-backend.d.ts +31 -2
- package/dist/mcp/local/local-backend.js +255 -46
- package/dist/mcp/resources.js +5 -4
- package/dist/mcp/staleness.d.ts +3 -13
- package/dist/mcp/staleness.js +2 -31
- package/dist/mcp/tools.js +80 -4
- package/dist/server/analyze-job.d.ts +2 -0
- package/dist/server/analyze-job.js +4 -0
- package/dist/server/api.d.ts +20 -1
- package/dist/server/api.js +306 -71
- package/dist/server/git-clone.d.ts +2 -1
- package/dist/server/git-clone.js +98 -5
- package/dist/storage/git.d.ts +13 -0
- package/dist/storage/git.js +25 -0
- package/dist/storage/repo-manager.js +1 -1
- package/package.json +8 -2
- package/scripts/patch-tree-sitter-swift.cjs +78 -0
- package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
- package/dist/core/ingestion/named-binding-processor.js +0 -42
- package/dist/core/ingestion/resolution-context.d.ts +0 -58
- package/dist/core/ingestion/resolution-context.js +0 -135
- package/dist/core/ingestion/symbol-table.d.ts +0 -79
- package/dist/core/ingestion/symbol-table.js +0 -115
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
function isGrpcWildcard(cid) {
|
|
2
|
+
return cid.startsWith('grpc::') && cid.endsWith('/*');
|
|
3
|
+
}
|
|
4
|
+
export function normalizeContractId(id) {
|
|
5
|
+
const colonIdx = id.indexOf('::');
|
|
6
|
+
if (colonIdx === -1)
|
|
7
|
+
return id;
|
|
8
|
+
const type = id.substring(0, colonIdx);
|
|
9
|
+
const rest = id.substring(colonIdx + 2);
|
|
10
|
+
switch (type) {
|
|
11
|
+
case 'http': {
|
|
12
|
+
const parts = rest.split('::');
|
|
13
|
+
if (parts.length >= 2) {
|
|
14
|
+
const method = parts[0].toUpperCase();
|
|
15
|
+
let pathPart = parts.slice(1).join('::');
|
|
16
|
+
pathPart = pathPart.replace(/\/+$/, '');
|
|
17
|
+
return `http::${method}::${pathPart}`;
|
|
18
|
+
}
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
21
|
+
case 'grpc': {
|
|
22
|
+
// Canonical form: `grpc::<lowercased-package-or-service>[/<method>]`.
|
|
23
|
+
//
|
|
24
|
+
// The package/service segment is lowercased because gRPC package
|
|
25
|
+
// names are effectively case-insensitive across language bindings
|
|
26
|
+
// (`auth.AuthService`, `auth.authservice`, `AUTH.AUTHSERVICE` all
|
|
27
|
+
// describe the same wire protocol service). The RPC method segment
|
|
28
|
+
// is preserved as-is because the HTTP/2 path used on the wire is
|
|
29
|
+
// case-sensitive per the gRPC spec (`/Service/MethodName`), and
|
|
30
|
+
// method names in generated clients match the proto source exactly.
|
|
31
|
+
//
|
|
32
|
+
// A package-only id (no slash) and a package/method id are treated
|
|
33
|
+
// as DISTINCT canonical forms: `grpc::userservice` does not match
|
|
34
|
+
// `grpc::userservice/Login`. That's by design — callers that want
|
|
35
|
+
// service-level manifest matching against method-level providers
|
|
36
|
+
// should use the gRPC wildcard form `grpc::UserService/*` which is
|
|
37
|
+
// handled by runWildcardMatch below.
|
|
38
|
+
const slashIdx = rest.indexOf('/');
|
|
39
|
+
if (slashIdx > 0) {
|
|
40
|
+
const pkg = rest.substring(0, slashIdx).toLowerCase();
|
|
41
|
+
const method = rest.substring(slashIdx);
|
|
42
|
+
return `grpc::${pkg}${method}`;
|
|
43
|
+
}
|
|
44
|
+
if (slashIdx === 0) {
|
|
45
|
+
// Malformed "/method" with leading slash — keep as-is so two
|
|
46
|
+
// equally malformed ids can still match each other.
|
|
47
|
+
return `grpc::${rest}`;
|
|
48
|
+
}
|
|
49
|
+
// No slash: package/service only. Lowercase to match the package
|
|
50
|
+
// segment produced by the pkg/method branch above.
|
|
51
|
+
return `grpc::${rest.toLowerCase()}`;
|
|
52
|
+
}
|
|
53
|
+
case 'topic':
|
|
54
|
+
return `topic::${rest.trim().toLowerCase()}`;
|
|
55
|
+
case 'lib':
|
|
56
|
+
return `lib::${rest.toLowerCase()}`;
|
|
57
|
+
default:
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function findMatchingKeys(contractId, index) {
|
|
62
|
+
const normalized = normalizeContractId(contractId);
|
|
63
|
+
if (index.has(normalized))
|
|
64
|
+
return [normalized];
|
|
65
|
+
if (normalized.startsWith('http::*::')) {
|
|
66
|
+
const pathPart = normalized.substring('http::*::'.length);
|
|
67
|
+
const matches = [];
|
|
68
|
+
for (const key of index.keys()) {
|
|
69
|
+
if (key.startsWith('http::') && key.endsWith(`::${pathPart}`)) {
|
|
70
|
+
matches.push(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return matches;
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
export function buildProviderIndex(contracts) {
|
|
78
|
+
const providers = contracts.filter((c) => c.role === 'provider');
|
|
79
|
+
const index = new Map();
|
|
80
|
+
for (const p of providers) {
|
|
81
|
+
const key = normalizeContractId(p.contractId);
|
|
82
|
+
const list = index.get(key) || [];
|
|
83
|
+
list.push(p);
|
|
84
|
+
index.set(key, list);
|
|
85
|
+
}
|
|
86
|
+
return index;
|
|
87
|
+
}
|
|
88
|
+
export function runExactMatch(contracts, providerIndex) {
|
|
89
|
+
const index = providerIndex ?? buildProviderIndex(contracts);
|
|
90
|
+
// Skip gRPC wildcard consumers — they go to wildcard pass only
|
|
91
|
+
const consumers = contracts.filter((c) => c.role === 'consumer' && !isGrpcWildcard(c.contractId));
|
|
92
|
+
const matched = [];
|
|
93
|
+
const matchedConsumerIds = new Set();
|
|
94
|
+
const matchedProviderIds = new Set();
|
|
95
|
+
for (const consumer of consumers) {
|
|
96
|
+
const matchingKeys = findMatchingKeys(consumer.contractId, index);
|
|
97
|
+
if (matchingKeys.length === 0)
|
|
98
|
+
continue;
|
|
99
|
+
const allMatchingProviders = matchingKeys.flatMap((k) => index.get(k) || []);
|
|
100
|
+
for (const provider of allMatchingProviders) {
|
|
101
|
+
if (provider.repo === consumer.repo) {
|
|
102
|
+
if (!provider.service || !consumer.service || provider.service === consumer.service) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
matched.push({
|
|
107
|
+
from: {
|
|
108
|
+
repo: consumer.repo,
|
|
109
|
+
service: consumer.service,
|
|
110
|
+
symbolUid: consumer.symbolUid,
|
|
111
|
+
symbolRef: consumer.symbolRef,
|
|
112
|
+
},
|
|
113
|
+
to: {
|
|
114
|
+
repo: provider.repo,
|
|
115
|
+
service: provider.service,
|
|
116
|
+
symbolUid: provider.symbolUid,
|
|
117
|
+
symbolRef: provider.symbolRef,
|
|
118
|
+
},
|
|
119
|
+
type: consumer.type,
|
|
120
|
+
contractId: consumer.contractId,
|
|
121
|
+
matchType: 'exact',
|
|
122
|
+
confidence: 1.0,
|
|
123
|
+
});
|
|
124
|
+
matchedConsumerIds.add(`${consumer.repo}::${consumer.contractId}`);
|
|
125
|
+
matchedProviderIds.add(`${provider.repo}::${provider.contractId}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// normalUnmatched: contracts that weren't matched in exact pass
|
|
129
|
+
const normalUnmatched = contracts.filter((c) => {
|
|
130
|
+
if (isGrpcWildcard(c.contractId))
|
|
131
|
+
return false; // excluded from exact, handled separately
|
|
132
|
+
const id = `${c.repo}::${c.contractId}`;
|
|
133
|
+
return c.role === 'provider' ? !matchedProviderIds.has(id) : !matchedConsumerIds.has(id);
|
|
134
|
+
});
|
|
135
|
+
// Re-add gRPC wildcard contracts — they were never in exact matching
|
|
136
|
+
const grpcWildcards = contracts.filter((c) => isGrpcWildcard(c.contractId));
|
|
137
|
+
const unmatched = [...normalUnmatched, ...grpcWildcards];
|
|
138
|
+
return { matched, unmatched };
|
|
139
|
+
}
|
|
140
|
+
export function runWildcardMatch(unmatched, providerIndex) {
|
|
141
|
+
const wildcardConsumers = unmatched.filter((c) => c.role === 'consumer' && isGrpcWildcard(c.contractId));
|
|
142
|
+
const matched = [];
|
|
143
|
+
const matchedConsumerIds = new Set();
|
|
144
|
+
for (const consumer of wildcardConsumers) {
|
|
145
|
+
const normalized = normalizeContractId(consumer.contractId);
|
|
146
|
+
// "grpc::com.example.userservice/*" → "com.example.userservice"
|
|
147
|
+
// "grpc::userservice/*" → "userservice"
|
|
148
|
+
const fqService = normalized.slice(normalized.indexOf('::') + 2, -2); // strip "grpc::" and "/*"
|
|
149
|
+
for (const [key, providers] of providerIndex) {
|
|
150
|
+
// Only match against non-wildcard gRPC providers (method-level IDs)
|
|
151
|
+
if (!key.startsWith('grpc::') || key.endsWith('/*'))
|
|
152
|
+
continue;
|
|
153
|
+
const afterPrefix = key.slice(6); // strip "grpc::"
|
|
154
|
+
const slashIdx = afterPrefix.indexOf('/');
|
|
155
|
+
if (slashIdx < 0)
|
|
156
|
+
continue;
|
|
157
|
+
const providerFqService = afterPrefix.slice(0, slashIdx);
|
|
158
|
+
// Match: exact FQ service, or bare-name match when consumer has no package
|
|
159
|
+
const isMatch = providerFqService === fqService ||
|
|
160
|
+
(!fqService.includes('.') && providerFqService.endsWith('.' + fqService));
|
|
161
|
+
if (!isMatch)
|
|
162
|
+
continue;
|
|
163
|
+
for (const provider of providers) {
|
|
164
|
+
// Skip same-repo same-service (same logic as runExactMatch)
|
|
165
|
+
if (provider.repo === consumer.repo) {
|
|
166
|
+
if (!provider.service || !consumer.service || provider.service === consumer.service) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
matched.push({
|
|
171
|
+
from: {
|
|
172
|
+
repo: consumer.repo,
|
|
173
|
+
service: consumer.service,
|
|
174
|
+
symbolUid: consumer.symbolUid,
|
|
175
|
+
symbolRef: consumer.symbolRef,
|
|
176
|
+
},
|
|
177
|
+
to: {
|
|
178
|
+
repo: provider.repo,
|
|
179
|
+
service: provider.service,
|
|
180
|
+
symbolUid: provider.symbolUid,
|
|
181
|
+
symbolRef: provider.symbolRef,
|
|
182
|
+
},
|
|
183
|
+
type: consumer.type,
|
|
184
|
+
contractId: consumer.contractId, // consumer's wildcard ID
|
|
185
|
+
matchType: 'wildcard',
|
|
186
|
+
confidence: Math.min(provider.confidence, consumer.confidence),
|
|
187
|
+
});
|
|
188
|
+
matchedConsumerIds.add(`${consumer.repo}::${consumer.contractId}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const remaining = unmatched.filter((c) => {
|
|
193
|
+
if (c.role !== 'consumer' || !isGrpcWildcard(c.contractId))
|
|
194
|
+
return true;
|
|
195
|
+
return !matchedConsumerIds.has(`${c.repo}::${c.contractId}`);
|
|
196
|
+
});
|
|
197
|
+
return { matched, remaining };
|
|
198
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
function contractKey(contract) {
|
|
2
|
+
return [contract.repo, contract.contractId, contract.role, contract.symbolRef.filePath].join('\0');
|
|
3
|
+
}
|
|
4
|
+
function endpointKey(endpoint) {
|
|
5
|
+
return [
|
|
6
|
+
endpoint.repo,
|
|
7
|
+
endpoint.service ?? '',
|
|
8
|
+
endpoint.symbolRef.filePath,
|
|
9
|
+
endpoint.symbolRef.name,
|
|
10
|
+
].join('\0');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Score a contract by how much information it carries, so `dedupeContracts`
|
|
14
|
+
* can prefer the "richer" record when two contracts collide on the same
|
|
15
|
+
* `(repo, contractId, role, filePath)` key.
|
|
16
|
+
*
|
|
17
|
+
* Weights express a priority ordering, not calibrated probabilities:
|
|
18
|
+
* +3 — `symbolUid` resolved (tier 1 of the downstream lookup — highest
|
|
19
|
+
* signal because it's the strongest anchor for cross-impact traversal
|
|
20
|
+
* and the only one that's robust to renames)
|
|
21
|
+
* +2 — any of `filePath`, `symbolRef.name`, or `symbolName` that's more
|
|
22
|
+
* specific than the contractId itself (tier 2 signal — resolves
|
|
23
|
+
* uniquely in most cases and survives across syncs)
|
|
24
|
+
* +1 — `service` tag (monorepo attribution — useful but not sufficient
|
|
25
|
+
* on its own) or non-manifest origin (auto-extracted contracts are
|
|
26
|
+
* preferred over manifest-declared synthetic ones because the former
|
|
27
|
+
* are grounded in real source code)
|
|
28
|
+
*
|
|
29
|
+
* The absolute numbers don't matter, only their relative ordering.
|
|
30
|
+
*/
|
|
31
|
+
function contractRichness(contract) {
|
|
32
|
+
let score = 0;
|
|
33
|
+
if (contract.symbolUid)
|
|
34
|
+
score += 3;
|
|
35
|
+
if (contract.symbolRef.filePath)
|
|
36
|
+
score += 2;
|
|
37
|
+
if (contract.symbolRef.name && contract.symbolRef.name !== contract.contractId)
|
|
38
|
+
score += 2;
|
|
39
|
+
if (contract.symbolName && contract.symbolName !== contract.contractId)
|
|
40
|
+
score += 2;
|
|
41
|
+
if (contract.service)
|
|
42
|
+
score += 1;
|
|
43
|
+
if (contract.meta.source !== 'manifest')
|
|
44
|
+
score += 1;
|
|
45
|
+
return score;
|
|
46
|
+
}
|
|
47
|
+
function mergeContracts(existing, incoming) {
|
|
48
|
+
const [primary, secondary] = contractRichness(incoming) > contractRichness(existing)
|
|
49
|
+
? [incoming, existing]
|
|
50
|
+
: [existing, incoming];
|
|
51
|
+
const symbolRefName = primary.symbolRef.name || secondary.symbolRef.name;
|
|
52
|
+
return {
|
|
53
|
+
...secondary,
|
|
54
|
+
...primary,
|
|
55
|
+
symbolUid: primary.symbolUid || secondary.symbolUid,
|
|
56
|
+
symbolRef: {
|
|
57
|
+
filePath: primary.symbolRef.filePath || secondary.symbolRef.filePath,
|
|
58
|
+
name: symbolRefName,
|
|
59
|
+
},
|
|
60
|
+
symbolName: primary.symbolName || secondary.symbolName || symbolRefName,
|
|
61
|
+
confidence: Math.max(existing.confidence, incoming.confidence),
|
|
62
|
+
service: primary.service ?? secondary.service,
|
|
63
|
+
meta: { ...secondary.meta, ...primary.meta },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function mergeEndpoints(existing, incoming) {
|
|
67
|
+
return {
|
|
68
|
+
repo: existing.repo,
|
|
69
|
+
service: existing.service ?? incoming.service,
|
|
70
|
+
symbolUid: existing.symbolUid || incoming.symbolUid,
|
|
71
|
+
symbolRef: {
|
|
72
|
+
filePath: existing.symbolRef.filePath || incoming.symbolRef.filePath,
|
|
73
|
+
name: existing.symbolRef.name || incoming.symbolRef.name,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function crossLinkKey(link) {
|
|
78
|
+
return [
|
|
79
|
+
link.type,
|
|
80
|
+
link.contractId,
|
|
81
|
+
link.matchType,
|
|
82
|
+
endpointKey(link.from),
|
|
83
|
+
endpointKey(link.to),
|
|
84
|
+
].join('\0');
|
|
85
|
+
}
|
|
86
|
+
export function dedupeContracts(items) {
|
|
87
|
+
const deduped = new Map();
|
|
88
|
+
for (const contract of items) {
|
|
89
|
+
const key = contractKey(contract);
|
|
90
|
+
const existing = deduped.get(key);
|
|
91
|
+
deduped.set(key, existing ? mergeContracts(existing, contract) : contract);
|
|
92
|
+
}
|
|
93
|
+
return [...deduped.values()];
|
|
94
|
+
}
|
|
95
|
+
export function dedupeCrossLinks(items) {
|
|
96
|
+
const deduped = new Map();
|
|
97
|
+
for (const link of items) {
|
|
98
|
+
const key = crossLinkKey(link);
|
|
99
|
+
const existing = deduped.get(key);
|
|
100
|
+
if (!existing) {
|
|
101
|
+
deduped.set(key, link);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const keepIncoming = link.confidence > existing.confidence;
|
|
105
|
+
const primary = keepIncoming ? link : existing;
|
|
106
|
+
const secondary = keepIncoming ? existing : link;
|
|
107
|
+
deduped.set(key, {
|
|
108
|
+
...primary,
|
|
109
|
+
confidence: Math.max(existing.confidence, link.confidence),
|
|
110
|
+
from: mergeEndpoints(primary.from, secondary.from),
|
|
111
|
+
to: mergeEndpoints(primary.to, secondary.to),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return [...deduped.values()];
|
|
115
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ServiceBoundary {
|
|
2
|
+
servicePath: string;
|
|
3
|
+
serviceName: string;
|
|
4
|
+
markers: string[];
|
|
5
|
+
confidence: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function detectServiceBoundaries(repoPath: string): Promise<ServiceBoundary[]>;
|
|
8
|
+
export declare function assignService(filePath: string, boundaries: ServiceBoundary[]): string | undefined;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const SERVICE_MARKERS = [
|
|
4
|
+
'package.json',
|
|
5
|
+
'go.mod',
|
|
6
|
+
'Dockerfile',
|
|
7
|
+
'pom.xml',
|
|
8
|
+
'build.gradle',
|
|
9
|
+
'build.gradle.kts',
|
|
10
|
+
'Cargo.toml',
|
|
11
|
+
'pyproject.toml',
|
|
12
|
+
'requirements.txt',
|
|
13
|
+
'mix.exs',
|
|
14
|
+
];
|
|
15
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
16
|
+
'.ts',
|
|
17
|
+
'.tsx',
|
|
18
|
+
'.js',
|
|
19
|
+
'.jsx',
|
|
20
|
+
'.mjs',
|
|
21
|
+
'.cjs',
|
|
22
|
+
'.go',
|
|
23
|
+
'.java',
|
|
24
|
+
'.kt',
|
|
25
|
+
'.kts',
|
|
26
|
+
'.py',
|
|
27
|
+
'.pyi',
|
|
28
|
+
'.rs',
|
|
29
|
+
'.c',
|
|
30
|
+
'.cpp',
|
|
31
|
+
'.h',
|
|
32
|
+
'.hpp',
|
|
33
|
+
'.cs',
|
|
34
|
+
'.rb',
|
|
35
|
+
'.php',
|
|
36
|
+
'.swift',
|
|
37
|
+
'.dart',
|
|
38
|
+
'.ex',
|
|
39
|
+
'.exs',
|
|
40
|
+
'.erl',
|
|
41
|
+
'.proto',
|
|
42
|
+
]);
|
|
43
|
+
const EXCLUDED_DIRS = new Set([
|
|
44
|
+
'node_modules',
|
|
45
|
+
'vendor',
|
|
46
|
+
'target',
|
|
47
|
+
'build',
|
|
48
|
+
'dist',
|
|
49
|
+
'__pycache__',
|
|
50
|
+
'.venv',
|
|
51
|
+
'venv',
|
|
52
|
+
'.tox',
|
|
53
|
+
'.mypy_cache',
|
|
54
|
+
'.gradle',
|
|
55
|
+
'.mvn',
|
|
56
|
+
'out',
|
|
57
|
+
'bin',
|
|
58
|
+
]);
|
|
59
|
+
export async function detectServiceBoundaries(repoPath) {
|
|
60
|
+
const boundaries = [];
|
|
61
|
+
await walkForBoundaries(repoPath, repoPath, boundaries);
|
|
62
|
+
return boundaries;
|
|
63
|
+
}
|
|
64
|
+
async function walkForBoundaries(dir, repoRoot, results) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const isRoot = path.resolve(dir) === path.resolve(repoRoot);
|
|
73
|
+
const foundMarkers = [];
|
|
74
|
+
let hasSourceFiles = false;
|
|
75
|
+
const subdirs = [];
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.name.startsWith('.') || EXCLUDED_DIRS.has(entry.name))
|
|
78
|
+
continue;
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
subdirs.push(path.join(dir, entry.name));
|
|
81
|
+
}
|
|
82
|
+
else if (entry.isFile()) {
|
|
83
|
+
if (SERVICE_MARKERS.includes(entry.name)) {
|
|
84
|
+
foundMarkers.push(entry.name);
|
|
85
|
+
}
|
|
86
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
87
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
88
|
+
hasSourceFiles = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Check subdirectories for source files if not found at this level
|
|
93
|
+
if (!hasSourceFiles && foundMarkers.length > 0) {
|
|
94
|
+
hasSourceFiles = await hasSourceFilesInSubdirs(subdirs);
|
|
95
|
+
}
|
|
96
|
+
if (!isRoot && foundMarkers.length >= 1 && hasSourceFiles) {
|
|
97
|
+
const relativePath = path.relative(repoRoot, dir).replace(/\\/g, '/');
|
|
98
|
+
const serviceName = path.basename(dir);
|
|
99
|
+
const confidence = computeConfidence(foundMarkers.length);
|
|
100
|
+
results.push({
|
|
101
|
+
servicePath: relativePath,
|
|
102
|
+
serviceName,
|
|
103
|
+
markers: foundMarkers,
|
|
104
|
+
confidence,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Recurse into subdirectories
|
|
108
|
+
for (const subdir of subdirs) {
|
|
109
|
+
await walkForBoundaries(subdir, repoRoot, results);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function hasSourceFilesInSubdirs(subdirs) {
|
|
113
|
+
for (const subdir of subdirs) {
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = await fs.readdir(subdir, { withFileTypes: true });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (entry.isFile()) {
|
|
123
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
124
|
+
if (SOURCE_EXTENSIONS.has(ext))
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name)) {
|
|
128
|
+
const deeper = await hasSourceFilesInSubdirs([path.join(subdir, entry.name)]);
|
|
129
|
+
if (deeper)
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function computeConfidence(markerCount) {
|
|
137
|
+
if (markerCount >= 3)
|
|
138
|
+
return 1.0;
|
|
139
|
+
if (markerCount === 2)
|
|
140
|
+
return 0.9;
|
|
141
|
+
return 0.75;
|
|
142
|
+
}
|
|
143
|
+
export function assignService(filePath, boundaries) {
|
|
144
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
145
|
+
let bestMatch;
|
|
146
|
+
let bestLength = 0;
|
|
147
|
+
for (const boundary of boundaries) {
|
|
148
|
+
const prefix = boundary.servicePath + '/';
|
|
149
|
+
if (normalized.startsWith(prefix) && boundary.servicePath.length > bestLength) {
|
|
150
|
+
bestMatch = boundary;
|
|
151
|
+
bestLength = boundary.servicePath.length;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return bestMatch?.servicePath;
|
|
155
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group orchestration shared by MCP (LocalBackend) and CLI.
|
|
3
|
+
* DB access is injected via GroupToolPort so this module stays free of LocalBackend private API.
|
|
4
|
+
*/
|
|
5
|
+
export interface GroupRepoHandle {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
repoPath: string;
|
|
9
|
+
storagePath: string;
|
|
10
|
+
indexedAt?: string;
|
|
11
|
+
lastCommit?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface GroupToolPort {
|
|
14
|
+
resolveRepo(repoParam?: string): Promise<GroupRepoHandle>;
|
|
15
|
+
impact(repo: GroupRepoHandle, params: {
|
|
16
|
+
target: string;
|
|
17
|
+
direction: 'upstream' | 'downstream';
|
|
18
|
+
maxDepth?: number;
|
|
19
|
+
relationTypes?: string[];
|
|
20
|
+
includeTests?: boolean;
|
|
21
|
+
minConfidence?: number;
|
|
22
|
+
}): Promise<unknown>;
|
|
23
|
+
query(repo: GroupRepoHandle, params: {
|
|
24
|
+
query: string;
|
|
25
|
+
task_context?: string;
|
|
26
|
+
goal?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
max_symbols?: number;
|
|
29
|
+
include_content?: boolean;
|
|
30
|
+
}): Promise<unknown>;
|
|
31
|
+
impactByUid(repoId: string, uid: string, direction: string, opts: {
|
|
32
|
+
maxDepth: number;
|
|
33
|
+
relationTypes: string[];
|
|
34
|
+
minConfidence: number;
|
|
35
|
+
includeTests: boolean;
|
|
36
|
+
}): Promise<unknown | null>;
|
|
37
|
+
}
|
|
38
|
+
export declare class GroupService {
|
|
39
|
+
private readonly port;
|
|
40
|
+
constructor(port: GroupToolPort);
|
|
41
|
+
groupList(params: Record<string, unknown>): Promise<unknown>;
|
|
42
|
+
groupSync(params: Record<string, unknown>): Promise<unknown>;
|
|
43
|
+
groupContracts(params: Record<string, unknown>): Promise<unknown>;
|
|
44
|
+
groupQuery(params: Record<string, unknown>): Promise<unknown>;
|
|
45
|
+
groupStatus(params: Record<string, unknown>): Promise<unknown>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group orchestration shared by MCP (LocalBackend) and CLI.
|
|
3
|
+
* DB access is injected via GroupToolPort so this module stays free of LocalBackend private API.
|
|
4
|
+
*/
|
|
5
|
+
import { checkStaleness } from '../git-staleness.js';
|
|
6
|
+
import { loadGroupConfig } from './config-parser.js';
|
|
7
|
+
import { getDefaultGitnexusDir, getGroupDir, listGroups, readContractRegistry } from './storage.js';
|
|
8
|
+
import { syncGroup } from './sync.js';
|
|
9
|
+
function repoInSubgroup(repoPath, subgroup) {
|
|
10
|
+
if (!subgroup?.trim())
|
|
11
|
+
return true;
|
|
12
|
+
const s = subgroup.replace(/\/+$/, '');
|
|
13
|
+
return repoPath === s || repoPath.startsWith(`${s}/`);
|
|
14
|
+
}
|
|
15
|
+
export class GroupService {
|
|
16
|
+
port;
|
|
17
|
+
constructor(port) {
|
|
18
|
+
this.port = port;
|
|
19
|
+
}
|
|
20
|
+
async groupList(params) {
|
|
21
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
22
|
+
if (!name) {
|
|
23
|
+
const groups = await listGroups();
|
|
24
|
+
return { groups };
|
|
25
|
+
}
|
|
26
|
+
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
27
|
+
const config = await loadGroupConfig(groupDir);
|
|
28
|
+
return {
|
|
29
|
+
name: config.name,
|
|
30
|
+
description: config.description,
|
|
31
|
+
repos: config.repos,
|
|
32
|
+
links: config.links,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async groupSync(params) {
|
|
36
|
+
const name = String(params.name ?? '').trim();
|
|
37
|
+
if (!name)
|
|
38
|
+
return { error: 'name is required' };
|
|
39
|
+
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
40
|
+
const config = await loadGroupConfig(groupDir);
|
|
41
|
+
const result = await syncGroup(config, {
|
|
42
|
+
groupDir,
|
|
43
|
+
exactOnly: Boolean(params.exactOnly),
|
|
44
|
+
skipEmbeddings: Boolean(params.skipEmbeddings),
|
|
45
|
+
allowStale: Boolean(params.allowStale),
|
|
46
|
+
verbose: Boolean(params.verbose),
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
contracts: result.contracts.length,
|
|
50
|
+
crossLinks: result.crossLinks.length,
|
|
51
|
+
unmatched: result.unmatched.length,
|
|
52
|
+
missingRepos: result.missingRepos,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async groupContracts(params) {
|
|
56
|
+
const name = String(params.name ?? '').trim();
|
|
57
|
+
if (!name)
|
|
58
|
+
return { error: 'name is required' };
|
|
59
|
+
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
60
|
+
const registry = await readContractRegistry(groupDir);
|
|
61
|
+
if (!registry) {
|
|
62
|
+
return { error: `No contracts.json for group "${name}". Run group_sync first.` };
|
|
63
|
+
}
|
|
64
|
+
let contracts = registry.contracts;
|
|
65
|
+
if (params.type)
|
|
66
|
+
contracts = contracts.filter((c) => c.type === params.type);
|
|
67
|
+
if (params.repo)
|
|
68
|
+
contracts = contracts.filter((c) => c.repo === params.repo);
|
|
69
|
+
if (params.unmatchedOnly) {
|
|
70
|
+
const matchedIds = new Set(registry.crossLinks.flatMap((l) => [
|
|
71
|
+
`${l.from.repo}::${l.contractId}`,
|
|
72
|
+
`${l.to.repo}::${l.contractId}`,
|
|
73
|
+
]));
|
|
74
|
+
contracts = contracts.filter((c) => !matchedIds.has(`${c.repo}::${c.contractId}`));
|
|
75
|
+
}
|
|
76
|
+
return { contracts, crossLinks: registry.crossLinks };
|
|
77
|
+
}
|
|
78
|
+
async groupQuery(params) {
|
|
79
|
+
const name = String(params.name ?? '').trim();
|
|
80
|
+
const queryText = String(params.query ?? '').trim();
|
|
81
|
+
if (!name || !queryText)
|
|
82
|
+
return { error: 'name and query are required' };
|
|
83
|
+
const limit = typeof params.limit === 'number' && params.limit > 0 ? params.limit : 5;
|
|
84
|
+
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
85
|
+
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
86
|
+
const config = await loadGroupConfig(groupDir);
|
|
87
|
+
const perRepo = [];
|
|
88
|
+
for (const [repoPath, registryName] of Object.entries(config.repos)) {
|
|
89
|
+
if (!repoInSubgroup(repoPath, subgroup))
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const repoObj = await this.port.resolveRepo(registryName);
|
|
93
|
+
const queryResult = (await this.port.query(repoObj, {
|
|
94
|
+
query: queryText,
|
|
95
|
+
limit,
|
|
96
|
+
max_symbols: 10,
|
|
97
|
+
include_content: false,
|
|
98
|
+
}));
|
|
99
|
+
const processes = queryResult.processes || [];
|
|
100
|
+
const scored = processes.map((p, idx) => ({
|
|
101
|
+
...p,
|
|
102
|
+
_rrf_score: 1 / (idx + 1 + 60),
|
|
103
|
+
_repo: repoPath,
|
|
104
|
+
}));
|
|
105
|
+
perRepo.push({ repo: repoPath, score: 0, processes: scored });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
perRepo.push({ repo: repoPath, score: 0, processes: [] });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const allProcesses = perRepo.flatMap((r) => r.processes);
|
|
112
|
+
allProcesses.sort((a, b) => b._rrf_score - a._rrf_score);
|
|
113
|
+
const topN = allProcesses.slice(0, limit);
|
|
114
|
+
return {
|
|
115
|
+
group: name,
|
|
116
|
+
query: queryText,
|
|
117
|
+
results: topN,
|
|
118
|
+
per_repo: perRepo.map((r) => ({ repo: r.repo, count: r.processes.length })),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async groupStatus(params) {
|
|
122
|
+
const name = String(params.name ?? '').trim();
|
|
123
|
+
if (!name)
|
|
124
|
+
return { error: 'name is required' };
|
|
125
|
+
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
126
|
+
const config = await loadGroupConfig(groupDir);
|
|
127
|
+
const registry = await readContractRegistry(groupDir);
|
|
128
|
+
const repoStatuses = {};
|
|
129
|
+
const fsp = await import('node:fs/promises');
|
|
130
|
+
const pathMod = await import('node:path');
|
|
131
|
+
for (const [repoPath, registryName] of Object.entries(config.repos)) {
|
|
132
|
+
try {
|
|
133
|
+
const repoObj = await this.port.resolveRepo(registryName);
|
|
134
|
+
const metaPath = pathMod.join(repoObj.storagePath, 'meta.json');
|
|
135
|
+
const metaRaw = await fsp.readFile(metaPath, 'utf-8').catch(() => '{}');
|
|
136
|
+
const meta = JSON.parse(metaRaw);
|
|
137
|
+
const staleness = meta.lastCommit
|
|
138
|
+
? checkStaleness(repoObj.repoPath, meta.lastCommit)
|
|
139
|
+
: { isStale: true, commitsBehind: -1 };
|
|
140
|
+
const snapshot = registry?.repoSnapshots[repoPath];
|
|
141
|
+
const contractsStale = snapshot && meta.indexedAt ? snapshot.indexedAt !== meta.indexedAt : !snapshot;
|
|
142
|
+
repoStatuses[repoPath] = {
|
|
143
|
+
indexStale: staleness.isStale,
|
|
144
|
+
contractsStale: Boolean(contractsStale),
|
|
145
|
+
missing: false,
|
|
146
|
+
commitsBehind: staleness.commitsBehind,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
repoStatuses[repoPath] = { indexStale: false, contractsStale: false, missing: true };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
group: name,
|
|
155
|
+
lastSync: registry?.generatedAt || null,
|
|
156
|
+
missingRepos: registry?.missingRepos || [],
|
|
157
|
+
repos: repoStatuses,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|