gitnexus 1.6.6-rc.80 → 1.6.6-rc.82

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.
@@ -19,9 +19,13 @@ const FASTAPI_VERBS = {
19
19
  delete: 'DELETE',
20
20
  patch: 'PATCH',
21
21
  };
22
- // ─── Provider: FastAPI @app.get/... ──────────────────────────────────
23
- const FASTAPI_PATTERNS = compilePatterns({
24
- name: 'python-fastapi',
22
+ // ─── Provider: FastAPI @app.<verb> / @router.<verb> ──────────────────
23
+ // Two separate patterns so we can tag detections by decorator object.
24
+ // Only `@router.*` detections participate in `include_router(prefix=)`
25
+ // path-prefix joining (see `PythonRepoContext` + `joinPrefix`); `@app.*`
26
+ // routes already carry their final path verbatim.
27
+ const FASTAPI_APP_PATTERNS = compilePatterns({
28
+ name: 'python-fastapi-app',
25
29
  language: Python,
26
30
  patterns: [
27
31
  {
@@ -37,6 +41,133 @@ const FASTAPI_PATTERNS = compilePatterns({
37
41
  },
38
42
  ],
39
43
  });
44
+ const FASTAPI_ROUTER_PATTERNS = compilePatterns({
45
+ name: 'python-fastapi-router',
46
+ language: Python,
47
+ patterns: [
48
+ {
49
+ meta: {},
50
+ query: `
51
+ (decorator
52
+ (call
53
+ function: (attribute
54
+ object: (identifier) @obj (#eq? @obj "router")
55
+ attribute: (identifier) @method (#match? @method "^(get|post|put|delete|patch)$"))
56
+ arguments: (argument_list . (string) @path)))
57
+ `,
58
+ },
59
+ ],
60
+ });
61
+ // ─── include_router(<router_obj>, prefix='/x') across the repo ────────
62
+ // Two shapes are common:
63
+ // app.include_router(assistant.router, prefix='/ai')
64
+ // app.include_router(my_router, prefix='/ai')
65
+ // The first names the originating module via `<module>.router`; the second
66
+ // references a name imported into the host file. We capture both.
67
+ const INCLUDE_ROUTER_ATTR_PATTERNS = compilePatterns({
68
+ name: 'python-fastapi-include-router-attr',
69
+ language: Python,
70
+ patterns: [
71
+ {
72
+ meta: {},
73
+ // Match any `<host>.include_router(<module>.router, ..., prefix='/x')`
74
+ // call. We deliberately do NOT pin `<host>` to the literal name `app`
75
+ // — production code routinely uses `api`, `application`, `asgi_app`,
76
+ // etc. The shape (`include_router` invoked with a router argument and
77
+ // a `prefix=` keyword) is specific enough on its own; restricting the
78
+ // host produces false negatives without removing meaningful false
79
+ // positives.
80
+ query: `
81
+ (call
82
+ function: (attribute
83
+ attribute: (identifier) @incl (#eq? @incl "include_router"))
84
+ arguments: (argument_list
85
+ (attribute
86
+ object: (identifier) @router_module
87
+ attribute: (identifier) @router_attr (#eq? @router_attr "router"))
88
+ (keyword_argument
89
+ name: (identifier) @kw (#eq? @kw "prefix")
90
+ value: (string) @prefix)))
91
+ `,
92
+ },
93
+ ],
94
+ });
95
+ const INCLUDE_ROUTER_NAME_PATTERNS = compilePatterns({
96
+ name: 'python-fastapi-include-router-name',
97
+ language: Python,
98
+ patterns: [
99
+ {
100
+ meta: {},
101
+ // Same `<host>` rationale as INCLUDE_ROUTER_ATTR_PATTERNS — see above.
102
+ query: `
103
+ (call
104
+ function: (attribute
105
+ attribute: (identifier) @incl (#eq? @incl "include_router"))
106
+ arguments: (argument_list
107
+ (identifier) @router_name
108
+ (keyword_argument
109
+ name: (identifier) @kw (#eq? @kw "prefix")
110
+ value: (string) @prefix)))
111
+ `,
112
+ },
113
+ ],
114
+ });
115
+ // `from .api.assistant import router` style — used together with
116
+ // INCLUDE_ROUTER_NAME so we can map a local name back to its module
117
+ // path, then back to the file the router was declared in.
118
+ const FROM_IMPORT_ROUTER_PATTERNS = compilePatterns({
119
+ name: 'python-fastapi-from-import-router',
120
+ language: Python,
121
+ patterns: [
122
+ {
123
+ meta: {},
124
+ query: `
125
+ (import_from_statement
126
+ module_name: (_) @module
127
+ name: (dotted_name (identifier) @imported (#eq? @imported "router")))
128
+ `,
129
+ },
130
+ {
131
+ meta: {},
132
+ query: `
133
+ (import_from_statement
134
+ module_name: (_) @module
135
+ name: (aliased_import
136
+ name: (dotted_name (identifier) @imported (#eq? @imported "router"))
137
+ alias: (identifier) @alias))
138
+ `,
139
+ },
140
+ ],
141
+ });
142
+ // `from api import users` / `from api import users as u` — module-level
143
+ // imports where the imported name is itself the module that owns
144
+ // `<name>.router`. Lets Shape A (`<host>.include_router(<name>.router, …)`)
145
+ // look up the full package path of `<name>` and pin the prefix onto the
146
+ // exact file (`api/users.py`) rather than every file basenamed `users.py`.
147
+ const FROM_IMPORT_MODULE_PATTERNS = compilePatterns({
148
+ name: 'python-fastapi-from-import-module',
149
+ language: Python,
150
+ patterns: [
151
+ {
152
+ meta: {},
153
+ query: `
154
+ (import_from_statement
155
+ module_name: (_) @module
156
+ name: (dotted_name (identifier) @imported))
157
+ `,
158
+ },
159
+ {
160
+ meta: {},
161
+ query: `
162
+ (import_from_statement
163
+ module_name: (_) @module
164
+ name: (aliased_import
165
+ name: (dotted_name (identifier) @imported)
166
+ alias: (identifier) @alias))
167
+ `,
168
+ },
169
+ ],
170
+ });
40
171
  // ─── Consumer: requests.get/post/... ──────────────────────────────────
41
172
  const REQUESTS_VERB_PATTERNS = compilePatterns({
42
173
  name: 'python-requests-verb',
@@ -405,14 +536,183 @@ const HTTPX_ASYNC_CLIENT_GENERIC_PATTERNS = compilePatterns({
405
536
  },
406
537
  ],
407
538
  });
539
+ /** Strip `.py` and return the bare basename (e.g. `api/users.py` → `users`). */
540
+ function fileShortKey(rel) {
541
+ const slash = rel.lastIndexOf('/');
542
+ const file = slash >= 0 ? rel.slice(slash + 1) : rel;
543
+ return file.endsWith('.py') ? file.slice(0, -3) : file;
544
+ }
545
+ /**
546
+ * Long key for a `.py` file: parent directory + stem, joined with `/`.
547
+ * Files at the repo root return the empty string (no parent), in which
548
+ * case callers should fall back to the short key.
549
+ */
550
+ function fileLongKey(rel) {
551
+ const noExt = rel.endsWith('.py') ? rel.slice(0, -3) : rel;
552
+ const lastSlash = noExt.lastIndexOf('/');
553
+ if (lastSlash < 0)
554
+ return '';
555
+ const beforeLast = noExt.slice(0, lastSlash);
556
+ const stem = noExt.slice(lastSlash + 1);
557
+ const prevSlash = beforeLast.lastIndexOf('/');
558
+ const parent = prevSlash >= 0 ? beforeLast.slice(prevSlash + 1) : beforeLast;
559
+ return `${parent}/${stem}`;
560
+ }
561
+ /** Last `.`-separated segment of a (possibly relative) module path. */
562
+ function lastSegmentOfDotted(text) {
563
+ const stripped = text.replace(/^\.+/, '');
564
+ if (!stripped)
565
+ return '';
566
+ const dot = stripped.lastIndexOf('.');
567
+ return dot >= 0 ? stripped.slice(dot + 1) : stripped;
568
+ }
569
+ /**
570
+ * Last two `.`-separated segments of a (possibly relative) module path
571
+ * joined with `/`, e.g. `api.users` → `api/users`. Single-segment paths
572
+ * and pure-dot inputs return the empty string; callers should fall back
573
+ * to the short key in that case.
574
+ */
575
+ function lastTwoSegmentsAsLongKey(text) {
576
+ const stripped = text.replace(/^\.+/, '');
577
+ if (!stripped)
578
+ return '';
579
+ const last = stripped.lastIndexOf('.');
580
+ if (last <= 0)
581
+ return '';
582
+ const beforeLast = stripped.slice(0, last);
583
+ const stem = stripped.slice(last + 1);
584
+ const prev = beforeLast.lastIndexOf('.');
585
+ const parent = prev >= 0 ? beforeLast.slice(prev + 1) : beforeLast;
586
+ return `${parent}/${stem}`;
587
+ }
588
+ function recordPrefix(target, key, prefix) {
589
+ const set = target.get(key) ?? new Set();
590
+ set.add(prefix);
591
+ target.set(key, set);
592
+ }
593
+ function buildPythonRepoContext(files, parser, readFile, parseSource) {
594
+ const prefixesByLongKey = new Map();
595
+ const prefixesByShortKey = new Map();
596
+ // Pre-pass over .py files. We deliberately run this even on files
597
+ // that don't contain `include_router` — the cost of an extra parse
598
+ // is bounded by the file count, and detecting `include_router`
599
+ // beforehand would require its own grep/scan.
600
+ for (const rel of files) {
601
+ if (!rel.endsWith('.py'))
602
+ continue;
603
+ const src = readFile(rel);
604
+ if (!src)
605
+ continue;
606
+ if (!src.includes('include_router'))
607
+ continue;
608
+ parser.setLanguage(Python);
609
+ const tree = parseSource(parser, src);
610
+ if (!tree)
611
+ continue;
612
+ const localNameToModule = new Map();
613
+ for (const m of runCompiledPatterns(FROM_IMPORT_ROUTER_PATTERNS, tree)) {
614
+ const moduleNode = m.captures.module;
615
+ const aliasNode = m.captures.alias;
616
+ const importedNode = m.captures.imported;
617
+ if (!moduleNode || !importedNode)
618
+ continue;
619
+ const localName = aliasNode?.text ?? importedNode.text;
620
+ const moduleShort = lastSegmentOfDotted(moduleNode.text);
621
+ if (!moduleShort)
622
+ continue;
623
+ const moduleLong = lastTwoSegmentsAsLongKey(moduleNode.text);
624
+ localNameToModule.set(localName, { moduleShort, moduleLong });
625
+ }
626
+ // Module-alias map: name imported from a multi-segment package →
627
+ // long key. Lets Shape A look up the precise file for `<name>.router`
628
+ // even when `<name>` collides with another package's basename.
629
+ const localNameToModuleAlias = new Map();
630
+ for (const m of runCompiledPatterns(FROM_IMPORT_MODULE_PATTERNS, tree)) {
631
+ const moduleNode = m.captures.module;
632
+ const importedNode = m.captures.imported;
633
+ const aliasNode = m.captures.alias;
634
+ if (!moduleNode || !importedNode)
635
+ continue;
636
+ // Skip the `router` shape — already handled by FROM_IMPORT_ROUTER_PATTERNS
637
+ // above and stored under its router-aware semantics.
638
+ if (importedNode.text === 'router')
639
+ continue;
640
+ const moduleLong = lastTwoSegmentsAsLongKey(`${moduleNode.text}.${importedNode.text}`);
641
+ if (!moduleLong)
642
+ continue;
643
+ const localName = aliasNode?.text ?? importedNode.text;
644
+ localNameToModuleAlias.set(localName, moduleLong);
645
+ }
646
+ // Shape A: `<host>.include_router(<module>.router, prefix='/x')`.
647
+ // The call site gives us only a short module name. We promote to a
648
+ // long key when the same file imports `<module>` via either
649
+ // `from <pkg> import <module>` (recorded in `localNameToModuleAlias`
650
+ // — the typical pattern) or, less commonly, a router-aware import
651
+ // statement. Only fall back to the basename short key when neither
652
+ // alias is available.
653
+ for (const m of runCompiledPatterns(INCLUDE_ROUTER_ATTR_PATTERNS, tree)) {
654
+ const modNode = m.captures.router_module;
655
+ const prefixNode = m.captures.prefix;
656
+ if (!modNode || !prefixNode)
657
+ continue;
658
+ const prefix = unquoteLiteral(prefixNode.text);
659
+ if (prefix === null)
660
+ continue;
661
+ const moduleShort = modNode.text;
662
+ const aliasLong = localNameToModuleAlias.get(moduleShort);
663
+ const sameFileImport = localNameToModule.get(moduleShort);
664
+ const longKey = aliasLong ?? sameFileImport?.moduleLong;
665
+ if (longKey) {
666
+ recordPrefix(prefixesByLongKey, longKey, prefix);
667
+ }
668
+ else {
669
+ recordPrefix(prefixesByShortKey, moduleShort, prefix);
670
+ }
671
+ }
672
+ // Shape B: `<host>.include_router(my_router, prefix='/x')` — resolve
673
+ // `my_router` via the import map built above. Whenever the import
674
+ // statement supplied a multi-segment module path the long key is
675
+ // recorded, eliminating cross-package collisions.
676
+ for (const m of runCompiledPatterns(INCLUDE_ROUTER_NAME_PATTERNS, tree)) {
677
+ const nameNode = m.captures.router_name;
678
+ const prefixNode = m.captures.prefix;
679
+ if (!nameNode || !prefixNode)
680
+ continue;
681
+ const localImp = localNameToModule.get(nameNode.text);
682
+ if (!localImp)
683
+ continue;
684
+ const prefix = unquoteLiteral(prefixNode.text);
685
+ if (prefix === null)
686
+ continue;
687
+ if (localImp.moduleLong) {
688
+ recordPrefix(prefixesByLongKey, localImp.moduleLong, prefix);
689
+ }
690
+ else {
691
+ recordPrefix(prefixesByShortKey, localImp.moduleShort, prefix);
692
+ }
693
+ }
694
+ }
695
+ return { prefixesByLongKey, prefixesByShortKey };
696
+ }
697
+ function joinPrefix(prefix, route) {
698
+ // Mirror FastAPI's path joining: trim trailing slash off prefix,
699
+ // ensure exactly one leading slash on the result.
700
+ const p = prefix.replace(/\/+$/, '');
701
+ const r = route.startsWith('/') ? route : `/${route}`;
702
+ return `${p}${r}`;
703
+ }
408
704
  export const PYTHON_HTTP_PLUGIN = {
409
705
  name: 'python-http',
410
706
  language: Python,
411
- scan(tree) {
707
+ prepareRepo({ files, parser, readFile, parseSource }) {
708
+ return buildPythonRepoContext(files, parser, readFile, parseSource);
709
+ },
710
+ scan(tree, repoContext, fileRel) {
412
711
  const out = [];
413
712
  const httpxAsyncClients = collectHttpxAsyncClients(tree);
414
- // Providers: FastAPI
415
- for (const match of runCompiledPatterns(FASTAPI_PATTERNS, tree)) {
713
+ const ctx = repoContext;
714
+ // Providers: FastAPI @app.<verb>("/path") already absolute path.
715
+ for (const match of runCompiledPatterns(FASTAPI_APP_PATTERNS, tree)) {
416
716
  const methodNode = match.captures.method;
417
717
  const pathNode = match.captures.path;
418
718
  if (!methodNode || !pathNode)
@@ -432,6 +732,45 @@ export const PYTHON_HTTP_PLUGIN = {
432
732
  confidence: 0.8,
433
733
  });
434
734
  }
735
+ // Providers: FastAPI @router.<verb>("/path") — must be joined
736
+ // with the prefix(es) declared at the include_router site. When
737
+ // no prefix is found we still emit the unprefixed path so this
738
+ // change is strictly additive vs. the prior @app-only behaviour;
739
+ // when the same router is mounted under multiple prefixes we emit
740
+ // one detection per prefix.
741
+ for (const match of runCompiledPatterns(FASTAPI_ROUTER_PATTERNS, tree)) {
742
+ const methodNode = match.captures.method;
743
+ const pathNode = match.captures.path;
744
+ if (!methodNode || !pathNode)
745
+ continue;
746
+ const httpMethod = FASTAPI_VERBS[methodNode.text];
747
+ if (!httpMethod)
748
+ continue;
749
+ const rawPath = unquoteLiteral(pathNode.text);
750
+ if (rawPath === null)
751
+ continue;
752
+ // Long key first (precise, package-aware), short key as fallback.
753
+ // Mirrors the ingestion-side resolution in parse-impl.ts so the
754
+ // graph nodes and group contracts agree on which prefix applies.
755
+ const longKey = fileRel ? fileLongKey(fileRel) : '';
756
+ const longPrefixes = longKey ? ctx?.prefixesByLongKey.get(longKey) : undefined;
757
+ const shortKey = fileRel ? fileShortKey(fileRel) : '';
758
+ const shortPrefixes = longPrefixes || !shortKey ? undefined : ctx?.prefixesByShortKey.get(shortKey);
759
+ const prefixSet = longPrefixes ?? shortPrefixes;
760
+ const paths = prefixSet && prefixSet.size > 0
761
+ ? [...prefixSet].map((p) => joinPrefix(p, rawPath))
762
+ : [rawPath];
763
+ for (const p of paths) {
764
+ out.push({
765
+ role: 'provider',
766
+ framework: 'fastapi',
767
+ method: httpMethod,
768
+ path: p,
769
+ name: null,
770
+ confidence: 0.8,
771
+ });
772
+ }
773
+ }
435
774
  // Consumers: requests.<verb>
436
775
  for (const match of runCompiledPatterns(REQUESTS_VERB_PATTERNS, tree)) {
437
776
  const methodNode = match.captures.method;
@@ -47,15 +47,47 @@ export interface HttpDetection {
47
47
  * `LanguagePatterns.language` in `tree-sitter-scanner.ts` — the
48
48
  * grammar modules export different shapes.
49
49
  */
50
+ /**
51
+ * Per-repo state a plugin can build during a `prepareRepo` pass before
52
+ * any per-file `scan` is invoked. The orchestrator threads this opaque
53
+ * value back into each `scan` call so plugins can resolve cross-file
54
+ * facts (e.g. FastAPI `app.include_router(prefix=...)` mappings live
55
+ * in `main.py` but apply to handlers declared in `api/*.py`).
56
+ *
57
+ * Plugins that have no cross-file state can omit `prepareRepo` and
58
+ * receive `undefined`.
59
+ */
60
+ export type RepoContext = unknown;
50
61
  export interface HttpLanguagePlugin {
51
62
  /** Human-readable plugin name for diagnostics. */
52
63
  name: string;
53
64
  /** tree-sitter grammar object (passed to the shared parser). */
54
65
  language: unknown;
66
+ /**
67
+ * Optional pre-pass: walk the relevant files in the repo and produce
68
+ * an opaque context that `scan` can use to resolve cross-file facts.
69
+ * Implementations must not throw — return undefined on any error so
70
+ * the orchestrator falls back to context-less scanning.
71
+ */
72
+ prepareRepo?(args: {
73
+ repoPath: string;
74
+ files: string[];
75
+ parser: Parser;
76
+ readFile: (rel: string) => string | null;
77
+ parseSource: (parser: Parser, src: string) => Parser.Tree | null;
78
+ }): RepoContext | undefined;
55
79
  /**
56
80
  * Scan a parsed tree and return zero or more HTTP detections. Plugins
57
81
  * must not throw — they should swallow per-match errors so a single
58
82
  * malformed construct does not abort the whole file.
83
+ *
84
+ * `repoContext` is whatever the plugin's `prepareRepo` produced (or
85
+ * `undefined` if there is no `prepareRepo`).
86
+ *
87
+ * `fileRel` is the repo-relative path of the file being scanned;
88
+ * plugins that resolve cross-file facts (e.g. FastAPI router prefix
89
+ * joining) need it to key into `repoContext`. Optional so existing
90
+ * single-file plugins can keep their unary `scan(tree)` shape.
59
91
  */
60
- scan(tree: Parser.Tree): HttpDetection[];
92
+ scan(tree: Parser.Tree, repoContext?: RepoContext, fileRel?: string): HttpDetection[];
61
93
  }
@@ -141,7 +141,35 @@ export class HttpRouteExtractor {
141
141
  // both graph-assisted enrichment and source-scan emission.
142
142
  const parser = new Parser();
143
143
  const cachedDetections = new Map();
144
- const getDetections = (rel) => {
144
+ // Per-plugin cross-file context (e.g. Python's FastAPI router →
145
+ // include_router(prefix=...) map). Built lazily on first
146
+ // `getDetections` call for a file the plugin handles, scoped to the
147
+ // file list returned by `getScannedFiles`. Stored by plugin name so
148
+ // a repo with multiple languages keeps each plugin's context
149
+ // independent.
150
+ const repoContextByPlugin = new Map();
151
+ const ensureRepoContext = async (plugin) => {
152
+ if (!plugin || typeof plugin.prepareRepo !== 'function')
153
+ return undefined;
154
+ if (repoContextByPlugin.has(plugin.name))
155
+ return repoContextByPlugin.get(plugin.name);
156
+ try {
157
+ const ctx = plugin.prepareRepo({
158
+ repoPath,
159
+ files: await getScannedFiles(),
160
+ parser,
161
+ readFile: (rel) => readSafe(repoPath, rel),
162
+ parseSource: (p, src) => parseSourceSafe(p, src),
163
+ });
164
+ repoContextByPlugin.set(plugin.name, ctx);
165
+ return ctx;
166
+ }
167
+ catch {
168
+ repoContextByPlugin.set(plugin.name, undefined);
169
+ return undefined;
170
+ }
171
+ };
172
+ const getDetections = async (rel) => {
145
173
  const cached = cachedDetections.get(rel);
146
174
  if (cached)
147
175
  return cached;
@@ -150,6 +178,7 @@ export class HttpRouteExtractor {
150
178
  cachedDetections.set(rel, []);
151
179
  return [];
152
180
  }
181
+ const repoContext = await ensureRepoContext(plugin);
153
182
  const content = readSafe(repoPath, rel);
154
183
  if (!content) {
155
184
  cachedDetections.set(rel, []);
@@ -158,7 +187,7 @@ export class HttpRouteExtractor {
158
187
  try {
159
188
  parser.setLanguage(plugin.language);
160
189
  const tree = parseSourceSafe(parser, content);
161
- const detections = plugin.scan(tree);
190
+ const detections = plugin.scan(tree, repoContext, rel);
162
191
  cachedDetections.set(rel, detections);
163
192
  return detections;
164
193
  }
@@ -179,9 +208,9 @@ export class HttpRouteExtractor {
179
208
  const graphProviders = dbExecutor != null ? await this.extractProvidersGraph(dbExecutor, getDetections) : [];
180
209
  // Source scan always runs to capture routes in languages/files not covered
181
210
  // by graph edges; the glob and per-file parse results are cached above.
182
- const providers = this.mergeGraphAndSourceContracts(graphProviders, this.extractProvidersSourceScan(await getScannedFiles(), getDetections));
211
+ const providers = this.mergeGraphAndSourceContracts(graphProviders, await this.extractProvidersSourceScan(await getScannedFiles(), getDetections));
183
212
  const graphConsumers = dbExecutor != null ? await this.extractConsumersGraph(dbExecutor, getDetections) : [];
184
- const consumers = this.mergeGraphAndSourceContracts(graphConsumers, this.extractConsumersSourceScan(await getScannedFiles(), getDetections));
213
+ const consumers = this.mergeGraphAndSourceContracts(graphConsumers, await this.extractConsumersSourceScan(await getScannedFiles(), getDetections));
185
214
  return [...providers, ...consumers];
186
215
  }
187
216
  async scanFiles(repoPath) {
@@ -219,7 +248,7 @@ export class HttpRouteExtractor {
219
248
  // helpers — tree-sitter gives both pieces of information
220
249
  // structurally. Always run the lookup: even when method is set by
221
250
  // `methodFromRouteReason`, we still need the handler name.
222
- const detections = filePath ? getDetections(filePath) : [];
251
+ const detections = filePath ? await getDetections(filePath) : [];
223
252
  const providerDetections = detections.filter((d) => d.role === 'provider');
224
253
  let handlerName = null;
225
254
  const normalizedRoute = normalizeHttpPath(routePath);
@@ -293,10 +322,10 @@ export class HttpRouteExtractor {
293
322
  return out;
294
323
  }
295
324
  // ─── Source-scan providers ─────────────────────────────────────────
296
- extractProvidersSourceScan(files, getDetections) {
325
+ async extractProvidersSourceScan(files, getDetections) {
297
326
  const out = [];
298
327
  for (const rel of files) {
299
- const detections = getDetections(rel);
328
+ const detections = await getDetections(rel);
300
329
  for (const d of detections) {
301
330
  if (d.role !== 'provider')
302
331
  continue;
@@ -338,7 +367,7 @@ export class HttpRouteExtractor {
338
367
  let method = 'GET';
339
368
  // Prefer the plugin's detected method if we can find a matching
340
369
  // fetch/axios call in the same file.
341
- const detections = filePath ? getDetections(filePath) : [];
370
+ const detections = filePath ? await getDetections(filePath) : [];
342
371
  // Symmetric to the provider path: if multiple consumer calls in
343
372
  // the same file share the same normalized path (e.g. a GET
344
373
  // fetch AND a POST fetch to `/api/orders`), `.find()` silently
@@ -388,10 +417,10 @@ export class HttpRouteExtractor {
388
417
  return out;
389
418
  }
390
419
  // ─── Source-scan consumers ─────────────────────────────────────────
391
- extractConsumersSourceScan(files, getDetections) {
420
+ async extractConsumersSourceScan(files, getDetections) {
392
421
  const out = [];
393
422
  for (const rel of files) {
394
- const detections = getDetections(rel);
423
+ const detections = await getDetections(rel);
395
424
  for (const d of detections) {
396
425
  if (d.role !== 'consumer')
397
426
  continue;
@@ -24,22 +24,18 @@
24
24
  * V2 additionally walks class ancestors (via MRO), so base-class enclosing
25
25
  * namespaces also contribute associated namespaces.
26
26
  *
27
- * **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
28
- * function reference like `utils::worker` contributes `utils` to the associated
29
- * set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
30
- * to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
31
- * entities for function-type arguments come from the **parameter types and return
32
- * type** of each function in the overload set — NOT the function's enclosing
33
- * namespace. For `void worker()`, the standard-compliant associated set is empty.
34
- * GitNexus instead contributes the enclosing namespace of any Function/Method
35
- * def whose simple name matches, because it enables the dominant real-world ADL
36
- * pattern at reasonable precision cost.
37
- *
38
- * For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
39
- * workspace lookup (only contributed when a Function/Method named `worker` exists
40
- * in `utils`). For unqualified refs the workspace is searched for any Function
41
- * def with that simple name. Locally-declared function-pointer variables
42
- * (e.g. `void (*g)()`) and function parameters are excluded from this path.
27
+ * Function-reference arguments follow ISO C++ `[basic.lookup.argdep]`:
28
+ * associated entities come from the parameter types and return type of each
29
+ * referenced function in the overload set, not from the function's enclosing
30
+ * namespace. For `void worker()`, the associated set is empty. For
31
+ * `void worker(api::Token)` or `api::Token make_token()`, `api` is associated
32
+ * through `Token`.
33
+ *
34
+ * For qualified refs (e.g. `utils::worker`) the workspace lookup is restricted
35
+ * to functions/methods named `worker` in `utils`; for unqualified refs the
36
+ * workspace is searched for matching functions/methods by simple name. Locally
37
+ * declared function-pointer variables and function parameters are excluded
38
+ * from this path.
43
39
  *
44
40
  * ADL candidates are merged with ordinary unqualified-lookup candidates
45
41
  * in the free-call fallback before overload narrowing.
@@ -94,11 +90,8 @@ export interface CppAdlArgInfo {
94
90
  /** When set, the arg is a potential free-function reference (not a locally-
95
91
  * declared function-pointer variable or function parameter). Contains the
96
92
  * identifier text as written in source (e.g. `"utils::worker"` or
97
- * `"worker"`). GitNexus approximation: the function's enclosing namespace
98
- * is contributed to the ADL associated set. For qualified refs a workspace
99
- * lookup confirms a Function/Method with that simple name exists in the
100
- * namespace before contributing; for unqualified refs every namespace
101
- * containing a matching Function/Method def is contributed. */
93
+ * `"worker"`). Resolution contributes associated namespaces from each
94
+ * referenced Function/Method def's parameter and return types. */
102
95
  readonly functionRefText?: string;
103
96
  }
104
97
  /** Record per-call-site argument info. Called once per call site from