gitnexus 1.6.12-rc.26 → 1.6.12-rc.27

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.
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Ruby import resolution rules:
5
5
  * - `require_relative './foo'` → resolve relative to the importing file's dir
6
- * - `require 'foo'` → suffix-match via the existing Ruby import resolver
7
- * - External gems → null (unresolvable within the repo)
6
+ * - `require 'foo'` → use scoped gem metadata before legacy suffix matching
7
+ * - Known local gems → resolve only within their declared load roots
8
+ * - Known external gems → null, even if an unrelated repo file suffix matches
8
9
  */
9
10
  export interface RubyResolveContext {
10
11
  readonly fromFile: string;
@@ -18,9 +19,10 @@ export interface RubyResolveContext {
18
19
  * against the importing file's directory, trying `.rb` and `/index.rb`
19
20
  * suffixes.
20
21
  *
21
- * For bare requires (gem-style like `'json'`, `'serializable'`), delegates
22
- * to the existing `resolveRubyImportInternal` which uses suffix matching.
23
- *
24
- * Returns `null` for external gems that have no matching file in the repo.
22
+ * For bare requires, scoped manifest metadata takes precedence: known local
23
+ * gems resolve only within their declared load roots (a miss returns `null`),
24
+ * and known external gem prefixes return `null` even if a repo suffix matches.
25
+ * Only requires without matching gem evidence delegate to the existing
26
+ * `resolveRubyImportInternal` suffix matcher.
25
27
  */
26
- export declare function resolveRubyImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, _resolutionConfig?: unknown): string | readonly string[] | null;
28
+ export declare function resolveRubyImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, resolutionConfig?: unknown): string | readonly string[] | null;
@@ -3,12 +3,14 @@
3
3
  *
4
4
  * Ruby import resolution rules:
5
5
  * - `require_relative './foo'` → resolve relative to the importing file's dir
6
- * - `require 'foo'` → suffix-match via the existing Ruby import resolver
7
- * - External gems → null (unresolvable within the repo)
6
+ * - `require 'foo'` → use scoped gem metadata before legacy suffix matching
7
+ * - Known local gems → resolve only within their declared load roots
8
+ * - Known external gems → null, even if an unrelated repo file suffix matches
8
9
  */
9
10
  import { resolveRubyImportInternal } from '../../import-resolvers/ruby.js';
10
11
  import { getWorkspaceFileIndex } from '../../import-resolvers/workspace-file-index.js';
11
12
  import { isHeritageMarker } from '../../utils/heritage-marker.js';
13
+ import { findRubyResolutionScope, } from './resolution-config.js';
12
14
  // ─── resolveRubyImportTarget ──────────────────────────────────────────────
13
15
  /**
14
16
  * ScopeResolver-shaped adapter:
@@ -18,12 +20,13 @@ import { isHeritageMarker } from '../../utils/heritage-marker.js';
18
20
  * against the importing file's directory, trying `.rb` and `/index.rb`
19
21
  * suffixes.
20
22
  *
21
- * For bare requires (gem-style like `'json'`, `'serializable'`), delegates
22
- * to the existing `resolveRubyImportInternal` which uses suffix matching.
23
- *
24
- * Returns `null` for external gems that have no matching file in the repo.
23
+ * For bare requires, scoped manifest metadata takes precedence: known local
24
+ * gems resolve only within their declared load roots (a miss returns `null`),
25
+ * and known external gem prefixes return `null` even if a repo suffix matches.
26
+ * Only requires without matching gem evidence delegate to the existing
27
+ * `resolveRubyImportInternal` suffix matcher.
25
28
  */
26
- export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, _resolutionConfig) {
29
+ export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig) {
27
30
  if (!targetRaw)
28
31
  return null;
29
32
  if (isHeritageMarker(targetRaw))
@@ -37,7 +40,16 @@ export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, _reso
37
40
  const resolved = resolveRelative(targetRaw, fromDir, allFilePaths);
38
41
  return resolved;
39
42
  }
40
- // ── require: bare/gem-style suffix matching ─────────────────────────
43
+ // ── require: scoped gem evidence before repository-wide fallback ────
44
+ const config = resolutionConfig;
45
+ const scope = config === null || config === undefined ? undefined : findRubyResolutionScope(config, fromFile);
46
+ if (scope !== undefined) {
47
+ const localGemTarget = resolveLocalGemTarget(targetRaw, scope, allFilePaths);
48
+ if (localGemTarget !== undefined)
49
+ return localGemTarget;
50
+ if (matchesRequirePrefix(targetRaw, scope.externalRequirePrefixes))
51
+ return null;
52
+ }
41
53
  return resolveBare(targetRaw, allFilePaths);
42
54
  }
43
55
  // ─── internal helpers ─────────────────────────────────────────────────────
@@ -73,6 +85,55 @@ function resolveRelative(targetRaw, fromDir, allFilePaths) {
73
85
  return resolvedPath;
74
86
  return null;
75
87
  }
88
+ /**
89
+ * Yield the require stem, then each slash-delimited ancestor. This makes both
90
+ * local-root and external-gem lookup O(require path depth), not O(gem count).
91
+ * A trailing `.rb` is stripped first so `require 'my_engine.rb'` matches the
92
+ * configured prefix `my_engine`, matching Ruby's optional-suffix require.
93
+ */
94
+ function requirePrefixCandidates(targetRaw) {
95
+ const stem = targetRaw.endsWith('.rb') ? targetRaw.slice(0, -3) : targetRaw;
96
+ if (stem.length === 0)
97
+ return [];
98
+ const candidates = [stem];
99
+ let slash = stem.lastIndexOf('/');
100
+ while (slash !== -1) {
101
+ candidates.push(stem.slice(0, slash));
102
+ slash = stem.lastIndexOf('/', slash - 1);
103
+ }
104
+ return candidates;
105
+ }
106
+ function matchesRequirePrefix(targetRaw, prefixes) {
107
+ return requirePrefixCandidates(targetRaw).some((candidate) => prefixes.has(candidate));
108
+ }
109
+ /**
110
+ * Resolve a path/gemspec-backed gem against its declared Ruby load roots.
111
+ * `undefined` means no local-gem prefix matched; `null` means one matched but
112
+ * none of its declared load roots contained the target.
113
+ */
114
+ function resolveLocalGemTarget(targetRaw, scope, allFilePaths) {
115
+ for (const prefix of requirePrefixCandidates(targetRaw)) {
116
+ const loadRoots = scope.localLoadRootsByPrefix.get(prefix);
117
+ if (loadRoots === undefined)
118
+ continue;
119
+ for (const loadRoot of loadRoots) {
120
+ const base = loadRoot ? `${loadRoot}/${targetRaw}` : targetRaw;
121
+ if (base.endsWith('.rb')) {
122
+ if (allFilePaths.has(base))
123
+ return base;
124
+ continue;
125
+ }
126
+ const rbFile = `${base}.rb`;
127
+ if (allFilePaths.has(rbFile))
128
+ return rbFile;
129
+ }
130
+ // The prefix is owned by a known local gem. If its declared roots do not
131
+ // contain the target, repository-wide suffix matching would fabricate an
132
+ // edge to an unrelated file.
133
+ return null;
134
+ }
135
+ return undefined;
136
+ }
76
137
  /**
77
138
  * Resolve a bare require path (`'serializable'`, `'json'`, `'net/http'`)
78
139
  * via suffix matching using the existing Ruby import resolver.
@@ -0,0 +1,28 @@
1
+ export interface RubyResolutionScope {
2
+ /** Require prefixes provided by gems whose source is outside this repository. */
3
+ readonly externalRequirePrefixes: ReadonlySet<string>;
4
+ /** Require prefix -> repository-relative Ruby load roots for local gems. */
5
+ readonly localLoadRootsByPrefix: ReadonlyMap<string, readonly string[]>;
6
+ }
7
+ export interface RubyResolutionConfig {
8
+ /** Manifest directory (repository-relative POSIX path) -> dependency scope. */
9
+ readonly scopesByDirectory: ReadonlyMap<string, RubyResolutionScope>;
10
+ }
11
+ /**
12
+ * Select the nearest manifest directory that owns `fromFile`.
13
+ *
14
+ * Walking ancestors makes lookup O(path depth), independent of how many
15
+ * sibling Gemfiles a monorepo contains.
16
+ */
17
+ export declare function findRubyResolutionScope(config: RubyResolutionConfig, fromFile: string): RubyResolutionScope | undefined;
18
+ /**
19
+ * Statically collect Ruby dependency sources without evaluating Gemfile or
20
+ * gemspec code. Scopes stay separate by manifest directory so sibling projects
21
+ * in a monorepo cannot suppress one another's local imports. Lockfiles
22
+ * contribute remote GEM/GIT specs and local PATH/GEMSPEC load roots only when
23
+ * an adjacent Gemfile or gemspec establishes a Bundler/RubyGems project.
24
+ *
25
+ * No declarative manifest means no safe gate, so return null and preserve the
26
+ * resolver's existing fail-open behavior for loose script directories.
27
+ */
28
+ export declare function loadRubyResolutionConfig(repoPath: string): RubyResolutionConfig | null;
@@ -0,0 +1,311 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, join, relative, resolve } from 'node:path';
3
+ const MAX_VISITED_DIRECTORIES = 20_000;
4
+ const SKIP_DIRECTORIES = new Set([
5
+ '.git',
6
+ '.gitnexus',
7
+ 'node_modules',
8
+ 'vendor',
9
+ 'dist',
10
+ 'build',
11
+ 'coverage',
12
+ ]);
13
+ const GEMFILE_DEPENDENCY = /^\s*gem\s*(?:\(\s*)?(['"])([A-Za-z0-9_.-]+)\1(?<options>.*)$/;
14
+ const GEMSPEC_DEPENDENCY = /^\s*(?:[A-Za-z_]\w*\.)?(?:add_dependency|add_runtime_dependency|add_development_dependency)\s*(?:\(\s*)?(['"])([A-Za-z0-9_.-]+)\1/;
15
+ const GEMSPEC_NAME = /^\s*(?:[A-Za-z_]\w*\.)?name\s*=\s*(['"])([A-Za-z0-9_.-]+)\1/;
16
+ const GEMSPEC_REQUIRE_PATHS = /^\s*(?:[A-Za-z_]\w*\.)?require_paths\s*=\s*\[([^\]]*)\]/;
17
+ const QUOTED_LITERAL = /(['"])([^'"]+)\1/g;
18
+ const LOCKFILE_SECTION = /^([A-Z][A-Z ]*)\s*$/;
19
+ const LOCKFILE_REMOTE = /^ {2}remote:\s*(.+?)\s*$/;
20
+ const LOCKFILE_SPECS_HEADER = /^ {2}specs:\s*$/;
21
+ const LOCKFILE_SPEC = /^ {4}([A-Za-z0-9_.-]+) \(/;
22
+ function createMutableScope() {
23
+ return {
24
+ externalRequirePrefixes: new Set(),
25
+ localLoadRootsByPrefix: new Map(),
26
+ };
27
+ }
28
+ function normalizeRepoPath(filePath) {
29
+ return filePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
30
+ }
31
+ function repoRelativePath(repoPath, candidate) {
32
+ const rel = relative(resolve(repoPath), resolve(candidate));
33
+ if (rel === '')
34
+ return '';
35
+ if (rel === '..' || rel.startsWith(`..\\`) || rel.startsWith('../') || isAbsolute(rel)) {
36
+ return null;
37
+ }
38
+ return normalizeRepoPath(rel);
39
+ }
40
+ function requirePrefixes(gemName, requireAs) {
41
+ const prefixes = new Set([gemName]);
42
+ if (gemName.includes('-'))
43
+ prefixes.add(gemName.replaceAll('-', '/'));
44
+ if (requireAs !== undefined && requireAs.length > 0)
45
+ prefixes.add(requireAs);
46
+ return [...prefixes];
47
+ }
48
+ function literalOption(options, name) {
49
+ const modern = new RegExp(`(?:^|[,\\s])${name}\\s*:\\s*(['"])([^'"]+)\\1`).exec(options);
50
+ if (modern !== null)
51
+ return modern[2];
52
+ const hashRocket = new RegExp(`(?:^|[,\\s]):${name}\\s*=>\\s*(['"])([^'"]+)\\1`).exec(options);
53
+ return hashRocket?.[2];
54
+ }
55
+ function hasOption(options, name) {
56
+ return (new RegExp(`(?:^|[,\\s])${name}\\s*:`).test(options) ||
57
+ new RegExp(`(?:^|[,\\s]):${name}\\s*=>`).test(options));
58
+ }
59
+ function addExternalGem(scope, gemName, requireAs) {
60
+ for (const prefix of requirePrefixes(gemName, requireAs)) {
61
+ if (!scope.localLoadRootsByPrefix.has(prefix))
62
+ scope.externalRequirePrefixes.add(prefix);
63
+ }
64
+ }
65
+ function addLocalGem(scope, gemName, loadRoots, requireAs) {
66
+ for (const prefix of requirePrefixes(gemName, requireAs)) {
67
+ scope.externalRequirePrefixes.delete(prefix);
68
+ let roots = scope.localLoadRootsByPrefix.get(prefix);
69
+ if (roots === undefined) {
70
+ roots = new Set();
71
+ scope.localLoadRootsByPrefix.set(prefix, roots);
72
+ }
73
+ for (const loadRoot of loadRoots)
74
+ roots.add(loadRoot);
75
+ }
76
+ }
77
+ function parseGemspecMetadata(contents) {
78
+ let name = null;
79
+ let requirePaths = null;
80
+ for (const line of contents.split(/\r?\n/)) {
81
+ const nameMatch = GEMSPEC_NAME.exec(line);
82
+ if (nameMatch !== null)
83
+ name = nameMatch[2];
84
+ const requirePathsMatch = GEMSPEC_REQUIRE_PATHS.exec(line);
85
+ if (requirePathsMatch === null)
86
+ continue;
87
+ const paths = [];
88
+ for (const match of requirePathsMatch[1].matchAll(QUOTED_LITERAL))
89
+ paths.push(match[2]);
90
+ if (paths.length > 0)
91
+ requirePaths = paths;
92
+ }
93
+ return { name, requirePaths: requirePaths ?? ['lib'] };
94
+ }
95
+ function loadLocalGemRoots(repoPath, gemRoot, expectedGemName) {
96
+ const gemRootRelative = repoRelativePath(repoPath, gemRoot);
97
+ if (gemRootRelative === null)
98
+ return null;
99
+ let requirePaths = ['lib'];
100
+ try {
101
+ const gemspecs = readdirSync(gemRoot, { withFileTypes: true })
102
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.gemspec'))
103
+ .map((entry) => join(gemRoot, entry.name))
104
+ .sort();
105
+ for (const gemspec of gemspecs) {
106
+ const metadata = parseGemspecMetadata(readFileSync(gemspec, 'utf8'));
107
+ if (metadata.name === expectedGemName) {
108
+ requirePaths = metadata.requirePaths;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ catch {
114
+ // An unreadable local gem stays local; the conventional lib root is a
115
+ // bounded best effort, and resolution falls through if no file matches.
116
+ }
117
+ return requirePaths
118
+ .map((requirePath) => repoRelativePath(repoPath, resolve(gemRoot, requirePath)))
119
+ .filter((loadRoot) => loadRoot !== null);
120
+ }
121
+ function addGemfileDependencies(contents, manifestDirectory, repoPath, scope) {
122
+ for (const line of contents.split(/\r?\n/)) {
123
+ const match = GEMFILE_DEPENDENCY.exec(line);
124
+ if (match === null)
125
+ continue;
126
+ const gemName = match[2];
127
+ const options = match.groups?.options ?? '';
128
+ const requireAs = literalOption(options, 'require');
129
+ const localPath = literalOption(options, 'path');
130
+ if (localPath !== undefined) {
131
+ const loadRoots = loadLocalGemRoots(repoPath, resolve(manifestDirectory, localPath), gemName);
132
+ if (loadRoots === null)
133
+ addExternalGem(scope, gemName, requireAs);
134
+ else
135
+ addLocalGem(scope, gemName, loadRoots, requireAs);
136
+ continue;
137
+ }
138
+ // A dynamic path expression cannot be classified without evaluating Ruby.
139
+ // Fail open instead of deleting a potentially real in-repo import edge.
140
+ if (hasOption(options, 'path'))
141
+ continue;
142
+ addExternalGem(scope, gemName, requireAs);
143
+ }
144
+ }
145
+ function addGemspecDependencies(contents, gemspecDirectory, repoPath, scope) {
146
+ const metadata = parseGemspecMetadata(contents);
147
+ if (metadata.name !== null) {
148
+ if (repoRelativePath(repoPath, gemspecDirectory) !== null) {
149
+ const loadRoots = metadata.requirePaths
150
+ .map((requirePath) => repoRelativePath(repoPath, resolve(gemspecDirectory, requirePath)))
151
+ .filter((loadRoot) => loadRoot !== null);
152
+ addLocalGem(scope, metadata.name, loadRoots);
153
+ }
154
+ }
155
+ for (const line of contents.split(/\r?\n/)) {
156
+ const match = GEMSPEC_DEPENDENCY.exec(line);
157
+ if (match !== null)
158
+ addExternalGem(scope, match[2]);
159
+ }
160
+ }
161
+ function addLockedDependencies(contents, lockfileDirectory, repoPath, scope) {
162
+ let section = '';
163
+ let remote = null;
164
+ let inSpecs = false;
165
+ for (const line of contents.split(/\r?\n/)) {
166
+ const sectionMatch = LOCKFILE_SECTION.exec(line);
167
+ if (sectionMatch !== null) {
168
+ section = sectionMatch[1];
169
+ remote = null;
170
+ inSpecs = false;
171
+ continue;
172
+ }
173
+ const remoteMatch = LOCKFILE_REMOTE.exec(line);
174
+ if (remoteMatch !== null) {
175
+ remote = remoteMatch[1];
176
+ continue;
177
+ }
178
+ if (LOCKFILE_SPECS_HEADER.test(line)) {
179
+ inSpecs = true;
180
+ continue;
181
+ }
182
+ if (!inSpecs)
183
+ continue;
184
+ const specMatch = LOCKFILE_SPEC.exec(line);
185
+ if (specMatch === null)
186
+ continue;
187
+ const gemName = specMatch[1];
188
+ if (section === 'GEM' || section === 'GIT') {
189
+ addExternalGem(scope, gemName);
190
+ continue;
191
+ }
192
+ if (section !== 'PATH' && section !== 'GEMSPEC')
193
+ continue;
194
+ if (remote === null)
195
+ continue;
196
+ const loadRoots = loadLocalGemRoots(repoPath, resolve(lockfileDirectory, remote), gemName);
197
+ if (loadRoots === null)
198
+ addExternalGem(scope, gemName);
199
+ else
200
+ addLocalGem(scope, gemName, loadRoots);
201
+ }
202
+ }
203
+ function freezeScope(scope) {
204
+ const localLoadRootsByPrefix = new Map();
205
+ for (const [prefix, roots] of scope.localLoadRootsByPrefix) {
206
+ localLoadRootsByPrefix.set(prefix, [...roots].sort());
207
+ }
208
+ return {
209
+ externalRequirePrefixes: new Set(scope.externalRequirePrefixes),
210
+ localLoadRootsByPrefix,
211
+ };
212
+ }
213
+ /**
214
+ * Select the nearest manifest directory that owns `fromFile`.
215
+ *
216
+ * Walking ancestors makes lookup O(path depth), independent of how many
217
+ * sibling Gemfiles a monorepo contains.
218
+ */
219
+ export function findRubyResolutionScope(config, fromFile) {
220
+ const normalized = normalizeRepoPath(fromFile);
221
+ let directory = normalized.includes('/') ? normalized.slice(0, normalized.lastIndexOf('/')) : '';
222
+ for (;;) {
223
+ const scope = config.scopesByDirectory.get(directory);
224
+ if (scope !== undefined)
225
+ return scope;
226
+ if (directory === '')
227
+ return undefined;
228
+ const slash = directory.lastIndexOf('/');
229
+ directory = slash === -1 ? '' : directory.slice(0, slash);
230
+ }
231
+ }
232
+ /**
233
+ * Statically collect Ruby dependency sources without evaluating Gemfile or
234
+ * gemspec code. Scopes stay separate by manifest directory so sibling projects
235
+ * in a monorepo cannot suppress one another's local imports. Lockfiles
236
+ * contribute remote GEM/GIT specs and local PATH/GEMSPEC load roots only when
237
+ * an adjacent Gemfile or gemspec establishes a Bundler/RubyGems project.
238
+ *
239
+ * No declarative manifest means no safe gate, so return null and preserve the
240
+ * resolver's existing fail-open behavior for loose script directories.
241
+ */
242
+ export function loadRubyResolutionConfig(repoPath) {
243
+ const pending = [repoPath];
244
+ const manifestsByDirectory = new Map();
245
+ const lockfilesByDirectory = new Map();
246
+ let visitedDirectories = 0;
247
+ while (pending.length > 0 && visitedDirectories < MAX_VISITED_DIRECTORIES) {
248
+ const directory = pending.pop();
249
+ if (directory === undefined)
250
+ break;
251
+ visitedDirectories++;
252
+ let entries;
253
+ try {
254
+ entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
255
+ }
256
+ catch {
257
+ continue;
258
+ }
259
+ for (const entry of entries) {
260
+ if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) {
261
+ pending.push(join(directory, entry.name));
262
+ }
263
+ else if (entry.isFile() && (entry.name === 'Gemfile' || entry.name.endsWith('.gemspec'))) {
264
+ const manifests = manifestsByDirectory.get(directory) ?? [];
265
+ manifests.push(join(directory, entry.name));
266
+ manifestsByDirectory.set(directory, manifests);
267
+ }
268
+ else if (entry.isFile() && entry.name === 'Gemfile.lock') {
269
+ lockfilesByDirectory.set(directory, join(directory, entry.name));
270
+ }
271
+ }
272
+ }
273
+ if (manifestsByDirectory.size === 0)
274
+ return null;
275
+ const mutableScopes = new Map();
276
+ for (const [directory, manifests] of [...manifestsByDirectory].sort(([left], [right]) => left.localeCompare(right))) {
277
+ const directoryRelative = repoRelativePath(repoPath, directory);
278
+ if (directoryRelative === null)
279
+ continue;
280
+ const scope = createMutableScope();
281
+ mutableScopes.set(directoryRelative, scope);
282
+ for (const manifest of manifests.sort()) {
283
+ try {
284
+ const contents = readFileSync(manifest, 'utf8');
285
+ if (manifest.endsWith('.gemspec')) {
286
+ addGemspecDependencies(contents, directory, repoPath, scope);
287
+ }
288
+ else {
289
+ addGemfileDependencies(contents, directory, repoPath, scope);
290
+ }
291
+ }
292
+ catch {
293
+ // A partially readable scope remains fail-open for unknown gems.
294
+ }
295
+ }
296
+ const lockfile = lockfilesByDirectory.get(directory);
297
+ if (lockfile === undefined)
298
+ continue;
299
+ try {
300
+ addLockedDependencies(readFileSync(lockfile, 'utf8'), directory, repoPath, scope);
301
+ }
302
+ catch {
303
+ // Direct declarations still provide bounded evidence when the lock is unreadable.
304
+ }
305
+ }
306
+ const scopesByDirectory = new Map();
307
+ for (const [directory, scope] of mutableScopes) {
308
+ scopesByDirectory.set(directory, freezeScope(scope));
309
+ }
310
+ return { scopesByDirectory };
311
+ }
@@ -7,6 +7,7 @@ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
7
7
  import { generateId } from '../../../../lib/utils.js';
8
8
  import { decodeMarker } from '../../utils/heritage-marker.js';
9
9
  import { rubyIsGlobalNameFallbackPlausible } from './name-fallback-visibility.js';
10
+ import { loadRubyResolutionConfig } from './resolution-config.js';
10
11
  /**
11
12
  * #1991: resolve a BARE mixin reference (`include Loggable`) to a nested module by
12
13
  * the INCLUDING class's lexical scope — Ruby looks up a constant in the innermost
@@ -248,6 +249,7 @@ export const rubyScopeResolver = {
248
249
  languageProvider: rubyProvider,
249
250
  importEdgeReason: 'ruby-scope: import',
250
251
  resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig),
252
+ loadResolutionConfig: (repoPath) => loadRubyResolutionConfig(repoPath),
251
253
  expandsWildcardTo: (targetModuleScope, parsedFiles) => expandRubyWildcardNames(targetModuleScope, parsedFiles),
252
254
  mergeBindings: (existing, incoming, scopeId) => rubyMergeBindings(existing, incoming, scopeId),
253
255
  arityCompatibility: (callsite, def) => rubyArityCompatibility(def, callsite),
@@ -47,7 +47,7 @@ export declare const DESTINATION_SCHEMA = "\nCREATE NODE TABLE Destination (\n
47
47
  export declare const SECTION_SCHEMA = "\nCREATE NODE TABLE Section (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n level INT64,\n content STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
48
48
  export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n text STRING,\n callees STRING,\n calleeIds STRING,\n PRIMARY KEY (id)\n)";
49
49
  /**
50
- * The 66 pairs NEITHER rule above generates — everything left after the two
50
+ * The pairs NEITHER rule above generates — everything left after the two
51
51
  * cross products are subtracted. Carried by CONTAINMENT, inheritance, imports
52
52
  * and DI: a container label crossed with a contained label. No predicate
53
53
  * describes that surface (any container can hold any definition).
@@ -60,6 +60,8 @@ export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id
60
60
  * templates), the two `Route|Process` / `Tool|Process` entry points whose
61
61
  * emitter names both labels as literals, and `BasicBlock|BasicBlock`, the PDG
62
62
  * substrate.
63
+ * Spring dynamic lookup in a constructor also emits INJECTS to a synthetic
64
+ * @Bean CodeElement (#3238), so Constructor|CodeElement belongs here.
63
65
  *
64
66
  * NOTHING A RULE ALREADY COVERS BELONGS HERE. `generatedRelationPairs` skips
65
67
  * any pair present in this block, so a redundant line does not merely duplicate
@@ -96,7 +98,7 @@ export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id
96
98
  * `test/integration/structural-pair-coverage.test.ts` guards this half from a
97
99
  * corpus — that is the guard, not this comment.
98
100
  */
99
- export declare const STRUCTURAL_PAIR_DDL = " FROM File TO Folder,\n FROM File TO CodeElement,\n FROM File TO `Typedef`,\n FROM File TO `Namespace`,\n FROM File TO `Impl`,\n FROM File TO `Static`,\n FROM File TO `Template`,\n FROM File TO Section,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO `Template`,\n FROM Function TO `Namespace`,\n FROM Function TO `Impl`,\n FROM Function TO `Typedef`,\n FROM Function TO CodeElement,\n FROM Class TO `Template`,\n FROM Class TO `Impl`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO CodeElement,\n FROM Method TO `Template`,\n FROM Method TO `Namespace`,\n FROM Method TO `Impl`,\n FROM Method TO CodeElement,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO CodeElement,\n FROM `Module` TO `Namespace`,\n FROM `Namespace` TO Function,\n FROM CodeElement TO CodeElement,\n FROM CodeElement TO Class,\n FROM CodeElement TO Category,\n FROM CodeElement TO Method,\n FROM CodeElement TO `Module`,\n FROM CodeElement TO `Property`,\n FROM Section TO Section,\n FROM Interface TO CodeElement,\n FROM `Namespace` TO `Struct`,\n FROM `Impl` TO Method,\n FROM `Impl` TO Function,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Typedef`,\n FROM Route TO Process,\n FROM Tool TO Process,\n FROM Destination TO `Property`,\n FROM BasicBlock TO BasicBlock";
101
+ export declare const STRUCTURAL_PAIR_DDL = " FROM File TO Folder,\n FROM File TO CodeElement,\n FROM File TO `Typedef`,\n FROM File TO `Namespace`,\n FROM File TO `Impl`,\n FROM File TO `Static`,\n FROM File TO `Template`,\n FROM File TO Section,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO `Template`,\n FROM Function TO `Namespace`,\n FROM Function TO `Impl`,\n FROM Function TO `Typedef`,\n FROM Function TO CodeElement,\n FROM Class TO `Template`,\n FROM Class TO `Impl`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO CodeElement,\n FROM Method TO `Template`,\n FROM Method TO `Namespace`,\n FROM Method TO `Impl`,\n FROM Method TO CodeElement,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO CodeElement,\n FROM `Module` TO `Namespace`,\n FROM `Namespace` TO Function,\n FROM CodeElement TO CodeElement,\n FROM CodeElement TO Class,\n FROM CodeElement TO Category,\n FROM CodeElement TO Method,\n FROM CodeElement TO `Module`,\n FROM CodeElement TO `Property`,\n FROM Section TO Section,\n FROM Interface TO CodeElement,\n FROM `Namespace` TO `Struct`,\n FROM `Impl` TO Method,\n FROM `Impl` TO Function,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Typedef`,\n FROM `Constructor` TO CodeElement,\n FROM Route TO Process,\n FROM Tool TO Process,\n FROM Destination TO `Property`,\n FROM BasicBlock TO BasicBlock";
100
102
  export declare const RELATION_SCHEMA: string;
101
103
  export declare const EMBEDDING_DIMS: number;
102
104
  /** HNSW vector index name for the CodeEmbedding table. */
@@ -485,7 +485,7 @@ const ATTACHMENT_TARGET_LABELS = [
485
485
  'Record',
486
486
  ];
487
487
  /**
488
- * The 66 pairs NEITHER rule above generates — everything left after the two
488
+ * The pairs NEITHER rule above generates — everything left after the two
489
489
  * cross products are subtracted. Carried by CONTAINMENT, inheritance, imports
490
490
  * and DI: a container label crossed with a contained label. No predicate
491
491
  * describes that surface (any container can hold any definition).
@@ -498,6 +498,8 @@ const ATTACHMENT_TARGET_LABELS = [
498
498
  * templates), the two `Route|Process` / `Tool|Process` entry points whose
499
499
  * emitter names both labels as literals, and `BasicBlock|BasicBlock`, the PDG
500
500
  * substrate.
501
+ * Spring dynamic lookup in a constructor also emits INJECTS to a synthetic
502
+ * @Bean CodeElement (#3238), so Constructor|CodeElement belongs here.
501
503
  *
502
504
  * NOTHING A RULE ALREADY COVERS BELONGS HERE. `generatedRelationPairs` skips
503
505
  * any pair present in this block, so a redundant line does not merely duplicate
@@ -591,6 +593,7 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
591
593
  FROM \`Constructor\` TO \`Impl\`,
592
594
  FROM \`Constructor\` TO \`Namespace\`,
593
595
  FROM \`Constructor\` TO \`Typedef\`,
596
+ FROM \`Constructor\` TO CodeElement,
594
597
  FROM Route TO Process,
595
598
  FROM Tool TO Process,
596
599
  FROM Destination TO \`Property\`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.12-rc.26",
3
+ "version": "1.6.12-rc.27",
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",