gitnexus 1.6.9 → 1.6.10-rc.10

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 (91) hide show
  1. package/README.md +126 -41
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.js +106 -3
  5. package/dist/cli/cli-message.d.ts +1 -1
  6. package/dist/cli/doctor.d.ts +5 -0
  7. package/dist/cli/doctor.js +41 -3
  8. package/dist/cli/editor-targets.d.ts +17 -2
  9. package/dist/cli/editor-targets.js +44 -0
  10. package/dist/cli/embeddings.d.ts +12 -0
  11. package/dist/cli/embeddings.js +49 -0
  12. package/dist/cli/help-i18n.js +4 -0
  13. package/dist/cli/i18n/en.d.ts +5 -1
  14. package/dist/cli/i18n/en.js +5 -1
  15. package/dist/cli/i18n/resources.d.ts +9 -1
  16. package/dist/cli/i18n/zh-CN.d.ts +4 -0
  17. package/dist/cli/i18n/zh-CN.js +5 -1
  18. package/dist/cli/index.js +12 -1
  19. package/dist/cli/setup.js +204 -28
  20. package/dist/cli/uninstall.js +70 -16
  21. package/dist/core/embeddings/embedder.js +16 -2
  22. package/dist/core/embeddings/http-client.d.ts +28 -1
  23. package/dist/core/embeddings/http-client.js +107 -15
  24. package/dist/core/embeddings/node-module-compat.d.ts +23 -0
  25. package/dist/core/embeddings/node-module-compat.js +23 -0
  26. package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
  27. package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
  28. package/dist/core/embeddings/runtime-install.d.ts +119 -0
  29. package/dist/core/embeddings/runtime-install.js +372 -0
  30. package/dist/core/embeddings/runtime-support.d.ts +48 -17
  31. package/dist/core/embeddings/runtime-support.js +96 -0
  32. package/dist/core/group/extractors/http-patterns/python.js +246 -108
  33. package/dist/core/group/extractors/manifest-extractor.js +4 -0
  34. package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
  35. package/dist/core/ingestion/cobol-processor.js +27 -26
  36. package/dist/core/ingestion/community-processor.d.ts +51 -1
  37. package/dist/core/ingestion/community-processor.js +316 -63
  38. package/dist/core/ingestion/emit-references.js +3 -2
  39. package/dist/core/ingestion/markdown-processor.js +3 -2
  40. package/dist/core/ingestion/parsing-processor.d.ts +3 -1
  41. package/dist/core/ingestion/parsing-processor.js +4 -0
  42. package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
  43. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
  44. package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
  45. package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
  46. package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
  47. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  48. package/dist/core/ingestion/tree-sitter-queries.js +12 -1
  49. package/dist/core/ingestion/utils/line-base.d.ts +20 -0
  50. package/dist/core/ingestion/utils/line-base.js +20 -0
  51. package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
  52. package/dist/core/ingestion/utils/symbol-labels.js +45 -0
  53. package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
  54. package/dist/core/ingestion/workers/parse-worker.js +39 -4
  55. package/dist/core/ingestion/workers/result-merge.js +4 -0
  56. package/dist/core/lbug/csv-generator.js +8 -2
  57. package/dist/core/lbug/extension-load-error.d.ts +67 -0
  58. package/dist/core/lbug/extension-load-error.js +320 -0
  59. package/dist/core/lbug/extension-loader.d.ts +17 -2
  60. package/dist/core/lbug/extension-loader.js +38 -13
  61. package/dist/core/lbug/lbug-adapter.js +7 -16
  62. package/dist/core/lbug/native-check.d.ts +22 -0
  63. package/dist/core/lbug/native-check.js +66 -0
  64. package/dist/core/lbug/pool-adapter.js +10 -1
  65. package/dist/core/lbug/sidecar-recovery.d.ts +32 -0
  66. package/dist/core/lbug/sidecar-recovery.js +91 -7
  67. package/dist/core/platform/capabilities.js +27 -1
  68. package/dist/core/run-analyze.js +33 -6
  69. package/dist/core/search/fts-indexes.d.ts +7 -0
  70. package/dist/core/search/fts-indexes.js +37 -0
  71. package/dist/mcp/core/embedder.js +16 -2
  72. package/dist/mcp/local/line-display.d.ts +22 -0
  73. package/dist/mcp/local/line-display.js +3 -0
  74. package/dist/mcp/local/local-backend.d.ts +14 -0
  75. package/dist/mcp/local/local-backend.js +63 -19
  76. package/dist/mcp/local/pdg-impact.d.ts +5 -3
  77. package/dist/mcp/local/pdg-impact.js +5 -2
  78. package/dist/mcp/resources.js +1 -0
  79. package/dist/mcp/tools.js +1 -1
  80. package/dist/server/api.js +2 -2
  81. package/dist/storage/parse-cache.js +1 -1
  82. package/dist/storage/repo-manager.d.ts +6 -1
  83. package/dist/storage/repo-manager.js +6 -1
  84. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
  85. package/hooks/claude/gitnexus-hook.cjs +59 -9
  86. package/package.json +5 -3
  87. package/scripts/cross-platform-tests.ts +26 -0
  88. package/scripts/ensure-fts.ts +32 -0
  89. package/scripts/install-duckdb-extension.mjs +85 -24
  90. package/scripts/run-cross-platform.ts +46 -6
  91. package/scripts/shard-arg.ts +30 -0
@@ -1,6 +1,7 @@
1
1
  import Python from 'tree-sitter-python';
2
2
  import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
3
3
  import { normalizeExtractedRoutePath } from '../../../ingestion/route-extractors/route-path.js';
4
+ import { extractPythonModuleConstants, parseConstOperands, resolveOperands, } from '../../../ingestion/route-extractors/python-const-resolver.js';
4
5
  /**
5
6
  * Python HTTP plugin. Handles:
6
7
  * - FastAPI `@app.get("/path")` provider decorators
@@ -67,6 +68,44 @@ const FASTAPI_ROUTER_PATTERNS = compilePatterns({
67
68
  },
68
69
  ],
69
70
  });
71
+ // #2391: `@router.<verb>` / `@app.<verb>` whose first argument is a non-literal
72
+ // path — a bare imported constant or a `+`-concatenation. The path is resolved
73
+ // against the repo-wide constant map (parity with the ingestion side) and, on
74
+ // failure, the route is skipped (no provider contract) exactly like ingestion.
75
+ const FASTAPI_ROUTER_EXPR_PATTERNS = compilePatterns({
76
+ name: 'python-fastapi-router-expr',
77
+ language: Python,
78
+ patterns: [
79
+ {
80
+ meta: {},
81
+ query: `
82
+ (decorator
83
+ (call
84
+ function: (attribute
85
+ object: (identifier) @obj (#eq? @obj "router")
86
+ attribute: (identifier) @method (#match? @method "^(get|post|put|delete|patch)$"))
87
+ arguments: (argument_list . [(identifier) (binary_operator)] @path)))
88
+ `,
89
+ },
90
+ ],
91
+ });
92
+ const FASTAPI_APP_EXPR_PATTERNS = compilePatterns({
93
+ name: 'python-fastapi-app-expr',
94
+ language: Python,
95
+ patterns: [
96
+ {
97
+ meta: {},
98
+ query: `
99
+ (decorator
100
+ (call
101
+ function: (attribute
102
+ object: (identifier) @obj (#eq? @obj "app")
103
+ attribute: (identifier) @method (#match? @method "^(get|post|put|delete|patch)$"))
104
+ arguments: (argument_list . [(identifier) (binary_operator)] @path)))
105
+ `,
106
+ },
107
+ ],
108
+ });
70
109
  // ─── Provider: Flask `app.add_url_rule('/path', view_func=handler)` ───
71
110
  // The imperative Flask route registration: unlike `@app.route` (whose handler
72
111
  // is the decorated function, same-file), `view_func` is frequently an IMPORTED
@@ -830,111 +869,153 @@ function recordPrefix(target, key, prefix) {
830
869
  set.add(prefix);
831
870
  target.set(key, set);
832
871
  }
872
+ // Cheap cost-gate pre-filter: a `@router`/`@app.<verb>(` call whose first
873
+ // argument is non-literal — either it STARTS with an identifier (a bare constant
874
+ // or the head of `CONST + "/x"`), or it is a string-literal-LEADING concat
875
+ // (`"/api" + SUFFIX`) detected by a `+` before the decorator's closing paren
876
+ // (#2393). `[^)]*` spans the whole argument, including a Black-formatted concat
877
+ // that wraps across lines, but stays bounded by the decorator's own `)`. Gating
878
+ // the literal-leading case on the `+` (not merely a leading quote) keeps a plain
879
+ // string route `@router.get("/x")` OFF the gate, so a literal-only repo pays no
880
+ // parse pass. Deliberately loose — a false positive only costs a parse; a false
881
+ // negative would silently drop the feature.
882
+ const NONLITERAL_ROUTE_DECORATOR_RE = /@\s*(?:app|router)\s*\.\s*(?:get|post|put|delete|patch)\s*\(\s*(?:[A-Za-z_]|["'][^)]*\+)/;
833
883
  function buildPythonRepoContext(files, parser, readFile, parseSource) {
834
884
  const prefixesByLongKey = new Map();
835
885
  const prefixesByShortKey = new Map();
836
- // Cross-file pre-pass: only `include_router` sites need it they bind a
837
- // prefix declared in one file to a router defined in another. Same-file
838
- // `APIRouter(prefix=...)` is resolved in scan() from the file's own tree, so
839
- // APIRouter-only files are left out here and never parsed twice.
886
+ // Single read pass (#2393): slurp every `.py` file's content ONCE. This used to
887
+ // be two passes the include_router pre-pass below and the #2391 constant cost
888
+ // gate each re-read every `.py` file. The composed-route cost gate is computed
889
+ // in the same pass so a literal-only repo still does exactly one read and zero
890
+ // parses.
891
+ const pyContents = new Map();
892
+ let hasComposedRoute = false;
840
893
  for (const rel of files) {
841
894
  if (!rel.endsWith('.py'))
842
895
  continue;
843
896
  const src = readFile(rel);
844
897
  if (!src)
845
898
  continue;
846
- if (!src.includes('include_router'))
899
+ pyContents.set(rel, src);
900
+ if (!hasComposedRoute && NONLITERAL_ROUTE_DECORATOR_RE.test(src))
901
+ hasComposedRoute = true;
902
+ }
903
+ // Single PARSE pass (#2391): parse each `.py` at most once and feed BOTH the
904
+ // include_router prefix pre-pass and the composed-constant map below. This used
905
+ // to be two loops, so an include_router file in a composed repo was parsed
906
+ // twice. A file that needs neither pass is not parsed at all (cost gates intact).
907
+ //
908
+ // Cross-file pre-pass: only `include_router` sites need it — they bind a prefix
909
+ // declared in one file to a router defined in another. Same-file
910
+ // `APIRouter(prefix=...)` is resolved in scan() from the file's own tree.
911
+ const constantsByFile = new Map();
912
+ for (const [rel, src] of pyContents) {
913
+ const needsRouter = src.includes('include_router');
914
+ if (!needsRouter && !hasComposedRoute)
847
915
  continue;
848
916
  parser.setLanguage(Python);
849
917
  const tree = parseSource(parser, src);
850
918
  if (!tree)
851
919
  continue;
852
- const localNameToModule = new Map();
853
- for (const m of runCompiledPatterns(FROM_IMPORT_ROUTER_PATTERNS, tree)) {
854
- const moduleNode = m.captures.module;
855
- const aliasNode = m.captures.alias;
856
- const importedNode = m.captures.imported;
857
- if (!moduleNode || !importedNode)
858
- continue;
859
- const localName = aliasNode?.text ?? importedNode.text;
860
- const moduleShort = lastSegmentOfDotted(moduleNode.text);
861
- if (!moduleShort)
862
- continue;
863
- const moduleLong = lastTwoSegmentsAsLongKey(moduleNode.text);
864
- localNameToModule.set(localName, { moduleShort, moduleLong });
865
- }
866
- // Module-alias map: name imported from a multi-segment package →
867
- // long key. Lets Shape A look up the precise file for `<name>.router`
868
- // even when `<name>` collides with another package's basename.
869
- const localNameToModuleAlias = new Map();
870
- for (const m of runCompiledPatterns(FROM_IMPORT_MODULE_PATTERNS, tree)) {
871
- const moduleNode = m.captures.module;
872
- const importedNode = m.captures.imported;
873
- const aliasNode = m.captures.alias;
874
- if (!moduleNode || !importedNode)
875
- continue;
876
- // Skip the `router` shape — already handled by FROM_IMPORT_ROUTER_PATTERNS
877
- // above and stored under its router-aware semantics.
878
- if (importedNode.text === 'router')
879
- continue;
880
- const moduleLong = lastTwoSegmentsAsLongKey(`${moduleNode.text}.${importedNode.text}`);
881
- if (!moduleLong)
882
- continue;
883
- const localName = aliasNode?.text ?? importedNode.text;
884
- localNameToModuleAlias.set(localName, moduleLong);
885
- }
886
- // Shape A: `<host>.include_router(<module>.router, prefix='/x')`.
887
- // The call site gives us only a short module name. We promote to a
888
- // long key when the same file imports `<module>` via either
889
- // `from <pkg> import <module>` (recorded in `localNameToModuleAlias`
890
- // — the typical pattern) or, less commonly, a router-aware import
891
- // statement. Only fall back to the basename short key when neither
892
- // alias is available.
893
- for (const m of runCompiledPatterns(INCLUDE_ROUTER_ATTR_PATTERNS, tree)) {
894
- const modNode = m.captures.router_module;
895
- const prefixNode = m.captures.prefix;
896
- if (!modNode || !prefixNode)
897
- continue;
898
- const prefix = unquoteLiteral(prefixNode.text);
899
- if (prefix === null)
900
- continue;
901
- const moduleShort = modNode.text;
902
- const aliasLong = localNameToModuleAlias.get(moduleShort);
903
- const sameFileImport = localNameToModule.get(moduleShort);
904
- const longKey = aliasLong ?? sameFileImport?.moduleLong;
905
- if (longKey) {
906
- recordPrefix(prefixesByLongKey, longKey, prefix);
920
+ if (needsRouter) {
921
+ const localNameToModule = new Map();
922
+ for (const m of runCompiledPatterns(FROM_IMPORT_ROUTER_PATTERNS, tree)) {
923
+ const moduleNode = m.captures.module;
924
+ const aliasNode = m.captures.alias;
925
+ const importedNode = m.captures.imported;
926
+ if (!moduleNode || !importedNode)
927
+ continue;
928
+ const localName = aliasNode?.text ?? importedNode.text;
929
+ const moduleShort = lastSegmentOfDotted(moduleNode.text);
930
+ if (!moduleShort)
931
+ continue;
932
+ const moduleLong = lastTwoSegmentsAsLongKey(moduleNode.text);
933
+ localNameToModule.set(localName, { moduleShort, moduleLong });
907
934
  }
908
- else {
909
- recordPrefix(prefixesByShortKey, moduleShort, prefix);
935
+ // Module-alias map: name imported from a multi-segment package →
936
+ // long key. Lets Shape A look up the precise file for `<name>.router`
937
+ // even when `<name>` collides with another package's basename.
938
+ const localNameToModuleAlias = new Map();
939
+ for (const m of runCompiledPatterns(FROM_IMPORT_MODULE_PATTERNS, tree)) {
940
+ const moduleNode = m.captures.module;
941
+ const importedNode = m.captures.imported;
942
+ const aliasNode = m.captures.alias;
943
+ if (!moduleNode || !importedNode)
944
+ continue;
945
+ // Skip the `router` shape — already handled by FROM_IMPORT_ROUTER_PATTERNS
946
+ // above and stored under its router-aware semantics.
947
+ if (importedNode.text === 'router')
948
+ continue;
949
+ const moduleLong = lastTwoSegmentsAsLongKey(`${moduleNode.text}.${importedNode.text}`);
950
+ if (!moduleLong)
951
+ continue;
952
+ const localName = aliasNode?.text ?? importedNode.text;
953
+ localNameToModuleAlias.set(localName, moduleLong);
910
954
  }
911
- }
912
- // Shape B: `<host>.include_router(my_router, prefix='/x')` resolve
913
- // `my_router` via the import map built above. Whenever the import
914
- // statement supplied a multi-segment module path the long key is
915
- // recorded, eliminating cross-package collisions.
916
- for (const m of runCompiledPatterns(INCLUDE_ROUTER_NAME_PATTERNS, tree)) {
917
- const nameNode = m.captures.router_name;
918
- const prefixNode = m.captures.prefix;
919
- if (!nameNode || !prefixNode)
920
- continue;
921
- const localImp = localNameToModule.get(nameNode.text);
922
- if (!localImp)
923
- continue;
924
- const prefix = unquoteLiteral(prefixNode.text);
925
- if (prefix === null)
926
- continue;
927
- if (localImp.moduleLong) {
928
- recordPrefix(prefixesByLongKey, localImp.moduleLong, prefix);
955
+ // Shape A: `<host>.include_router(<module>.router, prefix='/x')`.
956
+ // The call site gives us only a short module name. We promote to a
957
+ // long key when the same file imports `<module>` via either
958
+ // `from <pkg> import <module>` (recorded in `localNameToModuleAlias`
959
+ // — the typical pattern) or, less commonly, a router-aware import
960
+ // statement. Only fall back to the basename short key when neither
961
+ // alias is available.
962
+ for (const m of runCompiledPatterns(INCLUDE_ROUTER_ATTR_PATTERNS, tree)) {
963
+ const modNode = m.captures.router_module;
964
+ const prefixNode = m.captures.prefix;
965
+ if (!modNode || !prefixNode)
966
+ continue;
967
+ const prefix = unquoteLiteral(prefixNode.text);
968
+ if (prefix === null)
969
+ continue;
970
+ const moduleShort = modNode.text;
971
+ const aliasLong = localNameToModuleAlias.get(moduleShort);
972
+ const sameFileImport = localNameToModule.get(moduleShort);
973
+ const longKey = aliasLong ?? sameFileImport?.moduleLong;
974
+ if (longKey) {
975
+ recordPrefix(prefixesByLongKey, longKey, prefix);
976
+ }
977
+ else {
978
+ recordPrefix(prefixesByShortKey, moduleShort, prefix);
979
+ }
929
980
  }
930
- else {
931
- recordPrefix(prefixesByShortKey, localImp.moduleShort, prefix);
981
+ // Shape B: `<host>.include_router(my_router, prefix='/x')` — resolve
982
+ // `my_router` via the import map built above. Whenever the import
983
+ // statement supplied a multi-segment module path the long key is
984
+ // recorded, eliminating cross-package collisions.
985
+ for (const m of runCompiledPatterns(INCLUDE_ROUTER_NAME_PATTERNS, tree)) {
986
+ const nameNode = m.captures.router_name;
987
+ const prefixNode = m.captures.prefix;
988
+ if (!nameNode || !prefixNode)
989
+ continue;
990
+ const localImp = localNameToModule.get(nameNode.text);
991
+ if (!localImp)
992
+ continue;
993
+ const prefix = unquoteLiteral(prefixNode.text);
994
+ if (prefix === null)
995
+ continue;
996
+ if (localImp.moduleLong) {
997
+ recordPrefix(prefixesByLongKey, localImp.moduleLong, prefix);
998
+ }
999
+ else {
1000
+ recordPrefix(prefixesByShortKey, localImp.moduleShort, prefix);
1001
+ }
1002
+ }
1003
+ }
1004
+ // #2391: build the repo-wide constant map for resolving non-literal decorator
1005
+ // paths (KTD6 cost gate: only when `hasComposedRoute`). Parse EVERY `.py` so
1006
+ // the resolvable set matches the ingestion aggregate (R4 parity) — a narrower
1007
+ // set would return null where ingestion resolves.
1008
+ if (hasComposedRoute) {
1009
+ const mc = extractPythonModuleConstants(tree);
1010
+ if (mc.literals.size > 0 || mc.exprs.size > 0 || mc.imports.size > 0) {
1011
+ constantsByFile.set(rel, mc);
932
1012
  }
933
1013
  }
934
1014
  }
935
1015
  return {
936
1016
  prefixesByLongKey,
937
1017
  prefixesByShortKey,
1018
+ constantsByFile,
938
1019
  };
939
1020
  }
940
1021
  function joinPrefix(prefix, route) {
@@ -971,32 +1052,67 @@ export const PYTHON_HTTP_PLUGIN = {
971
1052
  // statements, so an imperatively-registered handler (Flask `view_func`) that
972
1053
  // is an imported (possibly aliased) symbol resolves to its real definition.
973
1054
  const importMap = buildPythonImportMap(tree);
974
- // Providers: FastAPI @app.<verb>("/path") already absolute path.
975
- for (const match of runCompiledPatterns(FASTAPI_APP_PATTERNS, tree)) {
976
- const methodNode = match.captures.method;
977
- const pathNode = match.captures.path;
978
- if (!methodNode || !pathNode)
979
- continue;
980
- const httpMethod = FASTAPI_VERBS[methodNode.text];
981
- if (!httpMethod)
982
- continue;
983
- const path = unquoteLiteral(pathNode.text);
984
- if (path === null)
985
- continue;
1055
+ // #2391: fold a non-literal decorator argument (bare constant or
1056
+ // `+`-concatenation) to its literal path against the repo constant map, or
1057
+ // `null` skip (the same floor the ingestion side applies, so provider
1058
+ // contracts and graph Route nodes agree on both resolved and dropped routes).
1059
+ const resolveExprArg = (argNode) => {
1060
+ const cbf = ctx?.constantsByFile;
1061
+ if (!cbf || !fileRel)
1062
+ return null;
1063
+ // Build an operand list and fold via `resolveOperands` — the SAME entry the
1064
+ // ingestion side uses (parse-impl folds `routePathOperands`). Using the
1065
+ // by-name `resolveConstant` here would enter `foldName` one depth shallower,
1066
+ // so at the MAX_RESOLVE_DEPTH boundary the group would resolve a chain
1067
+ // ingestion drops, breaking R4 parity (#2393).
1068
+ const operands = argNode.type === 'identifier'
1069
+ ? [{ kind: 'ref', name: argNode.text }]
1070
+ : parseConstOperands(argNode);
1071
+ return operands ? resolveOperands(fileRel, operands, cbf) : null;
1072
+ };
1073
+ const emitAppProvider = (httpMethod, pathVal, line) => {
986
1074
  out.push({
987
1075
  role: 'provider',
988
1076
  framework: 'fastapi',
989
1077
  method: httpMethod,
990
- path,
1078
+ path: pathVal,
991
1079
  name: null,
992
1080
  // The decorated handler has no captured name → resolve by line-span
993
1081
  // containment. Best-effort fallback: FastAPI routes are graph-backed
994
1082
  // (ingestion decorator routes) and the function span starts at `def`
995
1083
  // (decorators excluded), so this lands the single-decorator case and
996
1084
  // degrades to file-level for multi-decorator stacks.
997
- line: pathNode.startPosition.row + 1,
1085
+ line,
998
1086
  confidence: 0.8,
999
1087
  });
1088
+ };
1089
+ // Providers: FastAPI @app.<verb>("/path") — already absolute path.
1090
+ for (const match of runCompiledPatterns(FASTAPI_APP_PATTERNS, tree)) {
1091
+ const methodNode = match.captures.method;
1092
+ const pathNode = match.captures.path;
1093
+ if (!methodNode || !pathNode)
1094
+ continue;
1095
+ const httpMethod = FASTAPI_VERBS[methodNode.text];
1096
+ if (!httpMethod)
1097
+ continue;
1098
+ const path = unquoteLiteral(pathNode.text);
1099
+ if (path === null)
1100
+ continue;
1101
+ emitAppProvider(httpMethod, path, pathNode.startPosition.row + 1);
1102
+ }
1103
+ // Providers: FastAPI @app.<verb>(CONST | A + "/x") — resolved composed path.
1104
+ for (const match of runCompiledPatterns(FASTAPI_APP_EXPR_PATTERNS, tree)) {
1105
+ const methodNode = match.captures.method;
1106
+ const pathNode = match.captures.path;
1107
+ if (!methodNode || !pathNode)
1108
+ continue;
1109
+ const httpMethod = FASTAPI_VERBS[methodNode.text];
1110
+ if (!httpMethod)
1111
+ continue;
1112
+ const resolved = resolveExprArg(pathNode);
1113
+ if (resolved === null)
1114
+ continue; // skip floor
1115
+ emitAppProvider(httpMethod, resolved, pathNode.startPosition.row + 1);
1000
1116
  }
1001
1117
  // Django providers come from the graph Route nodes (includes composed by
1002
1118
  // the ingestion route extractor), not a per-file source scan — see the note
@@ -1020,17 +1136,11 @@ export const PYTHON_HTTP_PLUGIN = {
1020
1136
  // change is strictly additive vs. the prior @app-only behaviour;
1021
1137
  // when the same router is mounted under multiple prefixes we emit
1022
1138
  // one detection per prefix.
1023
- for (const match of runCompiledPatterns(FASTAPI_ROUTER_PATTERNS, tree)) {
1024
- const methodNode = match.captures.method;
1025
- const pathNode = match.captures.path;
1026
- if (!methodNode || !pathNode)
1027
- continue;
1028
- const httpMethod = FASTAPI_VERBS[methodNode.text];
1029
- if (!httpMethod)
1030
- continue;
1031
- const rawPath = unquoteLiteral(pathNode.text);
1032
- if (rawPath === null)
1033
- continue;
1139
+ // Join a `@router.<verb>` path with the include_router / APIRouter prefix(es)
1140
+ // that apply to this file and emit one provider detection per prefix. Shared
1141
+ // by the literal and the #2391 non-literal (resolved) router loops so both
1142
+ // stack prefixes identically.
1143
+ const emitRouterProvider = (httpMethod, rawPath, line) => {
1034
1144
  // Long key first (precise, package-aware), short key as fallback.
1035
1145
  // Mirrors the ingestion-side resolution in parse-impl.ts so the
1036
1146
  // graph nodes and group contracts agree on which prefix applies.
@@ -1053,10 +1163,38 @@ export const PYTHON_HTTP_PLUGIN = {
1053
1163
  path: p,
1054
1164
  name: null,
1055
1165
  // Best-effort containment fallback — see the @app provider note above.
1056
- line: pathNode.startPosition.row + 1,
1166
+ line,
1057
1167
  confidence: 0.8,
1058
1168
  });
1059
1169
  }
1170
+ };
1171
+ for (const match of runCompiledPatterns(FASTAPI_ROUTER_PATTERNS, tree)) {
1172
+ const methodNode = match.captures.method;
1173
+ const pathNode = match.captures.path;
1174
+ if (!methodNode || !pathNode)
1175
+ continue;
1176
+ const httpMethod = FASTAPI_VERBS[methodNode.text];
1177
+ if (!httpMethod)
1178
+ continue;
1179
+ const rawPath = unquoteLiteral(pathNode.text);
1180
+ if (rawPath === null)
1181
+ continue;
1182
+ emitRouterProvider(httpMethod, rawPath, pathNode.startPosition.row + 1);
1183
+ }
1184
+ // Providers: FastAPI @router.<verb>(CONST | A + "/x") — resolved composed path
1185
+ // (#2391). Null resolution → skip, so provider/graph parity holds.
1186
+ for (const match of runCompiledPatterns(FASTAPI_ROUTER_EXPR_PATTERNS, tree)) {
1187
+ const methodNode = match.captures.method;
1188
+ const pathNode = match.captures.path;
1189
+ if (!methodNode || !pathNode)
1190
+ continue;
1191
+ const httpMethod = FASTAPI_VERBS[methodNode.text];
1192
+ if (!httpMethod)
1193
+ continue;
1194
+ const resolved = resolveExprArg(pathNode);
1195
+ if (resolved === null)
1196
+ continue;
1197
+ emitRouterProvider(httpMethod, resolved, pathNode.startPosition.row + 1);
1060
1198
  }
1061
1199
  // Providers: Flask `app.add_url_rule('/path', view_func=handler, methods=[…])`.
1062
1200
  // The handler is a `view_func` identifier, frequently an imported (possibly
@@ -7,6 +7,10 @@ import { logger } from '../../logger.js';
7
7
  // reserved-keyword labels `Macro` and `Union`, and LadybugDB's parser rejects
8
8
  // a disjunction that names a reserved keyword (#2325) — which the resolver's
9
9
  // try/catch then swallowed. `labels(n) IN` has no such collision.
10
+ // This list overlaps `ingestion/utils/symbol-labels.ts` (SYMBOL_NODE_LABELS) but
11
+ // is a deliberate SUBSET — it omits `Namespace`/`Variable`/`Module`. Unifying the
12
+ // two would widen which nodes resolve as contract symbols and must update the
13
+ // #2325 test, so they are intentionally kept separate for now.
10
14
  export const CUSTOM_CONTRACT_RESOLVE_QUERY = `MATCH (n)
11
15
  WHERE labels(n) IN ['Function','Method','Class','Interface','Struct','Enum','Trait','Constructor','TypeAlias','Impl','Macro','Union','Typedef','Property','Record','Delegate','Annotation','Template','Const','Static','CodeElement']
12
16
  AND n.name = $symbolName
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { parseJcl } from './jcl-parser.js';
19
19
  import { generateId } from '../../../lib/utils.js';
20
+ import { toZeroBasedLine } from '../utils/line-base.js';
20
21
  /**
21
22
  * Process JCL files and integrate into the knowledge graph.
22
23
  *
@@ -71,8 +72,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
71
72
  properties: {
72
73
  name: job.name,
73
74
  filePath,
74
- startLine: job.line,
75
- endLine: job.line,
75
+ startLine: toZeroBasedLine(job.line),
76
+ endLine: toZeroBasedLine(job.line),
76
77
  description: `jcl-job${classPart}${msgPart}`,
77
78
  },
78
79
  });
@@ -105,8 +106,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
105
106
  properties: {
106
107
  name: step.name,
107
108
  filePath,
108
- startLine: step.line,
109
- endLine: step.line,
109
+ startLine: toZeroBasedLine(step.line),
110
+ endLine: toZeroBasedLine(step.line),
110
111
  description: `jcl-step${pgmPart}${procPart}`,
111
112
  },
112
113
  });
@@ -170,8 +171,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
170
171
  properties: {
171
172
  name: dd.dataset,
172
173
  filePath,
173
- startLine: dd.line,
174
- endLine: dd.line,
174
+ startLine: toZeroBasedLine(dd.line),
175
+ endLine: toZeroBasedLine(dd.line),
175
176
  description: `jcl-dataset${dispPart}`,
176
177
  },
177
178
  });
@@ -202,8 +203,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
202
203
  properties: {
203
204
  name: proc.name,
204
205
  filePath,
205
- startLine: proc.line,
206
- endLine: proc.line,
206
+ startLine: toZeroBasedLine(proc.line),
207
+ endLine: toZeroBasedLine(proc.line),
207
208
  description: 'jcl-proc-instream',
208
209
  },
209
210
  });
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import path from 'node:path';
16
16
  import { generateId } from '../../lib/utils.js';
17
+ import { toZeroBasedLine } from './utils/line-base.js';
17
18
  import { SupportedLanguages } from '../../_shared/index.js';
18
19
  import { preprocessCobolSource, extractCobolSymbolsWithRegex, } from './cobol/cobol-preprocessor.js';
19
20
  import { expandCopies } from './cobol/cobol-copy-expander.js';
@@ -275,8 +276,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
275
276
  properties: {
276
277
  name: extracted.programName,
277
278
  filePath,
278
- startLine: 1,
279
- endLine: lines.length,
279
+ startLine: toZeroBasedLine(1),
280
+ endLine: toZeroBasedLine(lines.length),
280
281
  language: SupportedLanguages.Cobol,
281
282
  isExported: true,
282
283
  description: metaDesc || undefined,
@@ -310,8 +311,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
310
311
  properties: {
311
312
  name: prog.name,
312
313
  filePath,
313
- startLine: prog.startLine,
314
- endLine: prog.endLine,
314
+ startLine: toZeroBasedLine(prog.startLine),
315
+ endLine: toZeroBasedLine(prog.endLine),
315
316
  language: SupportedLanguages.Cobol,
316
317
  isExported: true,
317
318
  description: `nested-program${prog.isCommon ? ' common' : ''}`,
@@ -349,8 +350,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
349
350
  properties: {
350
351
  name: sec.name,
351
352
  filePath,
352
- startLine: sec.line,
353
- endLine: nextLine,
353
+ startLine: toZeroBasedLine(sec.line),
354
+ endLine: toZeroBasedLine(nextLine),
354
355
  language: SupportedLanguages.Cobol,
355
356
  isExported: true,
356
357
  },
@@ -379,8 +380,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
379
380
  properties: {
380
381
  name: para.name,
381
382
  filePath,
382
- startLine: para.line,
383
- endLine: nextLine,
383
+ startLine: toZeroBasedLine(para.line),
384
+ endLine: toZeroBasedLine(nextLine),
384
385
  language: SupportedLanguages.Cobol,
385
386
  isExported: true,
386
387
  },
@@ -412,8 +413,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
412
413
  properties: {
413
414
  name: item.name,
414
415
  filePath,
415
- startLine: item.line,
416
- endLine: item.line,
416
+ startLine: toZeroBasedLine(item.line),
417
+ endLine: toZeroBasedLine(item.line),
417
418
  language: SupportedLanguages.Cobol,
418
419
  description: `level:${item.level} section:${item.section}${item.pic ? ` pic:${item.pic}` : ''}`,
419
420
  },
@@ -506,8 +507,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
506
507
  properties: {
507
508
  name: `CALL ${call.target}`,
508
509
  filePath,
509
- startLine: call.line,
510
- endLine: call.line,
510
+ startLine: toZeroBasedLine(call.line),
511
+ endLine: toZeroBasedLine(call.line),
511
512
  language: SupportedLanguages.Cobol,
512
513
  description: 'dynamic-call (target is a data item, not resolvable statically)',
513
514
  },
@@ -619,8 +620,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
619
620
  properties: {
620
621
  name: `EXEC SQL ${sql.operation}`,
621
622
  filePath,
622
- startLine: sql.line,
623
- endLine: sql.line,
623
+ startLine: toZeroBasedLine(sql.line),
624
+ endLine: toZeroBasedLine(sql.line),
624
625
  language: SupportedLanguages.Cobol,
625
626
  description: `tables:[${sql.tables.join(',')}] cursors:[${sql.cursors.join(',')}]`,
626
627
  },
@@ -691,8 +692,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
691
692
  properties: {
692
693
  name: `EXEC CICS ${cics.command}`,
693
694
  filePath,
694
- startLine: cics.line,
695
- endLine: cics.line,
695
+ startLine: toZeroBasedLine(cics.line),
696
+ endLine: toZeroBasedLine(cics.line),
696
697
  language: SupportedLanguages.Cobol,
697
698
  description: [
698
699
  cics.mapName && `map:${cics.mapName}`,
@@ -726,8 +727,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
726
727
  properties: {
727
728
  name: `CICS ${cics.command} ${cics.programName}`,
728
729
  filePath,
729
- startLine: cics.line,
730
- endLine: cics.line,
730
+ startLine: toZeroBasedLine(cics.line),
731
+ endLine: toZeroBasedLine(cics.line),
731
732
  language: SupportedLanguages.Cobol,
732
733
  description: `cics-dynamic-program (target is data item ${cics.programName})`,
733
734
  },
@@ -879,8 +880,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
879
880
  properties: {
880
881
  name: entry.name,
881
882
  filePath,
882
- startLine: entry.line,
883
- endLine: entry.line,
883
+ startLine: toZeroBasedLine(entry.line),
884
+ endLine: toZeroBasedLine(entry.line),
884
885
  language: SupportedLanguages.Cobol,
885
886
  isExported: true,
886
887
  description: entry.parameters.length > 0 ? `using:${entry.parameters.join(',')}` : undefined,
@@ -1014,8 +1015,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
1014
1015
  properties: {
1015
1016
  name: `EXEC DLI ${dli.verb}`,
1016
1017
  filePath,
1017
- startLine: dli.line,
1018
- endLine: dli.line,
1018
+ startLine: toZeroBasedLine(dli.line),
1019
+ endLine: toZeroBasedLine(dli.line),
1019
1020
  language: SupportedLanguages.Cobol,
1020
1021
  description: [
1021
1022
  dli.segmentName && `segment:${dli.segmentName}`,
@@ -1148,8 +1149,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
1148
1149
  properties: {
1149
1150
  name: fd.selectName,
1150
1151
  filePath,
1151
- startLine: fd.line,
1152
- endLine: fd.line,
1152
+ startLine: toZeroBasedLine(fd.line),
1153
+ endLine: toZeroBasedLine(fd.line),
1153
1154
  language: SupportedLanguages.Cobol,
1154
1155
  description: `assign:${fd.assignTo}${fd.isOptional ? ' optional' : ''}${fd.organization ? ` org:${fd.organization}` : ''}${fd.access ? ` access:${fd.access}` : ''}`,
1155
1156
  },
@@ -1231,8 +1232,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
1231
1232
  properties: {
1232
1233
  name: `CANCEL ${cancel.target}`,
1233
1234
  filePath,
1234
- startLine: cancel.line,
1235
- endLine: cancel.line,
1235
+ startLine: toZeroBasedLine(cancel.line),
1236
+ endLine: toZeroBasedLine(cancel.line),
1236
1237
  language: SupportedLanguages.Cobol,
1237
1238
  description: 'dynamic-cancel (target is a data item, not resolvable statically)',
1238
1239
  },