gitnexus 1.6.6-rc.53 → 1.6.6-rc.55

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.
@@ -64,22 +64,22 @@ export async function syncGroup(config, opts) {
64
64
  let manifestCrossLinks = [];
65
65
  let dbExecutors;
66
66
  let registryEntries;
67
- const eo = opts?.extractorOverride;
68
- if (eo && eo.length === 0) {
69
- autoContracts = await eo();
70
- }
71
- else {
72
- registryEntries = await readRegistry();
73
- const entries = registryEntries;
74
- const resolve = opts?.resolveRepoHandle ?? defaultResolveHandle(entries);
75
- const httpEx = new HttpRouteExtractor();
76
- const grpcEx = new GrpcExtractor();
77
- const thriftEx = new ThriftExtractor();
78
- const topicEx = new TopicExtractor();
79
- const includeEx = new IncludeExtractor();
80
- dbExecutors = new Map();
81
- const openPoolIds = [];
82
- try {
67
+ const openPoolIds = [];
68
+ try {
69
+ const eo = opts?.extractorOverride;
70
+ if (eo && eo.length === 0) {
71
+ autoContracts = await eo();
72
+ }
73
+ else {
74
+ registryEntries = await readRegistry();
75
+ const entries = registryEntries;
76
+ const resolve = opts?.resolveRepoHandle ?? defaultResolveHandle(entries);
77
+ const httpEx = new HttpRouteExtractor();
78
+ const grpcEx = new GrpcExtractor();
79
+ const thriftEx = new ThriftExtractor();
80
+ const topicEx = new TopicExtractor();
81
+ const includeEx = new IncludeExtractor();
82
+ dbExecutors = new Map();
83
83
  for (const [groupPath, regName] of Object.entries(config.repos)) {
84
84
  const handle = await resolve(regName, groupPath);
85
85
  if (!handle) {
@@ -166,55 +166,49 @@ export async function syncGroup(config, opts) {
166
166
  }
167
167
  }
168
168
  }
169
- finally {
170
- for (const id of [...new Set(openPoolIds)]) {
171
- await closeLbug(id).catch(() => { });
169
+ // Workspace discovery and manifest extraction run inside this outer try
170
+ // block so dbExecutors closures resolve against live pools (issue #1802).
171
+ // The finally below closes pools after this completes (or throws).
172
+ let allLinks = [...config.links];
173
+ if (config.detect.workspace_deps) {
174
+ const repoPaths = new Map();
175
+ if (!registryEntries)
176
+ registryEntries = await readRegistry();
177
+ for (const [groupPath, regName] of Object.entries(config.repos)) {
178
+ const e = registryEntries.find((en) => en.name === regName);
179
+ if (e)
180
+ repoPaths.set(groupPath, e.path);
172
181
  }
173
- }
174
- }
175
- // Auto-discover workspace dependency contracts (Rust Cargo workspaces, etc.)
176
- // and merge them with explicit manifest links. Discovered links use the same
177
- // ManifestExtractor pipeline as hand-written links in group.yaml.
178
- let allLinks = [...config.links];
179
- if (config.detect.workspace_deps) {
180
- const repoPaths = new Map();
181
- if (!registryEntries)
182
- registryEntries = await readRegistry();
183
- for (const [groupPath, regName] of Object.entries(config.repos)) {
184
- const e = registryEntries.find((en) => en.name === regName);
185
- if (e)
186
- repoPaths.set(groupPath, e.path);
187
- }
188
- const wsResult = await discoverWorkspaceLinks(config.repos, repoPaths, dbExecutors);
189
- if (wsResult.links.length > 0) {
190
- allLinks = [...allLinks, ...wsResult.links];
191
- if (opts?.verbose) {
192
- for (const s of wsResult.stats) {
193
- logger.info(` workspace-deps: discovered ${s.linkCount} cross-${s.ecosystem.toLowerCase()} links from ${s.projectCount} ${s.ecosystem} projects`);
182
+ const wsResult = await discoverWorkspaceLinks(config.repos, repoPaths, dbExecutors);
183
+ if (wsResult.links.length > 0) {
184
+ allLinks = [...allLinks, ...wsResult.links];
185
+ if (opts?.verbose) {
186
+ for (const s of wsResult.stats) {
187
+ logger.info(` workspace-deps: discovered ${s.linkCount} cross-${s.ecosystem.toLowerCase()} links from ${s.projectCount} ${s.ecosystem} projects`);
188
+ }
194
189
  }
195
190
  }
196
191
  }
197
- }
198
- // Process manifest links declared in group.yaml (plus any auto-discovered).
199
- // ManifestExtractor is fully implemented but was never wired into this
200
- // pipeline config.links were parsed and validated but silently dropped.
201
- // Placed after the DB try/finally: resolveSymbol falls back to synthetic
202
- // UIDs when dbExecutors is undefined or a pool is closed, so cross-links
203
- // are always generated regardless of whether real DB executors are available.
204
- if (allLinks.length > 0) {
205
- const knownRepos = new Set(Object.keys(config.repos));
206
- for (const link of allLinks) {
207
- const dangling = [link.from, link.to].filter((r) => !knownRepos.has(r));
208
- if (dangling.length > 0) {
209
- logger.warn(`[group/sync] manifest link ${link.type}:${link.contract} references repos not in config.repos: ${dangling.join(', ')} — cross-links will use synthetic UIDs`);
192
+ if (allLinks.length > 0) {
193
+ const knownRepos = new Set(Object.keys(config.repos));
194
+ for (const link of allLinks) {
195
+ const dangling = [link.from, link.to].filter((r) => !knownRepos.has(r));
196
+ if (dangling.length > 0) {
197
+ logger.warn(`[group/sync] manifest link ${link.type}:${link.contract} references repos not in config.repos: ${dangling.join(', ')} cross-links will use synthetic UIDs`);
198
+ }
199
+ }
200
+ const manifestEx = new ManifestExtractor();
201
+ const manifestResult = await manifestEx.extractFromManifest(allLinks, dbExecutors);
202
+ autoContracts.push(...manifestResult.contracts);
203
+ manifestCrossLinks = manifestResult.crossLinks;
204
+ if (opts?.verbose) {
205
+ logger.info(` manifest: ${manifestCrossLinks.length} cross-links from ${allLinks.length} links (${config.links.length} declared + ${allLinks.length - config.links.length} discovered)`);
210
206
  }
211
207
  }
212
- const manifestEx = new ManifestExtractor();
213
- const manifestResult = await manifestEx.extractFromManifest(allLinks, dbExecutors);
214
- autoContracts.push(...manifestResult.contracts);
215
- manifestCrossLinks = manifestResult.crossLinks;
216
- if (opts?.verbose) {
217
- logger.info(` manifest: ${manifestCrossLinks.length} cross-links from ${allLinks.length} links (${config.links.length} declared + ${allLinks.length - config.links.length} discovered)`);
208
+ }
209
+ finally {
210
+ for (const id of [...new Set(openPoolIds)]) {
211
+ await closeLbug(id).catch(() => { });
218
212
  }
219
213
  }
220
214
  const providerIndex = buildProviderIndex(autoContracts, config.matching);
@@ -293,6 +293,9 @@ export function populatePhpNamespaceSiblings(parsedFiles, indexes, inputs) {
293
293
  //
294
294
  // Additionally, mirror from files that are imported via `use` (different
295
295
  // namespace) so return types from dependencies are chain-followable too.
296
+ const parsedByPath = new Map();
297
+ for (const p of parsedFiles)
298
+ parsedByPath.set(p.filePath, p);
296
299
  for (const parsed of parsedFiles) {
297
300
  const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
298
301
  if (moduleScope === undefined)
@@ -328,7 +331,7 @@ export function populatePhpNamespaceSiblings(parsedFiles, indexes, inputs) {
328
331
  }
329
332
  // Mirror return-type bindings from accessible files.
330
333
  for (const srcFilePath of accessibleFiles) {
331
- const srcParsed = parsedFiles.find((p) => p.filePath === srcFilePath);
334
+ const srcParsed = parsedByPath.get(srcFilePath);
332
335
  if (srcParsed === undefined)
333
336
  continue;
334
337
  const srcModuleScope = srcParsed.scopes.find((s) => s.kind === 'Module');
@@ -78,9 +78,11 @@ export type ScopeExtractorHooks = Pick<LanguageProvider, 'resolveScopeKind' | 'b
78
78
  * Drive the five extraction passes and return a `ParsedFile`.
79
79
  *
80
80
  * Throws `ScopeTreeInvariantError` (from #912) when the provider emits
81
- * captures that violate structural scope invariants. The error surfaces
82
- * upward rather than being silently corrected a malformed capture set
83
- * is a bug in the provider's `emitScopeCaptures`, not a data condition
84
- * to tolerate.
81
+ * captures that violate structural scope invariants (e.g., overlapping
82
+ * sibling scopes). When no `@scope.module` capture is present, a
83
+ * synthetic Module scope is created spanning all captures, and orphan
84
+ * non-Module scopes are re-parented under it. This enables indexing of
85
+ * files where tree-sitter produces an ERROR root (e.g., complex .phtml
86
+ * templates with mixed PHP/HTML/JS).
85
87
  */
86
88
  export declare function extract(matches: readonly CaptureMatch[], filePath: string, provider: ScopeExtractorHooks): ParsedFile;
@@ -65,17 +65,28 @@ import { extractTemplateArguments } from './utils/template-arguments.js';
65
65
  * Drive the five extraction passes and return a `ParsedFile`.
66
66
  *
67
67
  * Throws `ScopeTreeInvariantError` (from #912) when the provider emits
68
- * captures that violate structural scope invariants. The error surfaces
69
- * upward rather than being silently corrected a malformed capture set
70
- * is a bug in the provider's `emitScopeCaptures`, not a data condition
71
- * to tolerate.
68
+ * captures that violate structural scope invariants (e.g., overlapping
69
+ * sibling scopes). When no `@scope.module` capture is present, a
70
+ * synthetic Module scope is created spanning all captures, and orphan
71
+ * non-Module scopes are re-parented under it. This enables indexing of
72
+ * files where tree-sitter produces an ERROR root (e.g., complex .phtml
73
+ * templates with mixed PHP/HTML/JS).
72
74
  */
73
75
  export function extract(matches, filePath, provider) {
74
76
  // Partition matches by topic up front — one linear pass over the input.
75
77
  const partitioned = partitionByTopic(matches);
76
78
  // ── Pass 1: build the scope tree ─────────────────────────────────────
77
79
  const scopeDrafts = pass1BuildScopes(partitioned.scope, filePath, provider);
78
- const moduleScope = ensureModuleScope(scopeDrafts, matches.length, filePath);
80
+ const moduleScope = ensureModuleScope(scopeDrafts, filePath, matches);
81
+ // Re-parent orphan drafts (parent === null, non-Module) under the
82
+ // Module scope. Replaces drafts with new ones carrying the correct
83
+ // parent — runs before content passes so bindings/ownedDefs are empty.
84
+ for (let i = 0; i < scopeDrafts.length; i++) {
85
+ const d = scopeDrafts[i];
86
+ if (d.parent === null && d.kind !== 'Module') {
87
+ scopeDrafts[i] = makeDraft(d.id, moduleScope.id, d.kind, d.range, d.filePath);
88
+ }
89
+ }
79
90
  const scopes = scopeDrafts.map(draftToScope);
80
91
  // buildScopeTree validates invariants (throws on violation) and exposes
81
92
  // the lookup contract consumed by Passes 2-5.
@@ -171,18 +182,29 @@ function topicOf(match) {
171
182
  }
172
183
  return 'unknown';
173
184
  }
174
- function ensureModuleScope(scopeDrafts, matchCount, filePath) {
185
+ function ensureModuleScope(scopeDrafts, filePath, allMatches) {
175
186
  const moduleScope = scopeDrafts.find((s) => s.kind === 'Module');
176
187
  if (moduleScope !== undefined)
177
188
  return moduleScope;
178
- if (scopeDrafts.length === 0 && matchCount === 0) {
179
- const range = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 };
180
- const synthetic = makeDraft(makeScopeId({ filePath, range, kind: 'Module' }), null, 'Module', range, filePath);
181
- scopeDrafts.push(synthetic);
182
- return synthetic;
189
+ // Synthesize a Module scope spanning all captures in the file.
190
+ // Computed from ALL captures (scope, declaration, reference, etc.)
191
+ // so the range covers top-level references that appear after the
192
+ // last inner scope — not just inner Function/Class scopes.
193
+ let endLine = 0;
194
+ let endCol = 0;
195
+ for (const match of allMatches) {
196
+ for (const capture of Object.values(match)) {
197
+ if (capture.range.endLine > endLine ||
198
+ (capture.range.endLine === endLine && capture.range.endCol > endCol)) {
199
+ endLine = capture.range.endLine;
200
+ endCol = capture.range.endCol;
201
+ }
202
+ }
183
203
  }
184
- throw new Error(`ScopeExtractor: no Module scope found for '${filePath}'. ` +
185
- `Provider must emit at least one @scope.module capture per file.`);
204
+ const range = { startLine: 0, startCol: 0, endLine, endCol };
205
+ const synthetic = makeDraft(makeScopeId({ filePath, range, kind: 'Module' }), null, 'Module', range, filePath);
206
+ scopeDrafts.push(synthetic);
207
+ return synthetic;
186
208
  }
187
209
  function draftToScope(draft) {
188
210
  const frozenBindings = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.53",
3
+ "version": "1.6.6-rc.55",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",