gitnexus 1.6.11-rc.36 → 1.6.11-rc.37

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,7 +3,7 @@ import JavaScript from 'tree-sitter-javascript';
3
3
  import TypeScript from 'tree-sitter-typescript';
4
4
  import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
5
5
  import { MAX_FOLD_LENGTH } from '../../../ingestion/route-extractors/constant-resolver.js';
6
- import { DATA_ROUTE_TABLE_SOURCE, scanDataRouteTables, } from '../../../ingestion/route-extractors/data-route-table.js';
6
+ import { DATA_ROUTE_TABLE_SOURCE, propertyName, scanDataRouteTables, } from '../../../ingestion/route-extractors/data-route-table.js';
7
7
  import { extractNestRoutes } from '../../../ingestion/route-extractors/nest.js';
8
8
  import { normalizeExtractedRoutePath } from '../../../ingestion/route-extractors/route-path.js';
9
9
  import { buildJsRepoFacts, extractJsModuleFacts, isAxiosNamespace, isHttpClientRef, resolveJsPathExpression, } from '../../../ingestion/route-extractors/js-const-resolver.js';
@@ -123,6 +123,35 @@ const AXIOS_OBJECT_SPEC = {
123
123
  arguments: (arguments (object) @options))
124
124
  `,
125
125
  };
126
+ // ─── Consumer: wrapped client X.request({ url, method }) ────────────
127
+ // Enterprise wrapper shape: an axios instance (or a named request helper)
128
+ // re-exported under a local name — `httpClient.request({ url, method })`
129
+ // from `@winex-plugin/win-request`, `$http.request(...)`. Generic names
130
+ // like `api` need axios.create/import proof (`isHttpClientRef`); spelling
131
+ // alone is too common (graphql-request helpers, domain `api` objects).
132
+ // The member property is `request` (not an HTTP verb), so this cannot
133
+ // collide with the Express provider pattern (`router.get`) or the axios
134
+ // member form (`axios.get`). Option keys are resolved programmatically,
135
+ // same as the jQuery ajax / axios object forms.
136
+ //
137
+ // The query captures the receiver so scan can reject unrelated
138
+ // `.request({ url })` APIs (`cy.request`, `queue.request`).
139
+ const REQUEST_OBJECT_SPEC = {
140
+ meta: {},
141
+ query: `
142
+ (call_expression
143
+ function: (member_expression
144
+ object: (_) @obj
145
+ property: (property_identifier) @fn (#eq? @fn "request"))
146
+ arguments: (arguments . (object) @options))
147
+ `,
148
+ };
149
+ /**
150
+ * Receivers admitted as wrapped HTTP clients without axios.create proof.
151
+ * Spelling-only: the last identifier in `obj.text` (`this.$http` → `$http`).
152
+ * Keep this set small — every extra name is a false-positive surface.
153
+ */
154
+ const WRAPPED_REQUEST_RECEIVERS = new Set(['httpClient', '$http']);
126
155
  function compileBundle(language, name) {
127
156
  const mk = (spec, suffix) => compilePatterns({
128
157
  name: `${name}-${suffix}`,
@@ -137,6 +166,7 @@ function compileBundle(language, name) {
137
166
  jqueryShorthand: mk(JQUERY_SHORTHAND_SPEC, 'jquery-shorthand'),
138
167
  jqueryAjax: mk(JQUERY_AJAX_SPEC, 'jquery-ajax'),
139
168
  axiosObject: mk(AXIOS_OBJECT_SPEC, 'axios-object'),
169
+ requestObject: mk(REQUEST_OBJECT_SPEC, 'request-object'),
140
170
  };
141
171
  }
142
172
  const JAVASCRIPT_BUNDLE = compileBundle(JavaScript, 'javascript-http');
@@ -148,6 +178,7 @@ const TSX_BUNDLE = compileBundle(TypeScript.tsx, 'tsx-http');
148
178
  * of `keyNames`. Returns null when no matching pair is present or the
149
179
  * value is not a string literal. Used by the jQuery ajax / axios object
150
180
  * consumers to resolve `url` / `method` / `type` keys in any order.
181
+ * Keys use shared `propertyName` so quoted `"method"` matches `method`.
151
182
  */
152
183
  function readStringProp(objectNode, keyNames) {
153
184
  for (let i = 0; i < objectNode.namedChildCount; i++) {
@@ -158,7 +189,8 @@ function readStringProp(objectNode, keyNames) {
158
189
  const valueNode = pair.childForFieldName('value');
159
190
  if (!keyNode || !valueNode)
160
191
  continue;
161
- if (!keyNames.includes(keyNode.text))
192
+ const key = propertyName(keyNode);
193
+ if (key === null || !keyNames.includes(key))
162
194
  continue;
163
195
  if (valueNode.type !== 'string' && valueNode.type !== 'template_string')
164
196
  continue;
@@ -168,6 +200,77 @@ function readStringProp(objectNode, keyNames) {
168
200
  }
169
201
  return null;
170
202
  }
203
+ /**
204
+ * Verb for wrapped `X.request({ url, method|type })`. Absent key → GET
205
+ * (same default as fetch-without-options / jQuery ajax). Present but not a
206
+ * string/template, supplied only via object spread, or later overwritten by
207
+ * a duplicate key / spread → `*` so matching can still link without pinning GET.
208
+ * Later properties win, matching JavaScript object-literal evaluation.
209
+ */
210
+ function readRequestMethod(objectNode, keyNames = ['method', 'type']) {
211
+ let last = { kind: 'absent' };
212
+ for (let i = 0; i < objectNode.namedChildCount; i++) {
213
+ const child = objectNode.namedChild(i);
214
+ if (!child)
215
+ continue;
216
+ if (child.type === 'spread_element') {
217
+ last = { kind: 'unknown' };
218
+ continue;
219
+ }
220
+ if (child.type === 'shorthand_property_identifier' ||
221
+ child.type === 'shorthand_property_identifier_pattern') {
222
+ if (keyNames.includes(child.text))
223
+ last = { kind: 'unknown' };
224
+ continue;
225
+ }
226
+ if (child.type !== 'pair')
227
+ continue;
228
+ const keyNode = child.childForFieldName('key');
229
+ const valueNode = child.childForFieldName('value');
230
+ if (!keyNode)
231
+ continue;
232
+ const key = propertyName(keyNode);
233
+ if (key === null || !keyNames.includes(key))
234
+ continue;
235
+ if (!valueNode || (valueNode.type !== 'string' && valueNode.type !== 'template_string')) {
236
+ last = { kind: 'unknown' };
237
+ continue;
238
+ }
239
+ const lit = unquoteLiteral(valueNode.text);
240
+ if (lit === null || lit.includes('${')) {
241
+ last = { kind: 'unknown' };
242
+ continue;
243
+ }
244
+ last = { kind: 'literal', value: lit };
245
+ }
246
+ if (last.kind === 'literal')
247
+ return last.value.toUpperCase();
248
+ if (last.kind === 'unknown')
249
+ return '*';
250
+ return 'GET';
251
+ }
252
+ function wrappedRequestReceiverName(receiver) {
253
+ const parts = receiver.split('.');
254
+ return parts[parts.length - 1] ?? receiver;
255
+ }
256
+ /** Axios module / axios.create instance, or a registered wrapper identifier. */
257
+ function isAdmittedWrappedRequestReceiver(receiver, fileKey, facts) {
258
+ if (WRAPPED_REQUEST_RECEIVERS.has(wrappedRequestReceiverName(receiver)))
259
+ return true;
260
+ try {
261
+ const isModule = facts === null || fileKey === undefined
262
+ ? receiver === 'axios'
263
+ : isAxiosNamespace(fileKey, receiver, facts);
264
+ if (isModule)
265
+ return true;
266
+ if (!facts || fileKey === undefined)
267
+ return false;
268
+ return isHttpClientRef(fileKey, receiver, facts);
269
+ }
270
+ catch {
271
+ return false;
272
+ }
273
+ }
171
274
  /**
172
275
  * Map each named import's LOCAL binding to its DECLARED export name and source
173
276
  * module, by walking the file's `import { x as y } from 'm'` statements. Lets
@@ -335,9 +438,10 @@ function resolveFactsFor(repoContext, fileRel) {
335
438
  *
336
439
  * `/{param}` matches every one-segment provider route in the group, and
337
440
  * `matching.exclude_links_param_only_paths` defaults to `false`. A path whose
338
- * leading term is an unresolved placeholder is refused for the same reason —
339
- * nothing pins where it starts. (`resolveJsPathExpression` already refuses those
340
- * it folded itself; this also covers the literal fallback below.)
441
+ * leading term is an unresolved placeholder is refused unless the next
442
+ * character is `/` — that is the gateway-prefix shape
443
+ * `` `${serviceClient}/api/v1/x` `` that `stripLeadingTemplatePrefix` keeps.
444
+ * Bare `{param}` and `{param}api/x` stay rejected: nothing pins a route.
341
445
  */
342
446
  function looksLikeHttpPath(path) {
343
447
  if (path === '')
@@ -352,7 +456,7 @@ function looksLikeHttpPath(path) {
352
456
  const shape = path.replace(/\$\{[^}]+\}/g, '{param}');
353
457
  if (/\s/.test(shape))
354
458
  return false;
355
- if (shape.startsWith('{param}'))
459
+ if (shape.startsWith('{param}') && !shape.startsWith('{param}/'))
356
460
  return false;
357
461
  // An all-digit string is a path only when it is written as one. A leading
358
462
  // slash is that evidence: `client.get('/123')` is a route whose segment the
@@ -613,8 +717,7 @@ function scanBundle(bundle, tree, repoContext, fileRel) {
613
717
  const path = readStringProp(optionsNode, ['url']);
614
718
  if (path === null)
615
719
  continue;
616
- const rawMethod = readStringProp(optionsNode, ['method']);
617
- const method = (rawMethod ?? 'GET').toUpperCase();
720
+ const method = readRequestMethod(optionsNode, ['method']);
618
721
  out.push({
619
722
  role: 'consumer',
620
723
  framework: 'axios',
@@ -625,6 +728,40 @@ function scanBundle(bundle, tree, repoContext, fileRel) {
625
728
  confidence: 0.7,
626
729
  });
627
730
  }
731
+ // Consumer: wrapped client `X.request({ url, method })` — the shared
732
+ // enterprise axios-instance shape (`httpClient.request` from
733
+ // win-request and friends). Emit the raw url (templates intact) so
734
+ // shared `normalizeConsumerPath` can strip a leading `${…}` gateway
735
+ // prefix and fold mid/tail interpolations to `{param}`. A plugin-side
736
+ // longest-slash-segment reducer would truncate those mid-templates
737
+ // before the shared normalizer ever saw them. This scan drops only
738
+ // static relative urls (no `${`, no leading `/`, not `https?://`).
739
+ // That filter is request-wrapper-specific: fetch/axios member forms
740
+ // already admit absolute urls and leave host stripping to
741
+ // `normalizeConsumerPath`.
742
+ for (const match of runCompiledPatterns(bundle.requestObject, tree)) {
743
+ const optionsNode = match.captures.options;
744
+ const objNode = match.captures.obj;
745
+ if (!optionsNode || !objNode)
746
+ continue;
747
+ if (!isAdmittedWrappedRequestReceiver(objNode.text, fileKey, facts))
748
+ continue;
749
+ const rawUrl = readStringProp(optionsNode, ['url']);
750
+ if (rawUrl === null)
751
+ continue;
752
+ const url = rawUrl.trim();
753
+ if (!url.includes('${') && !url.startsWith('/') && !/^https?:\/\//i.test(url))
754
+ continue;
755
+ out.push({
756
+ role: 'consumer',
757
+ framework: 'request',
758
+ method: readRequestMethod(optionsNode),
759
+ path: url,
760
+ name: null,
761
+ line: optionsNode.startPosition.row + 1,
762
+ confidence: 0.65,
763
+ });
764
+ }
628
765
  for (const route of scanDataRouteTables(tree)) {
629
766
  const imported = route.handlerLocalName === undefined ? undefined : importMap.get(route.handlerLocalName);
630
767
  out.push({
@@ -261,13 +261,56 @@ export function normalizeHttpPath(p) {
261
261
  return s === '' ? '/' : s;
262
262
  }
263
263
  /**
264
- * Consumer-side normalization is more aggressive:
265
- * - template literals (`${x}`) → `{param}`
266
- * - strip protocol + host if the URL is absolute
267
- * - numeric segments → `{param}` (so `/api/orders/42` → `/api/orders/{param}`)
264
+ * Strip LEADING template interpolations from a consumer url as gateway/host
265
+ * bindings — the enterprise wrapper shape `` `${serviceClient}/api/v1/x` ``
266
+ * where `${serviceClient}` selects the gateway service, not a route segment.
267
+ * This is consumer-path framework semantics (mirrors how an absolute
268
+ * `https://host/path` url keeps only its path), so it lives here rather than
269
+ * in any one language plugin:
270
+ * - `` `${c}/api/x` `` → `/api/x` (clean prefix; `${c}${d}/api/x` → `/api/x`)
271
+ * - `` `${c}/api/x/${id}` ``→ `/api/x/${id}` (mid/tail interpolations are left for the `{param}` pass)
272
+ * Returns null when the stripped remainder is not a single-slash path:
273
+ * - remainder without `/` — relative fragment (`${c}api/x`) or scheme/host
274
+ * (`${scheme}://${host}/api/x`); whether it is a path depends on
275
+ * unverifiable runtime state, so it is dropped rather than guessed at;
276
+ * - remainder starting with `//` — protocol-relative (`${proto}//host/api/x`);
277
+ * keeping it would later collapse to `/host/api/x`.
278
+ *
279
+ * The `?` in a query string cannot leak into the brace matching: `${...}`
280
+ * spans are matched by braces here (before any `{param}` replacement), and
281
+ * `normalizeHttpPath` splits on `?` only after the whole `${...}` span —
282
+ * including any `?` inside it — has been collapsed to `{param}`. So
283
+ * `` `${c}/api/x?id=${id}` `` reduces to `/api/x` on both orderings.
268
284
  */
285
+ function stripLeadingTemplatePrefix(url) {
286
+ if (!url.startsWith('${'))
287
+ return url;
288
+ const rest = url.replace(/^(?:\$\{[^}]*\})+/, '');
289
+ return rest.startsWith('/') && !rest.startsWith('//') ? rest : null;
290
+ }
291
+ /**
292
+ * Placeholder substituted for `${...}` before WHATWG `URL` parsing so the
293
+ * parser cannot percent-encode our own `{param}` markers. A genuine encoded
294
+ * segment like `%7Bfoo%7D` then survives as a literal, instead of being
295
+ * rewritten into braces and folded into `{param}`.
296
+ *
297
+ * Private-use U+E000 cannot appear in a real URL path, so a literal
298
+ * `__gitnexus_http_param__` segment is not rewritten into `{param}`.
299
+ */
300
+ const CONSUMER_PARAM_SENTINEL = '\uE000';
301
+ const CONSUMER_PARAM_SENTINEL_ENC = '%ee%80%80';
302
+ function restoreConsumerParamSentinel(pathOnly) {
303
+ return pathOnly
304
+ .split(CONSUMER_PARAM_SENTINEL)
305
+ .join('{param}')
306
+ .replace(new RegExp(CONSUMER_PARAM_SENTINEL_ENC, 'gi'), '{param}');
307
+ }
308
+ /** Canonicalize a consumer URL after `stripLeadingTemplatePrefix`. */
269
309
  function normalizeConsumerPath(url) {
270
- const templated = url.replace(/\$\{[^}]+\}/g, '{param}').trim();
310
+ const stripped = stripLeadingTemplatePrefix(url.trim());
311
+ if (stripped === null)
312
+ return null;
313
+ const templated = stripped.replace(/\$\{[^}]+\}/g, CONSUMER_PARAM_SENTINEL).trim();
271
314
  let pathOnly = templated;
272
315
  if (/^https?:\/\//i.test(templated)) {
273
316
  try {
@@ -277,6 +320,7 @@ function normalizeConsumerPath(url) {
277
320
  pathOnly = templated.replace(/^https?:\/\/[^/]+/i, '');
278
321
  }
279
322
  }
323
+ pathOnly = restoreConsumerParamSentinel(pathOnly);
280
324
  const normalized = normalizeHttpPath(pathOnly || '/');
281
325
  const segments = normalized
282
326
  .split('/')
@@ -886,6 +930,13 @@ export class HttpRouteExtractor {
886
930
  if (d.role !== 'consumer')
887
931
  continue;
888
932
  const pathNorm = normalizeConsumerPath(d.path);
933
+ // A consumer url that cannot be reduced to a routable path (e.g. a
934
+ // leading template binding that is neither a clean prefix nor a
935
+ // remainder that starts with `/`) is dropped here rather than emitted
936
+ // as a never-matching contract — same treatment the plugins give
937
+ // static relative urls at scan time.
938
+ if (pathNorm === null)
939
+ continue;
889
940
  // Resolve the function CONTAINING the fetch/axios call so the consumer
890
941
  // contract carries a real symbolUid (was always '' — the gap that left
891
942
  // cross-repo trace/impact unable to traverse HTTP links).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.36",
3
+ "version": "1.6.11-rc.37",
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",