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.
Files changed (201) hide show
  1. package/README.md +10 -0
  2. package/dist/_shared/graph/types.d.ts +1 -1
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +1 -0
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/language-detection.d.ts.map +1 -1
  7. package/dist/_shared/language-detection.js +2 -0
  8. package/dist/_shared/language-detection.js.map +1 -1
  9. package/dist/_shared/languages.d.ts +1 -0
  10. package/dist/_shared/languages.d.ts.map +1 -1
  11. package/dist/_shared/languages.js +1 -0
  12. package/dist/_shared/languages.js.map +1 -1
  13. package/dist/_shared/lbug/schema-constants.d.ts +1 -1
  14. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  15. package/dist/_shared/lbug/schema-constants.js +3 -1
  16. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  17. package/dist/_shared/mro-strategy.d.ts +19 -0
  18. package/dist/_shared/mro-strategy.d.ts.map +1 -0
  19. package/dist/_shared/mro-strategy.js +2 -0
  20. package/dist/_shared/mro-strategy.js.map +1 -0
  21. package/dist/cli/ai-context.d.ts +1 -0
  22. package/dist/cli/ai-context.js +28 -4
  23. package/dist/cli/analyze.d.ts +2 -0
  24. package/dist/cli/analyze.js +2 -1
  25. package/dist/cli/group.d.ts +2 -0
  26. package/dist/cli/group.js +233 -0
  27. package/dist/cli/index.js +3 -0
  28. package/dist/cli/serve.js +4 -1
  29. package/dist/cli/setup.js +34 -3
  30. package/dist/config/ignore-service.js +8 -3
  31. package/dist/core/augmentation/engine.js +1 -1
  32. package/dist/core/git-staleness.d.ts +13 -0
  33. package/dist/core/git-staleness.js +29 -0
  34. package/dist/core/group/bridge-db.d.ts +82 -0
  35. package/dist/core/group/bridge-db.js +460 -0
  36. package/dist/core/group/bridge-schema.d.ts +27 -0
  37. package/dist/core/group/bridge-schema.js +55 -0
  38. package/dist/core/group/config-parser.d.ts +3 -0
  39. package/dist/core/group/config-parser.js +83 -0
  40. package/dist/core/group/contract-extractor.d.ts +7 -0
  41. package/dist/core/group/contract-extractor.js +1 -0
  42. package/dist/core/group/extractors/grpc-extractor.d.ts +16 -0
  43. package/dist/core/group/extractors/grpc-extractor.js +264 -0
  44. package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
  45. package/dist/core/group/extractors/http-route-extractor.js +428 -0
  46. package/dist/core/group/extractors/topic-extractor.d.ts +9 -0
  47. package/dist/core/group/extractors/topic-extractor.js +234 -0
  48. package/dist/core/group/matching.d.ts +13 -0
  49. package/dist/core/group/matching.js +198 -0
  50. package/dist/core/group/normalization.d.ts +3 -0
  51. package/dist/core/group/normalization.js +115 -0
  52. package/dist/core/group/service-boundary-detector.d.ts +8 -0
  53. package/dist/core/group/service-boundary-detector.js +155 -0
  54. package/dist/core/group/service.d.ts +46 -0
  55. package/dist/core/group/service.js +160 -0
  56. package/dist/core/group/storage.d.ts +9 -0
  57. package/dist/core/group/storage.js +91 -0
  58. package/dist/core/group/sync.d.ts +21 -0
  59. package/dist/core/group/sync.js +148 -0
  60. package/dist/core/group/types.d.ts +130 -0
  61. package/dist/core/group/types.js +1 -0
  62. package/dist/core/ingestion/binding-accumulator.d.ts +207 -0
  63. package/dist/core/ingestion/binding-accumulator.js +332 -0
  64. package/dist/core/ingestion/call-processor.d.ts +155 -24
  65. package/dist/core/ingestion/call-processor.js +1129 -247
  66. package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
  67. package/dist/core/ingestion/class-extractors/generic.js +135 -0
  68. package/dist/core/ingestion/class-types.d.ts +34 -0
  69. package/dist/core/ingestion/class-types.js +1 -0
  70. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
  71. package/dist/core/ingestion/entry-point-scoring.js +1 -0
  72. package/dist/core/ingestion/field-types.d.ts +2 -2
  73. package/dist/core/ingestion/filesystem-walker.js +8 -0
  74. package/dist/core/ingestion/framework-detection.d.ts +1 -0
  75. package/dist/core/ingestion/framework-detection.js +1 -0
  76. package/dist/core/ingestion/heritage-processor.d.ts +8 -15
  77. package/dist/core/ingestion/heritage-processor.js +15 -28
  78. package/dist/core/ingestion/import-processor.d.ts +1 -11
  79. package/dist/core/ingestion/import-processor.js +0 -12
  80. package/dist/core/ingestion/import-resolvers/utils.js +1 -0
  81. package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
  82. package/dist/core/ingestion/import-resolvers/vue.js +9 -0
  83. package/dist/core/ingestion/language-provider.d.ts +6 -3
  84. package/dist/core/ingestion/languages/c-cpp.js +168 -1
  85. package/dist/core/ingestion/languages/csharp.js +20 -0
  86. package/dist/core/ingestion/languages/dart.js +26 -4
  87. package/dist/core/ingestion/languages/go.js +22 -0
  88. package/dist/core/ingestion/languages/index.d.ts +1 -0
  89. package/dist/core/ingestion/languages/index.js +2 -0
  90. package/dist/core/ingestion/languages/java.js +17 -0
  91. package/dist/core/ingestion/languages/kotlin.js +24 -1
  92. package/dist/core/ingestion/languages/php.js +23 -11
  93. package/dist/core/ingestion/languages/python.js +9 -0
  94. package/dist/core/ingestion/languages/ruby.js +28 -0
  95. package/dist/core/ingestion/languages/rust.js +38 -0
  96. package/dist/core/ingestion/languages/swift.js +31 -0
  97. package/dist/core/ingestion/languages/typescript.d.ts +1 -0
  98. package/dist/core/ingestion/languages/typescript.js +52 -3
  99. package/dist/core/ingestion/languages/vue.d.ts +13 -0
  100. package/dist/core/ingestion/languages/vue.js +81 -0
  101. package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
  102. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
  103. package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
  104. package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
  105. package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
  106. package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
  107. package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
  108. package/dist/core/ingestion/method-extractors/configs/jvm.js +13 -4
  109. package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
  110. package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
  111. package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
  112. package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
  113. package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
  114. package/dist/core/ingestion/method-extractors/configs/ruby.js +285 -0
  115. package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
  116. package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
  117. package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
  118. package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
  119. package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +85 -8
  120. package/dist/core/ingestion/method-extractors/generic.js +38 -15
  121. package/dist/core/ingestion/method-types.d.ts +25 -0
  122. package/dist/core/ingestion/model/field-registry.d.ts +18 -0
  123. package/dist/core/ingestion/model/field-registry.js +22 -0
  124. package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
  125. package/dist/core/ingestion/model/heritage-map.js +159 -0
  126. package/dist/core/ingestion/model/index.d.ts +20 -0
  127. package/dist/core/ingestion/model/index.js +41 -0
  128. package/dist/core/ingestion/model/method-registry.d.ts +62 -0
  129. package/dist/core/ingestion/model/method-registry.js +130 -0
  130. package/dist/core/ingestion/model/registration-table.d.ts +139 -0
  131. package/dist/core/ingestion/model/registration-table.js +224 -0
  132. package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
  133. package/dist/core/ingestion/model/resolution-context.js +337 -0
  134. package/dist/core/ingestion/model/resolve.d.ts +56 -0
  135. package/dist/core/ingestion/model/resolve.js +242 -0
  136. package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
  137. package/dist/core/ingestion/model/semantic-model.js +120 -0
  138. package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
  139. package/dist/core/ingestion/model/symbol-table.js +206 -0
  140. package/dist/core/ingestion/model/type-registry.d.ts +39 -0
  141. package/dist/core/ingestion/model/type-registry.js +62 -0
  142. package/dist/core/ingestion/mro-processor.d.ts +4 -3
  143. package/dist/core/ingestion/mro-processor.js +310 -106
  144. package/dist/core/ingestion/parsing-processor.d.ts +5 -4
  145. package/dist/core/ingestion/parsing-processor.js +210 -85
  146. package/dist/core/ingestion/pipeline.d.ts +2 -0
  147. package/dist/core/ingestion/pipeline.js +192 -68
  148. package/dist/core/ingestion/tree-sitter-queries.d.ts +5 -5
  149. package/dist/core/ingestion/tree-sitter-queries.js +21 -0
  150. package/dist/core/ingestion/type-env.d.ts +15 -2
  151. package/dist/core/ingestion/type-env.js +163 -102
  152. package/dist/core/ingestion/type-extractors/csharp.js +17 -0
  153. package/dist/core/ingestion/type-extractors/jvm.js +11 -0
  154. package/dist/core/ingestion/type-extractors/php.js +0 -55
  155. package/dist/core/ingestion/type-extractors/ruby.js +0 -32
  156. package/dist/core/ingestion/type-extractors/swift.js +13 -0
  157. package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
  158. package/dist/core/ingestion/type-extractors/typescript.js +66 -69
  159. package/dist/core/ingestion/utils/ast-helpers.d.ts +33 -43
  160. package/dist/core/ingestion/utils/ast-helpers.js +129 -572
  161. package/dist/core/ingestion/utils/method-props.d.ts +32 -0
  162. package/dist/core/ingestion/utils/method-props.js +147 -0
  163. package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
  164. package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
  165. package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
  166. package/dist/core/ingestion/workers/parse-worker.js +463 -198
  167. package/dist/core/lbug/lbug-adapter.d.ts +6 -0
  168. package/dist/core/lbug/lbug-adapter.js +68 -3
  169. package/dist/core/lbug/pool-adapter.d.ts +76 -0
  170. package/dist/core/lbug/pool-adapter.js +522 -0
  171. package/dist/core/run-analyze.d.ts +2 -0
  172. package/dist/core/run-analyze.js +1 -1
  173. package/dist/core/search/bm25-index.js +1 -1
  174. package/dist/core/tree-sitter/parser-loader.js +1 -0
  175. package/dist/core/wiki/graph-queries.js +1 -1
  176. package/dist/mcp/core/embedder.js +6 -5
  177. package/dist/mcp/core/lbug-adapter.d.ts +3 -63
  178. package/dist/mcp/core/lbug-adapter.js +3 -484
  179. package/dist/mcp/local/local-backend.d.ts +31 -2
  180. package/dist/mcp/local/local-backend.js +255 -46
  181. package/dist/mcp/resources.js +5 -4
  182. package/dist/mcp/staleness.d.ts +3 -13
  183. package/dist/mcp/staleness.js +2 -31
  184. package/dist/mcp/tools.js +80 -4
  185. package/dist/server/analyze-job.d.ts +2 -0
  186. package/dist/server/analyze-job.js +4 -0
  187. package/dist/server/api.d.ts +20 -1
  188. package/dist/server/api.js +306 -71
  189. package/dist/server/git-clone.d.ts +2 -1
  190. package/dist/server/git-clone.js +98 -5
  191. package/dist/storage/git.d.ts +13 -0
  192. package/dist/storage/git.js +25 -0
  193. package/dist/storage/repo-manager.js +1 -1
  194. package/package.json +8 -2
  195. package/scripts/patch-tree-sitter-swift.cjs +78 -0
  196. package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
  197. package/dist/core/ingestion/named-binding-processor.js +0 -42
  198. package/dist/core/ingestion/resolution-context.d.ts +0 -58
  199. package/dist/core/ingestion/resolution-context.js +0 -135
  200. package/dist/core/ingestion/symbol-table.d.ts +0 -79
  201. 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,3 @@
1
+ import type { CrossLink, StoredContract } from './types.js';
2
+ export declare function dedupeContracts(items: StoredContract[]): StoredContract[];
3
+ export declare function dedupeCrossLinks(items: CrossLink[]): CrossLink[];
@@ -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
+ }