gitnexus 1.6.11-rc.2 → 1.6.11-rc.21

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 (158) hide show
  1. package/README.md +43 -1
  2. package/dist/_shared/impact-risk.d.ts +37 -0
  3. package/dist/_shared/impact-risk.d.ts.map +1 -0
  4. package/dist/_shared/impact-risk.js +92 -0
  5. package/dist/_shared/impact-risk.js.map +1 -0
  6. package/dist/_shared/index.d.ts +2 -0
  7. package/dist/_shared/index.d.ts.map +1 -1
  8. package/dist/_shared/index.js +2 -0
  9. package/dist/_shared/index.js.map +1 -1
  10. package/dist/cli/ai-context.js +4 -4
  11. package/dist/cli/analyze-config.d.ts +2 -0
  12. package/dist/cli/analyze-config.js +16 -0
  13. package/dist/cli/analyze-options.d.ts +4 -0
  14. package/dist/cli/analyze.d.ts +17 -0
  15. package/dist/cli/analyze.js +43 -7
  16. package/dist/cli/eval-server.js +22 -5
  17. package/dist/cli/group.js +7 -1
  18. package/dist/cli/help-i18n.js +2 -0
  19. package/dist/cli/i18n/en.d.ts +12 -2
  20. package/dist/cli/i18n/en.js +12 -2
  21. package/dist/cli/i18n/resources.d.ts +22 -2
  22. package/dist/cli/i18n/zh-CN.d.ts +10 -0
  23. package/dist/cli/i18n/zh-CN.js +12 -2
  24. package/dist/cli/index.js +11 -4
  25. package/dist/cli/status.js +91 -6
  26. package/dist/cli/watch-queue.d.ts +41 -0
  27. package/dist/cli/watch-queue.js +184 -0
  28. package/dist/cli/watch.d.ts +20 -0
  29. package/dist/cli/watch.js +372 -0
  30. package/dist/cli/wiki.js +15 -2
  31. package/dist/config/ignore-service.d.ts +11 -0
  32. package/dist/config/ignore-service.js +41 -4
  33. package/dist/config/repo-control-file.d.ts +3 -0
  34. package/dist/config/repo-control-file.js +115 -0
  35. package/dist/core/group/config-parser.js +20 -2
  36. package/dist/core/group/cross-impact.d.ts +2 -1
  37. package/dist/core/group/cross-impact.js +33 -2
  38. package/dist/core/group/extractors/fs-utils.d.ts +2 -0
  39. package/dist/core/group/extractors/fs-utils.js +86 -0
  40. package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
  41. package/dist/core/group/extractors/graphql-extractor.js +652 -0
  42. package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
  43. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  44. package/dist/core/group/extractors/manifest-extractor.js +1 -1
  45. package/dist/core/group/matching.js +8 -1
  46. package/dist/core/group/service.js +5 -1
  47. package/dist/core/group/storage.js +1 -0
  48. package/dist/core/group/sync.d.ts +3 -1
  49. package/dist/core/group/sync.js +43 -5
  50. package/dist/core/group/types.d.ts +19 -5
  51. package/dist/core/incremental/derived-writeback.d.ts +36 -0
  52. package/dist/core/incremental/derived-writeback.js +68 -0
  53. package/dist/core/incremental/subgraph-extract.d.ts +6 -4
  54. package/dist/core/incremental/subgraph-extract.js +7 -5
  55. package/dist/core/index-content-drift.d.ts +54 -0
  56. package/dist/core/index-content-drift.js +127 -0
  57. package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
  58. package/dist/core/ingestion/filesystem-walker.js +20 -3
  59. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
  60. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
  61. package/dist/core/ingestion/language-provider.d.ts +43 -0
  62. package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
  63. package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
  64. package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
  65. package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
  66. package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
  67. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
  68. package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
  69. package/dist/core/ingestion/languages/java/captures.js +14 -1
  70. package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
  71. package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
  72. package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
  73. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
  74. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
  75. package/dist/core/ingestion/languages/java.js +2 -0
  76. package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
  77. package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
  78. package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
  79. package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
  80. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
  81. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
  82. package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
  83. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
  84. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
  85. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
  86. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
  87. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
  88. package/dist/core/ingestion/languages/kotlin.js +2 -0
  89. package/dist/core/ingestion/pipeline-phases/di.js +47 -14
  90. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
  91. package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
  92. package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
  93. package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
  94. package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
  95. package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
  96. package/dist/core/ingestion/pipeline.d.ts +6 -0
  97. package/dist/core/ingestion/pipeline.js +44 -12
  98. package/dist/core/ingestion/scope-extractor.js +1 -0
  99. package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
  100. package/dist/core/ingestion/utils/symbol-labels.js +2 -2
  101. package/dist/core/ingestion/workers/parse-worker.js +28 -2
  102. package/dist/core/lbug/lbug-adapter.d.ts +59 -0
  103. package/dist/core/lbug/lbug-adapter.js +154 -1
  104. package/dist/core/run-analyze.d.ts +17 -0
  105. package/dist/core/run-analyze.js +231 -23
  106. package/dist/core/search/fts-indexes.d.ts +19 -1
  107. package/dist/core/search/fts-indexes.js +28 -1
  108. package/dist/core/wiki/generator.js +8 -0
  109. package/dist/core/wiki/grok-client.d.ts +21 -0
  110. package/dist/core/wiki/grok-client.js +287 -0
  111. package/dist/core/wiki/llm-client.d.ts +1 -1
  112. package/dist/core/wiki/llm-client.js +5 -2
  113. package/dist/core/wiki/local-cli-client.d.ts +11 -0
  114. package/dist/core/wiki/local-cli-client.js +22 -9
  115. package/dist/mcp/local/local-backend.d.ts +24 -7
  116. package/dist/mcp/local/local-backend.js +190 -81
  117. package/dist/mcp/local/pdg-impact.d.ts +8 -4
  118. package/dist/mcp/local/pdg-impact.js +7 -2
  119. package/dist/mcp/repository-policy.d.ts +5 -1
  120. package/dist/mcp/repository-policy.js +48 -4
  121. package/dist/mcp/resources.js +2 -1
  122. package/dist/mcp/server.js +6 -5
  123. package/dist/mcp/tools.js +31 -19
  124. package/dist/server/api.js +23 -64
  125. package/dist/server/grep-params.d.ts +18 -0
  126. package/dist/server/grep-params.js +83 -0
  127. package/dist/server/grep-scan.d.ts +23 -0
  128. package/dist/server/grep-scan.js +107 -0
  129. package/dist/server/grep-worker.d.ts +1 -0
  130. package/dist/server/grep-worker.js +12 -0
  131. package/dist/server/mcp-http.d.ts +8 -0
  132. package/dist/server/mcp-http.js +16 -1
  133. package/dist/storage/file-hash.d.ts +5 -0
  134. package/dist/storage/file-hash.js +16 -6
  135. package/dist/storage/fs-atomic.d.ts +24 -0
  136. package/dist/storage/fs-atomic.js +86 -2
  137. package/dist/storage/git.d.ts +19 -8
  138. package/dist/storage/git.js +83 -24
  139. package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
  140. package/dist/storage/gitnexus-managed-paths.js +46 -0
  141. package/dist/storage/parse-cache.d.ts +22 -4
  142. package/dist/storage/parse-cache.js +106 -29
  143. package/dist/storage/parsedfile-store.d.ts +34 -60
  144. package/dist/storage/parsedfile-store.js +177 -171
  145. package/dist/storage/repo-manager.d.ts +11 -1
  146. package/dist/storage/repo-manager.js +23 -2
  147. package/dist/storage/repo-meta.d.ts +12 -0
  148. package/dist/storage/v8-sidecar.d.ts +48 -0
  149. package/dist/storage/v8-sidecar.js +347 -0
  150. package/dist/types/pipeline.d.ts +14 -0
  151. package/package.json +4 -1
  152. package/scripts/cross-platform-shard.ts +4 -2
  153. package/scripts/cross-platform-tests.ts +7 -1
  154. package/skills/gitnexus-cli.md +11 -3
  155. package/skills/gitnexus-impact-analysis.md +9 -0
  156. package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
  157. package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
  158. package/web/index.html +1 -1
@@ -1,7 +1,20 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { XMLParser } from 'fast-xml-parser';
3
4
  import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
4
5
  import { logger } from '../../logger.js';
6
+ // POMs are static metadata. Parse hierarchy with a real XML parser, but do not
7
+ // invoke Maven or resolve the effective model. Properties, profiles, and remote
8
+ // parent resolution remain outside this extractor's deterministic boundary.
9
+ const pomParser = new XMLParser({
10
+ ignoreAttributes: true,
11
+ removeNSPrefix: true,
12
+ trimValues: true,
13
+ parseTagValue: false,
14
+ processEntities: false,
15
+ ignoreDeclaration: true,
16
+ ignorePiTags: true,
17
+ });
5
18
  async function parseJavaManifest(repoPath) {
6
19
  const pomPath = path.join(repoPath, 'pom.xml');
7
20
  try {
@@ -9,13 +22,14 @@ async function parseJavaManifest(repoPath) {
9
22
  return parsePom(content);
10
23
  }
11
24
  catch {
12
- // fall through to Gradle
25
+ // Missing pom.xml — fall through to Gradle.
13
26
  }
27
+ const gradleSidecars = await readGradleSidecars(repoPath);
14
28
  for (const name of ['build.gradle.kts', 'build.gradle']) {
15
29
  const gradlePath = path.join(repoPath, name);
16
30
  try {
17
31
  const content = await fs.readFile(gradlePath, 'utf-8');
18
- return parseGradle(content, repoPath);
32
+ return parseGradle(content, repoPath, gradleSidecars);
19
33
  }
20
34
  catch {
21
35
  continue;
@@ -23,45 +37,244 @@ async function parseJavaManifest(repoPath) {
23
37
  }
24
38
  return null;
25
39
  }
40
+ async function readIfPresent(filePath) {
41
+ try {
42
+ return await fs.readFile(filePath, 'utf-8');
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ async function readGradleSidecars(repoPath) {
49
+ const [properties, settingsKts, settingsGroovy, catalog] = await Promise.all([
50
+ readIfPresent(path.join(repoPath, 'gradle.properties')),
51
+ readIfPresent(path.join(repoPath, 'settings.gradle.kts')),
52
+ readIfPresent(path.join(repoPath, 'settings.gradle')),
53
+ readIfPresent(path.join(repoPath, 'gradle', 'libs.versions.toml')),
54
+ ]);
55
+ const sidecars = {
56
+ catalogLibraries: new Map(),
57
+ catalogBundles: new Map(),
58
+ };
59
+ const groupMatch = properties?.match(/(?:^|\n)\s*group\s*=\s*([^\s#]+)/);
60
+ if (groupMatch)
61
+ sidecars.propertiesGroup = groupMatch[1];
62
+ const settings = settingsKts ?? settingsGroovy;
63
+ const nameMatch = settings?.match(/rootProject\.name\s*=\s*['"]([^'"]+)['"]/);
64
+ if (nameMatch)
65
+ sidecars.rootProjectName = nameMatch[1];
66
+ if (catalog) {
67
+ const parsed = parseGradleVersionCatalog(catalog);
68
+ sidecars.catalogLibraries = parsed.libraries;
69
+ sidecars.catalogBundles = parsed.bundles;
70
+ }
71
+ return sidecars;
72
+ }
73
+ function catalogAccessors(alias) {
74
+ const dotted = alias.replace(/[-_]/g, '.');
75
+ const camel = alias.replace(/[-_]+([A-Za-z0-9])/g, (_, char) => char.toUpperCase());
76
+ return [...new Set([alias, dotted, camel])];
77
+ }
78
+ function projectAccessorToArtifactId(accessor) {
79
+ const last = accessor.split('.').pop();
80
+ return last.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`).replace(/^-/, '');
81
+ }
82
+ function moduleToGa(module) {
83
+ const parts = module.split(':');
84
+ return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : undefined;
85
+ }
86
+ function parseInlineTomlTable(rhs) {
87
+ const fields = {};
88
+ for (const match of rhs.matchAll(/([A-Za-z0-9_-]+)\s*=\s*['"]([^'"]+)['"]/g)) {
89
+ fields[match[1]] = match[2];
90
+ }
91
+ return fields;
92
+ }
93
+ /** Default Gradle catalog (`gradle/libs.versions.toml`) — aliases only, no version resolution. */
94
+ function parseGradleVersionCatalog(toml) {
95
+ const libraries = new Map();
96
+ const bundles = new Map();
97
+ let section = 'other';
98
+ const addLibrary = (alias, ga) => {
99
+ for (const accessor of catalogAccessors(alias))
100
+ libraries.set(accessor, ga);
101
+ };
102
+ for (const raw of toml.split(/\r?\n/)) {
103
+ const line = raw.replace(/#.*$/, '').trim();
104
+ if (!line)
105
+ continue;
106
+ const header = line.match(/^\[([^\]]+)\]$/);
107
+ if (header) {
108
+ const name = header[1];
109
+ section =
110
+ name === 'libraries' || name.endsWith('.libraries')
111
+ ? 'libraries'
112
+ : name === 'bundles' || name.endsWith('.bundles')
113
+ ? 'bundles'
114
+ : 'other';
115
+ continue;
116
+ }
117
+ if (section === 'libraries') {
118
+ const dottedModule = line.match(/^([A-Za-z0-9._-]+)\.module\s*=\s*['"]([^'"]+)['"]$/);
119
+ if (dottedModule) {
120
+ const ga = moduleToGa(dottedModule[2]);
121
+ if (ga)
122
+ addLibrary(dottedModule[1], ga);
123
+ continue;
124
+ }
125
+ const assignment = line.match(/^([A-Za-z0-9._-]+)\s*=\s*(.+)$/);
126
+ if (!assignment)
127
+ continue;
128
+ const alias = assignment[1];
129
+ const rhs = assignment[2].trim();
130
+ const quoted = rhs.match(/^['"]([^'"]+)['"]$/);
131
+ if (quoted) {
132
+ const ga = moduleToGa(quoted[1]);
133
+ if (ga)
134
+ addLibrary(alias, ga);
135
+ continue;
136
+ }
137
+ const table = parseInlineTomlTable(rhs);
138
+ const ga = table.module
139
+ ? moduleToGa(table.module)
140
+ : table.group && table.name
141
+ ? `${table.group}:${table.name}`
142
+ : undefined;
143
+ if (ga)
144
+ addLibrary(alias, ga);
145
+ continue;
146
+ }
147
+ if (section === 'bundles') {
148
+ const assignment = line.match(/^([A-Za-z0-9._-]+)\s*=\s*\[([^\]]*)\]$/);
149
+ if (!assignment)
150
+ continue;
151
+ const members = [...assignment[2].matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]);
152
+ for (const accessor of catalogAccessors(assignment[1]))
153
+ bundles.set(accessor, members);
154
+ }
155
+ }
156
+ return { libraries, bundles };
157
+ }
158
+ const GRADLE_GROUP_PATTERNS = [
159
+ /(?:^|[\n{;])\s*(?:rootProject\.)?group\s*=\s*['"]([^'"]+)['"]/,
160
+ /(?:^|[\n{;])\s*group\s+['"]([^'"]+)['"]/,
161
+ ];
162
+ const GRADLE_COORD_CONFIGS = 'implementation|api|compileOnly|runtimeOnly|testImplementation|testApi|testCompileOnly|compile|kapt|ksp|commonMainImplementation|commonMainApi';
163
+ const CATALOG_ALIAS = '([A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+)*)(?:\\.get\\(\\)|\\.asProvider\\(\\))?';
164
+ function gradleDepRe(suffix) {
165
+ return new RegExp(`(?:${GRADLE_COORD_CONFIGS})\\s*${suffix}`, 'g');
166
+ }
167
+ function parseGradleGroup(content) {
168
+ for (const pattern of GRADLE_GROUP_PATTERNS) {
169
+ const match = content.match(pattern);
170
+ if (match?.[1])
171
+ return match[1];
172
+ }
173
+ return undefined;
174
+ }
175
+ function asXmlNode(value) {
176
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
177
+ ? value
178
+ : undefined;
179
+ }
180
+ function xmlText(value) {
181
+ if (typeof value === 'string' || typeof value === 'number') {
182
+ const text = String(value).trim();
183
+ return text || undefined;
184
+ }
185
+ const nested = asXmlNode(value)?.['#text'];
186
+ if (nested === undefined)
187
+ return undefined;
188
+ return xmlText(nested);
189
+ }
190
+ function xmlChildText(node, name) {
191
+ return node ? xmlText(node[name]) : undefined;
192
+ }
193
+ function asList(value) {
194
+ if (value === undefined || value === null)
195
+ return [];
196
+ return Array.isArray(value) ? value : [value];
197
+ }
198
+ /** Direct project dependencies only — not BOM, profiles, or plugin classpath. */
199
+ function collectProjectDependencies(project, deps) {
200
+ const dependencies = asXmlNode(project.dependencies);
201
+ if (!dependencies)
202
+ return;
203
+ for (const dep of asList(dependencies.dependency)) {
204
+ const depNode = asXmlNode(dep);
205
+ const groupId = xmlChildText(depNode, 'groupId');
206
+ const artifactId = xmlChildText(depNode, 'artifactId');
207
+ if (groupId && artifactId)
208
+ deps.push(`${groupId}:${artifactId}`);
209
+ }
210
+ }
26
211
  function parsePom(content) {
27
- const projectGroupMatch = content.match(/<project[^>]*>[\s\S]*?<groupId>([^<]+)<\/groupId>/);
28
- const projectArtifactMatch = content.match(/<project[^>]*>[\s\S]*?<artifactId>([^<]+)<\/artifactId>/);
29
- if (!projectGroupMatch || !projectArtifactMatch)
212
+ let parsed;
213
+ try {
214
+ // parseSourceSafe guards tree-sitter's Windows SIGSEGV by switching to a
215
+ // chunked input callback above 16 KB; XMLParser only accepts XML text, so
216
+ // routing POMs through it silently yields an empty document.
217
+ // eslint-disable-next-line gitnexus/require-safe-parse
218
+ parsed = pomParser.parse(content);
219
+ }
220
+ catch {
30
221
  return null;
31
- const groupId = projectGroupMatch[1].trim();
32
- const artifactId = projectArtifactMatch[1].trim();
33
- const deps = [];
34
- const depBlocks = content.matchAll(/<dependency>\s*([\s\S]*?)<\/dependency>/g);
35
- for (const block of depBlocks) {
36
- const gMatch = block[1].match(/<groupId>([^<]+)<\/groupId>/);
37
- const aMatch = block[1].match(/<artifactId>([^<]+)<\/artifactId>/);
38
- if (gMatch && aMatch) {
39
- deps.push(`${gMatch[1].trim()}:${aMatch[1].trim()}`);
40
- }
41
222
  }
223
+ const project = asXmlNode(asXmlNode(parsed)?.project);
224
+ if (!project)
225
+ return null;
226
+ // Maven inherits groupId from <parent>, but artifactId is always the
227
+ // project's own direct child and must never fall back to parent.artifactId.
228
+ const groupId = xmlChildText(project, 'groupId') ?? xmlChildText(asXmlNode(project.parent), 'groupId');
229
+ const artifactId = xmlChildText(project, 'artifactId');
230
+ if (!groupId || !artifactId)
231
+ return null;
232
+ const deps = [];
233
+ collectProjectDependencies(project, deps);
42
234
  return { groupId, artifactId, deps: [...new Set(deps)] };
43
235
  }
44
- function parseGradle(content, repoPath) {
45
- const groupMatch = content.match(/group\s*=\s*['"]([^'"]+)['"]/);
46
- const dirName = path.basename(repoPath);
47
- const groupId = groupMatch ? groupMatch[1] : '';
236
+ function parseGradle(content, repoPath, sidecars = { catalogLibraries: new Map(), catalogBundles: new Map() }) {
237
+ // Static text + default catalog file. Do not execute Gradle.
238
+ const groupId = parseGradleGroup(content) ?? sidecars.propertiesGroup ?? '';
48
239
  if (!groupId)
49
240
  return null;
50
- const artifactId = dirName;
241
+ const artifactId = sidecars.rootProjectName ?? path.basename(repoPath);
242
+ const { catalogLibraries, catalogBundles } = sidecars;
51
243
  const deps = [];
52
- // implementation("group:artifact:version") or api("group:artifact:version")
53
- const depMatches = content.matchAll(/(?:implementation|api|compileOnly|runtimeOnly)\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
54
- for (const m of depMatches) {
55
- const parts = m[1].split(':');
56
- if (parts.length >= 2) {
57
- deps.push(`${parts[0]}:${parts[1]}`);
244
+ const pushCatalogAlias = (alias) => {
245
+ const ga = catalogLibraries.get(alias);
246
+ if (ga)
247
+ deps.push(ga);
248
+ };
249
+ const namedPattern = gradleDepRe(`(?:\\(\\s*)?(?:group\\s*=\\s*['"](?<group1>[^'"]+)['"]\\s*,\\s*name\\s*=\\s*['"](?<name1>[^'"]+)['"]|name\\s*=\\s*['"](?<name2>[^'"]+)['"]\\s*,\\s*group\\s*=\\s*['"](?<group2>[^'"]+)['"]|group:\\s*['"](?<group3>[^'"]+)['"]\\s*,\\s*name:\\s*['"](?<name3>[^'"]+)['"]|name:\\s*['"](?<name4>[^'"]+)['"]\\s*,\\s*group:\\s*['"](?<group4>[^'"]+)['"])`);
250
+ for (const match of content.matchAll(namedPattern)) {
251
+ const group = match.groups?.group1 ?? match.groups?.group2 ?? match.groups?.group3 ?? match.groups?.group4;
252
+ const name = match.groups?.name1 ?? match.groups?.name2 ?? match.groups?.name3 ?? match.groups?.name4;
253
+ if (group && name)
254
+ deps.push(`${group}:${name}`);
255
+ }
256
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?libs(?:\\.libraries)?\\.(?!bundles\\.|plugins\\.)${CATALOG_ALIAS}`))) {
257
+ pushCatalogAlias(match[1]);
258
+ }
259
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?libs\\.bundles\\.${CATALOG_ALIAS}`))) {
260
+ for (const member of catalogBundles.get(match[1]) ?? []) {
261
+ for (const accessor of catalogAccessors(member))
262
+ pushCatalogAlias(accessor);
58
263
  }
59
264
  }
60
- // implementation(project(":subproject"))
61
- const projDeps = content.matchAll(/(?:implementation|api)\s*\(\s*project\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\)/g);
62
- for (const m of projDeps) {
63
- const subName = m[1].replace(/^:/, '');
64
- deps.push(`${groupId}:${subName}`);
265
+ for (const match of content.matchAll(gradleDepRe(`\\(\\s*projects\\.([A-Za-z][A-Za-z0-9.]*)`))) {
266
+ deps.push(`${groupId}:${projectAccessorToArtifactId(match[1])}`);
267
+ }
268
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*['"]([^'"]+)['"]\\s*\\)|['"]([^'"]+)['"])`))) {
269
+ const coord = match[1] ?? match[2];
270
+ if (!coord)
271
+ continue;
272
+ const parts = coord.split(':');
273
+ if (parts.length >= 2)
274
+ deps.push(`${parts[0]}:${parts[1]}`);
275
+ }
276
+ for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?project\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`))) {
277
+ deps.push(`${groupId}:${match[1].replace(/^:/, '')}`);
65
278
  }
66
279
  return { groupId, artifactId, deps: [...new Set(deps)] };
67
280
  }
@@ -47,7 +47,7 @@ export declare class ManifestExtractor {
47
47
  * equality matching without requiring wildcard logic downstream.
48
48
  *
49
49
  * NOTE on exhaustiveness: the switch covers every current
50
- * `ContractType` variant and falls through to a `never` assertion so
50
+ * manifest-declared contract type and falls through to a `never` assertion so
51
51
  * TypeScript fails the build if a new variant is added without a
52
52
  * corresponding case.
53
53
  */
@@ -311,7 +311,7 @@ export class ManifestExtractor {
311
311
  * equality matching without requiring wildcard logic downstream.
312
312
  *
313
313
  * NOTE on exhaustiveness: the switch covers every current
314
- * `ContractType` variant and falls through to a `never` assertion so
314
+ * manifest-declared contract type and falls through to a `never` assertion so
315
315
  * TypeScript fails the build if a new variant is added without a
316
316
  * corresponding case.
317
317
  */
@@ -21,7 +21,14 @@ function buildNoisyContractFilter(matchingConfig) {
21
21
  ? new Set(matchingConfig.exclude_links_paths.map((p) => p.replace(/\/+$/, '')))
22
22
  : new Set();
23
23
  const excludeParamOnly = matchingConfig?.exclude_links_param_only_paths === true;
24
- return function isNoisyHttpContract(contractId) {
24
+ return function isNoisyContract(contractId) {
25
+ if (contractId.startsWith('graphql::')) {
26
+ const parts = contractId.split('::');
27
+ if (parts.length < 3)
28
+ return false;
29
+ const field = parts.slice(2).join('::');
30
+ return excludePaths.has(field) || excludePaths.has(`/${field}`);
31
+ }
25
32
  if (!contractId.startsWith('http::'))
26
33
  return false;
27
34
  const parts = contractId.split('::');
@@ -301,8 +301,9 @@ export class GroupService {
301
301
  // group tools never need it — so deferring it here keeps that closure off
302
302
  // MCP server startup entirely and off every non-sync group call. The CLI
303
303
  // already does exactly this at `cli/group.ts`'s sync command.
304
- const { syncGroup } = await import('./sync.js');
304
+ const { syncGroup, formatGroupSyncAmbiguousError } = await import('./sync.js');
305
305
  const { GroupSyncLockError } = await import('./group-lock.js');
306
+ const { RegistryAmbiguousTargetError } = await import('../../storage/repo-manager.js');
306
307
  let result;
307
308
  try {
308
309
  result = await syncGroup(config, {
@@ -315,6 +316,9 @@ export class GroupService {
315
316
  });
316
317
  }
317
318
  catch (err) {
319
+ if (err instanceof RegistryAmbiguousTargetError) {
320
+ return { error: formatGroupSyncAmbiguousError(err) };
321
+ }
318
322
  // Fails closed (R9): this sync could not be protected against a concurrent
319
323
  // one, so it did not run and wrote nothing. Return it through the same
320
324
  // error channel a missing group uses — NEVER as a success payload of zeroes,
@@ -78,6 +78,7 @@ packages: {}
78
78
 
79
79
  detect:
80
80
  http: true
81
+ graphql: false
81
82
  grpc: true
82
83
  topics: true
83
84
 
@@ -1,4 +1,4 @@
1
- import { type RegistryEntry } from '../../storage/repo-manager.js';
1
+ import { RegistryAmbiguousTargetError, type RegistryEntry } from '../../storage/repo-manager.js';
2
2
  import type { GroupConfig, RepoHandle, RepoSnapshot, StoredContract, CrossLink, GroupManifestLink, MatchType } from './types.js';
3
3
  export interface SyncOptions {
4
4
  extractorOverride?: ((repo: RepoHandle) => Promise<StoredContract[]>) | (() => Promise<StoredContract[]>);
@@ -64,6 +64,8 @@ export interface SyncResult {
64
64
  registryOutcome: RegistryWriteOutcome;
65
65
  }
66
66
  export declare function stableRepoPoolId(entry: RegistryEntry, allEntries: RegistryEntry[]): string;
67
+ /** Operator copy for group sync — unique `--name`, not a path in yaml. */
68
+ export declare function formatGroupSyncAmbiguousError(err: RegistryAmbiguousTargetError): string;
67
69
  /** A batch of manifest links whose referenced in-group repos fit one resident window. */
68
70
  export interface ManifestWindow {
69
71
  links: GroupManifestLink[];
@@ -2,8 +2,9 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { Buffer } from 'node:buffer';
4
4
  import { initLbug, executeParameterized, pinRepo, getMaxResidentRepos, } from '../lbug/pool-adapter.js';
5
- import { readRegistry, readRegistryStrict, } from '../../storage/repo-manager.js';
5
+ import { findRegistryEntryByName, canonicalizePath, registryPathEquals, readRegistry, readRegistryStrict, RegistryAmbiguousTargetError, } from '../../storage/repo-manager.js';
6
6
  import { HttpRouteExtractor } from './extractors/http-route-extractor.js';
7
+ import { GraphqlExtractor } from './extractors/graphql-extractor.js';
7
8
  import { GrpcExtractor } from './extractors/grpc-extractor.js';
8
9
  import { ThriftExtractor } from './extractors/thrift-extractor.js';
9
10
  import { TopicExtractor } from './extractors/topic-extractor.js';
@@ -27,9 +28,16 @@ export function stableRepoPoolId(entry, allEntries) {
27
28
  }
28
29
  return base;
29
30
  }
31
+ /** Operator copy for group sync — unique `--name`, not a path in yaml. */
32
+ export function formatGroupSyncAmbiguousError(err) {
33
+ const listing = err.matches.map((m) => ` - ${m.path}`).join('\n');
34
+ return (`Multiple registered repos are named "${err.target}":\n${listing}\n` +
35
+ `Give each clone a unique registry name with \`gitnexus analyze --name\`, then re-sync. ` +
36
+ `Do not put a filesystem path in group.yaml.`);
37
+ }
30
38
  function defaultResolveHandle(allEntries) {
31
39
  return async (registryName, groupPath) => {
32
- const e = allEntries.find((en) => en.name === registryName);
40
+ const e = findRegistryEntryByName(allEntries, registryName);
33
41
  if (!e)
34
42
  return null;
35
43
  const poolId = stableRepoPoolId(e, allEntries);
@@ -135,6 +143,9 @@ export async function syncGroup(config, opts) {
135
143
  // windowed manifest resolution below (re-init + lease per window). Keyed by
136
144
  // group path because manifest links reference repos by group path.
137
145
  const repoHandles = new Map();
146
+ // Keep resolved disk paths even when extraction fails and removes the
147
+ // corresponding handle; workspace discovery does not need a readable index.
148
+ const resolvedRepoPaths = new Map();
138
149
  // Every eviction lease this sync holds. Window loops release their own leases
139
150
  // (bounding residency); this set is the defensive outer-finally sweep —
140
151
  // release disposers are idempotent, so double-release is a safe no-op.
@@ -152,7 +163,13 @@ export async function syncGroup(config, opts) {
152
163
  registryEntries = await readRegistryStrict();
153
164
  const entries = registryEntries;
154
165
  const resolve = opts?.resolveRepoHandle ?? defaultResolveHandle(entries);
166
+ if (!opts?.resolveRepoHandle) {
167
+ for (const regName of Object.values(config.repos)) {
168
+ findRegistryEntryByName(entries, regName);
169
+ }
170
+ }
155
171
  const httpEx = new HttpRouteExtractor();
172
+ const graphqlEx = new GraphqlExtractor();
156
173
  const grpcEx = new GrpcExtractor();
157
174
  const thriftEx = new ThriftExtractor();
158
175
  const topicEx = new TopicExtractor();
@@ -163,6 +180,7 @@ export async function syncGroup(config, opts) {
163
180
  missingRepos.push(groupPath);
164
181
  continue;
165
182
  }
183
+ resolvedRepoPaths.set(groupPath, handle.repoPath);
166
184
  const poolId = handle.id;
167
185
  const lbugPath = path.join(handle.storagePath, 'lbug');
168
186
  // Staged per repo, not appended straight to `autoContracts`. Extractors
@@ -181,7 +199,7 @@ export async function syncGroup(config, opts) {
181
199
  // resolution no longer reuses these executors — it re-inits + leases
182
200
  // each repo per window (see windowed resolution below, issue #2189).
183
201
  // Record the pool identity so windowed resolution can re-init.
184
- repoHandles.set(groupPath, { poolId, lbugPath });
202
+ repoHandles.set(groupPath, { poolId, lbugPath, repoPath: handle.repoPath });
185
203
  const executor = (query, params) => executeParameterized(poolId, query, params ?? {});
186
204
  const boundaries = await detectServiceBoundaries(handle.repoPath);
187
205
  if (config.detect.http) {
@@ -194,6 +212,16 @@ export async function syncGroup(config, opts) {
194
212
  });
195
213
  }
196
214
  }
215
+ if (config.detect.graphql === true) {
216
+ const extracted = await graphqlEx.extract(executor, handle.repoPath, handle);
217
+ for (const c of extracted) {
218
+ repoContracts.push({
219
+ ...c,
220
+ repo: groupPath,
221
+ service: assignService(c.symbolRef.filePath, boundaries),
222
+ });
223
+ }
224
+ }
197
225
  if (config.detect.grpc) {
198
226
  const extracted = await grpcEx.extract(executor, handle.repoPath, handle);
199
227
  for (const c of extracted) {
@@ -244,7 +272,8 @@ export async function syncGroup(config, opts) {
244
272
  };
245
273
  }
246
274
  catch {
247
- const e = entries.find((en) => en.name === regName);
275
+ const resolvedHandlePath = canonicalizePath(handle.repoPath);
276
+ const e = entries.find((en) => registryPathEquals(canonicalizePath(en.path), resolvedHandlePath));
248
277
  repoSnapshots[groupPath] = {
249
278
  indexedAt: e?.indexedAt || '',
250
279
  lastCommit: e?.lastCommit || '',
@@ -267,6 +296,8 @@ export async function syncGroup(config, opts) {
267
296
  autoContracts.push(contract);
268
297
  }
269
298
  catch (err) {
299
+ if (err instanceof RegistryAmbiguousTargetError)
300
+ throw err;
270
301
  // This spans initLbug plus all contract extraction for the repo. The
271
302
  // error used to be discarded entirely, so the only trace of (say) a
272
303
  // storage-version mismatch was an empty contracts.json and a later
@@ -297,7 +328,14 @@ export async function syncGroup(config, opts) {
297
328
  if (!registryEntries)
298
329
  registryEntries = await readRegistry();
299
330
  for (const [groupPath, regName] of Object.entries(config.repos)) {
300
- const e = registryEntries.find((en) => en.name === regName);
331
+ const resolvedPath = resolvedRepoPaths.get(groupPath);
332
+ if (resolvedPath) {
333
+ repoPaths.set(groupPath, resolvedPath);
334
+ continue;
335
+ }
336
+ if (opts?.resolveRepoHandle)
337
+ continue;
338
+ const e = findRegistryEntryByName(registryEntries, regName);
301
339
  if (e)
302
340
  repoPaths.set(groupPath, e.path);
303
341
  }
@@ -1,4 +1,6 @@
1
- export type ContractType = 'http' | 'grpc' | 'thrift' | 'topic' | 'lib' | 'custom' | 'include';
1
+ import type { ImpactRisk, ImpactRiskResult } from '../../_shared/index.js';
2
+ export type ContractType = 'http' | 'graphql' | 'grpc' | 'thrift' | 'topic' | 'lib' | 'custom' | 'include';
3
+ export type ManifestContractType = Exclude<ContractType, 'graphql'>;
2
4
  export type MatchType = 'exact' | 'manifest' | 'wildcard';
3
5
  export type ContractRole = 'provider' | 'consumer';
4
6
  export interface GroupConfig {
@@ -14,12 +16,13 @@ export interface GroupConfig {
14
16
  export interface GroupManifestLink {
15
17
  from: string;
16
18
  to: string;
17
- type: ContractType;
19
+ type: ManifestContractType;
18
20
  contract: string;
19
21
  role: ContractRole;
20
22
  }
21
23
  export interface DetectConfig {
22
24
  http: boolean;
25
+ graphql?: boolean;
23
26
  grpc: boolean;
24
27
  thrift: boolean;
25
28
  topics: boolean;
@@ -28,11 +31,12 @@ export interface DetectConfig {
28
31
  }
29
32
  export interface MatchingConfig {
30
33
  /**
31
- * HTTP paths to exclude from cross-link matching. Contracts at these paths
34
+ * HTTP paths or GraphQL root fields to exclude from cross-link matching. Contracts at these paths
32
35
  * are still extracted and visible in the registry, but they don't produce
33
36
  * cross-repo links. Useful for health-check endpoints (`/ping`, `/health`)
34
37
  * that every service exposes and would otherwise create N×M false links.
35
- * Trailing slashes are normalized before comparison.
38
+ * Trailing slashes are normalized before comparison. GraphQL fields may be
39
+ * written as `health` or `/health`.
36
40
  * @default []
37
41
  */
38
42
  exclude_links_paths?: string[];
@@ -163,7 +167,17 @@ export interface GroupImpactResult {
163
167
  modules_affected: number;
164
168
  cross_repo_hits: number;
165
169
  };
166
- risk: string;
170
+ risk: ImpactRisk;
171
+ /**
172
+ * Two-axis (direct + total) risk from the local leg, then `mergeRisk` with
173
+ * crossings — compare File vs symbol here, not via top-level `risk`.
174
+ */
175
+ riskSharedAxes?: ImpactRisk;
176
+ /**
177
+ * Local-leg scale metadata (File / skipped enrichment). Crossings do not
178
+ * invent process/module membership for File nodes.
179
+ */
180
+ riskScale?: ImpactRiskResult['riskScale'];
167
181
  /**
168
182
  * `'lower-bound'` when the fan-out was cut short, so `risk` is a FLOOR, not a
169
183
  * verdict. Same vocabulary as single-repo `impact`'s `epistemic` field.
@@ -0,0 +1,36 @@
1
+ import type { KnowledgeGraph } from '../graph/types.js';
2
+ import type { FileHashDiff } from '../../storage/file-hash.js';
3
+ /** The FTS-backed members of `tables`. */
4
+ export declare const ftsTablesAmong: (tables: Iterable<string>) => Set<string>;
5
+ /**
6
+ * Whether a surgical incremental write may reuse the persisted derived layer.
7
+ *
8
+ * Deletions disqualify it: the persisted Community/Process rows and their
9
+ * MEMBER_OF / STEP_IN_PROCESS edges can reference nodes that no longer exist
10
+ * after this run, and nothing short of re-deriving can tell which.
11
+ *
12
+ * Added or content-changed files also disqualify it: they can introduce,
13
+ * rename, or retarget symbols and CALLS edges that Leiden and flow extraction
14
+ * consume. File-deletion-only was too weak a proof that the derived graph is
15
+ * still valid.
16
+ */
17
+ export declare const shouldPreservePersistedDerivedGraph: (diff: Pick<FileHashDiff, "deleted" | "added" | "changed">) => boolean;
18
+ /**
19
+ * FTS-backed node tables that the fresh graph will WRITE rows into for
20
+ * `fileSet` — the inserting half of the DML.
21
+ *
22
+ * Callers must union this with a DB probe for the deleting half
23
+ * (`nodeTablesWithRowsForFiles`): a table whose last row in these files was
24
+ * just removed by the edit has nothing here, but still holds a stale row that
25
+ * the writeback must delete, and deleting it means taking its index down too.
26
+ */
27
+ export declare const incrementalFtsTablesFromGraph: (graph: KnowledgeGraph, fileSet: ReadonlySet<string>) => Set<string>;
28
+ /**
29
+ * The node tables an incremental DETACH DELETE should target, given the FTS
30
+ * tables this run is rebuilding.
31
+ *
32
+ * Every non-FTS table (Folder, CodeElement, …) deletes as before. An FTS-backed
33
+ * table only deletes when its index is being rebuilt anyway, because deleting
34
+ * from it otherwise would mean DML against a live FTS index (#2589).
35
+ */
36
+ export declare const nodeTablesForIncrementalDelete: (allNodeTables: readonly string[], rebuildingFtsTables: ReadonlySet<string>) => string[];