codegraph-engine 2.3.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

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 (80) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +172 -2
  3. codegraph/agent_capabilities.py +320 -3
  4. codegraph/agent_rules.py +1 -1
  5. codegraph/api_drift.py +455 -0
  6. codegraph/async_queue/__init__.py +50 -0
  7. codegraph/async_queue/detectors/__init__.py +48 -0
  8. codegraph/async_queue/detectors/celery.py +160 -0
  9. codegraph/async_queue/detectors/kafka.py +168 -0
  10. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  11. codegraph/async_queue/detectors/redis.py +130 -0
  12. codegraph/async_queue/detectors/sqs.py +135 -0
  13. codegraph/async_queue/models.py +161 -0
  14. codegraph/async_queue/otel_models.py +74 -0
  15. codegraph/async_queue/otel_parser.py +337 -0
  16. codegraph/async_queue/reconciler.py +427 -0
  17. codegraph/async_queue/scanner.py +291 -0
  18. codegraph/async_queue/traversal.py +447 -0
  19. codegraph/change_impact.py +320 -110
  20. codegraph/cli.py +776 -25
  21. codegraph/cli_output.py +17 -2
  22. codegraph/context.py +38 -11
  23. codegraph/context_freshness.py +2 -0
  24. codegraph/database/__init__.py +14 -0
  25. codegraph/database/extractor.py +712 -17
  26. codegraph/database/interrogation.py +388 -2
  27. codegraph/database/schema_drift.py +862 -0
  28. codegraph/epistemic.py +7 -0
  29. codegraph/evidence_contract.py +21 -0
  30. codegraph/frameworks.py +171 -6
  31. codegraph/frameworks_nestjs.py +502 -0
  32. codegraph/frameworks_nextjs.py +337 -0
  33. codegraph/frameworks_react.py +457 -0
  34. codegraph/freshness.py +2 -0
  35. codegraph/frontend_html.py +109 -0
  36. codegraph/git.py +2 -0
  37. codegraph/git_state.py +2 -0
  38. codegraph/indexing/__init__.py +7 -1
  39. codegraph/indexing/indexer.py +291 -38
  40. codegraph/indexing/models.py +6 -0
  41. codegraph/indexing/parser.py +239 -0
  42. codegraph/indexing/pool.py +136 -0
  43. codegraph/indexing/scanner.py +6 -0
  44. codegraph/indexing/templates.py +418 -0
  45. codegraph/interrogation.py +128 -20
  46. codegraph/mcp/server.py +190 -28
  47. codegraph/models.py +7 -0
  48. codegraph/monorepo.py +3 -0
  49. codegraph/optimizer.py +42 -5
  50. codegraph/process_lifecycle.py +31 -13
  51. codegraph/refactor/__init__.py +36 -0
  52. codegraph/refactor/models.py +114 -0
  53. codegraph/refactor/renamer.py +844 -0
  54. codegraph/refactor/transactions.py +403 -0
  55. codegraph/resolver.py +1202 -159
  56. codegraph/retrieval_policy.py +6 -0
  57. codegraph/route_topology.py +172 -0
  58. codegraph/runtime/__init__.py +19 -0
  59. codegraph/runtime/collector.py +155 -0
  60. codegraph/runtime/db_watcher.py +191 -0
  61. codegraph/runtime/ingestor.py +30 -10
  62. codegraph/runtime/port_inspector.py +304 -0
  63. codegraph/runtime/runner.py +155 -6
  64. codegraph/runtime/trace_linker.py +175 -0
  65. codegraph/search/hybrid.py +26 -0
  66. codegraph/semantic_decorators.py +34 -3
  67. codegraph/structural_diff.py +2 -0
  68. codegraph/styles.py +144 -0
  69. codegraph/symbol_history.py +2 -0
  70. codegraph/tsconfig.py +236 -0
  71. codegraph/ui/__init__.py +6 -0
  72. codegraph/ui/server.py +715 -0
  73. codegraph/watcher.py +323 -0
  74. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +150 -38
  75. codegraph_engine-3.0.0.dist-info/RECORD +129 -0
  76. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  77. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
  78. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
  79. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
  80. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
@@ -521,7 +521,7 @@ def has_potential_orm_models(content: str, file_path: str, language: str = "pyth
521
521
  if language == "python" or file_path.endswith(".py"):
522
522
  return any(tok in content for tok in _PYTHON_ORM_MODEL_TOKENS)
523
523
  if language in ("javascript", "typescript") or file_path.endswith((".js", ".jsx", ".ts", ".tsx")):
524
- return any(tok in content for tok in ("Entity", "define(", ".init(", "model(", "Schema("))
524
+ return any(tok in content for tok in ("Entity", "define(", ".init(", "model(", "Schema(", "pgTable", "mysqlTable", "sqliteTable"))
525
525
  return False
526
526
 
527
527
 
@@ -1294,7 +1294,11 @@ def _record_sqlalchemy_column(
1294
1294
  if acname == "ForeignKey" and arg.args:
1295
1295
  fk_target = _ast_const_str(arg.args[0])
1296
1296
  else:
1297
- data_type = acname
1297
+ if arg.args and isinstance(arg.args[0], (ast.Constant, ast.UnaryOp)):
1298
+ arg_str = _ast_const_str(arg.args[0]) or (str(arg.args[0].value) if isinstance(arg.args[0], ast.Constant) else "")
1299
+ data_type = f"{acname}({arg_str})" if arg_str else acname
1300
+ else:
1301
+ data_type = acname
1298
1302
 
1299
1303
  col_cid = build_db_canonical_id(
1300
1304
  DatabaseEntityKind.COLUMN,
@@ -1683,6 +1687,12 @@ def _record_django_field(
1683
1687
  dialect=dialect,
1684
1688
  schema=schema,
1685
1689
  )
1690
+ max_len_node = _get_kwarg(call_node, "max_length")
1691
+ effective_data_type = field_type
1692
+ if max_len_node is not None:
1693
+ if isinstance(max_len_node, ast.Constant) and isinstance(max_len_node.value, int):
1694
+ effective_data_type = f"{field_type}({max_len_node.value})"
1695
+
1686
1696
  res.entities.append(
1687
1697
  DatabaseEntity(
1688
1698
  canonical_id=col_cid,
@@ -1692,7 +1702,7 @@ def _record_django_field(
1692
1702
  schema_name=schema,
1693
1703
  table_name=table_name,
1694
1704
  column_name=col_name,
1695
- data_type=field_type,
1705
+ data_type=effective_data_type,
1696
1706
  nullable=nullable,
1697
1707
  is_primary_key=is_pk,
1698
1708
  is_unique=is_unique,
@@ -2813,9 +2823,14 @@ def _extract_prisma_schema(
2813
2823
  index_generation: int,
2814
2824
  res: DatabaseExtractionResult,
2815
2825
  ) -> None:
2826
+ ds_m = re.search(r'datasource\s+\w+\s*\{[\s\S]*?provider\s*=\s*"([^"]+)"', content)
2827
+ if ds_m:
2828
+ dialect = normalize_dialect(ds_m.group(1))
2829
+
2816
2830
  for m in re.finditer(r"model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}", content):
2817
2831
  model_name = m.group(1)
2818
2832
  body = m.group(2)
2833
+ model_line = content[: m.start()].count("\n") + 1
2819
2834
  map_m = re.search(r'@@map\(\s*"([^"]+)"\s*\)', body)
2820
2835
  tbl_name = (map_m.group(1) if map_m else _pluralize_table(_camel_to_snake(model_name))).lower()
2821
2836
  tbl_cid = build_db_canonical_id(
@@ -2836,8 +2851,8 @@ def _extract_prisma_schema(
2836
2851
  orm_model_id=model_cid,
2837
2852
  framework="prisma",
2838
2853
  file_path=file_path,
2839
- start_line=1,
2840
- end_line=1,
2854
+ start_line=model_line,
2855
+ end_line=model_line,
2841
2856
  evidence=f"model {model_name}",
2842
2857
  evidence_class="FRAMEWORK_VERIFIED",
2843
2858
  )
@@ -2853,8 +2868,8 @@ def _extract_prisma_schema(
2853
2868
  orm_model_id=model_cid,
2854
2869
  framework="prisma",
2855
2870
  file_path=file_path,
2856
- start_line=1,
2857
- end_line=1,
2871
+ start_line=model_line,
2872
+ end_line=model_line,
2858
2873
  evidence=f"model {model_name}",
2859
2874
  evidence_class="FRAMEWORK_VERIFIED",
2860
2875
  )
@@ -2867,8 +2882,8 @@ def _extract_prisma_schema(
2867
2882
  evidence_class="FRAMEWORK_VERIFIED",
2868
2883
  confidence="HIGH",
2869
2884
  file=file_path,
2870
- start_line=1,
2871
- end_line=1,
2885
+ start_line=model_line,
2886
+ end_line=model_line,
2872
2887
  evidence=f"model {model_name} -> {tbl_name}",
2873
2888
  status="FACT",
2874
2889
  parser_version=PARSER_VERSION,
@@ -2876,6 +2891,543 @@ def _extract_prisma_schema(
2876
2891
  )
2877
2892
  )
2878
2893
 
2894
+ # Pre-scan model body for @relation(fields: [...]) to accurately mark FK columns
2895
+ fk_fields: set[str] = set()
2896
+ for r_m in re.finditer(r'@relation\([\s\S]*?fields:\s*\[([A-Za-z0-9_,\s]+)\]', body):
2897
+ for fld in r_m.group(1).split(","):
2898
+ clean_fld = fld.strip()
2899
+ if clean_fld:
2900
+ fk_fields.add(clean_fld)
2901
+
2902
+ body_start_idx = m.start() + content[m.start() :].find("{") + 1
2903
+ current_offset = body_start_idx
2904
+ for line in body.splitlines():
2905
+ clean = line.strip()
2906
+ field_line = content[:current_offset].count("\n") + 1
2907
+ current_offset += len(line) + 1
2908
+ if not clean or clean.startswith(("//", "@@")):
2909
+ continue
2910
+ field_m = re.match(
2911
+ r"^([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z0-9_]+)(\?)?(\[\])?\s*(.*)$",
2912
+ clean,
2913
+ )
2914
+ if not field_m:
2915
+ continue
2916
+ field_name = field_m.group(1)
2917
+ raw_type = field_m.group(2)
2918
+ is_optional = bool(field_m.group(3))
2919
+ field_attrs = field_m.group(5)
2920
+
2921
+ if raw_type not in (
2922
+ "String", "Int", "BigInt", "Float", "Decimal", "Boolean",
2923
+ "DateTime", "Json", "Bytes",
2924
+ ) and "@relation" in field_attrs and "fields:" not in field_attrs:
2925
+ continue
2926
+
2927
+ map_field_m = re.search(r'@map\(\s*"([^"]+)"\s*\)', field_attrs)
2928
+ col_name = (map_field_m.group(1) if map_field_m else field_name).lower()
2929
+
2930
+ is_pk = bool("@id" in field_attrs or field_name == "id")
2931
+ is_unique = bool("@unique" in field_attrs)
2932
+ nullable = bool(is_optional)
2933
+
2934
+ def_m = re.search(r'@default\(([^)]*)\)', field_attrs)
2935
+ default_val = def_m.group(1).strip() if def_m else None
2936
+
2937
+ tgt_column = None
2938
+ fk_m = re.search(r'@relation\([\s\S]*?references:\s*\[([A-Za-z0-9_,\s]+)\]', field_attrs)
2939
+ if fk_m:
2940
+ tgt_col_raw = fk_m.group(1).split(",")[0].strip()
2941
+ tgt_column = tgt_col_raw
2942
+
2943
+ is_fk = bool(field_name in fk_fields or col_name in fk_fields or tgt_column)
2944
+
2945
+ col_cid = build_db_canonical_id(
2946
+ DatabaseEntityKind.COLUMN,
2947
+ table=tbl_name,
2948
+ column=col_name,
2949
+ dialect=dialect,
2950
+ schema=schema,
2951
+ )
2952
+
2953
+ res.entities.append(
2954
+ DatabaseEntity(
2955
+ canonical_id=col_cid,
2956
+ kind=DatabaseEntityKind.COLUMN.value,
2957
+ name=col_name,
2958
+ dialect=dialect,
2959
+ schema_name=schema,
2960
+ table_name=tbl_name,
2961
+ column_name=col_name,
2962
+ data_type=raw_type,
2963
+ nullable=nullable,
2964
+ default_value=default_val,
2965
+ is_primary_key=is_pk,
2966
+ is_foreign_key=is_fk,
2967
+ is_unique=is_unique,
2968
+ target_table=None,
2969
+ target_column=tgt_column,
2970
+ orm_model_id=model_cid,
2971
+ framework="prisma",
2972
+ file_path=file_path,
2973
+ start_line=field_line,
2974
+ end_line=field_line,
2975
+ evidence=clean,
2976
+ evidence_class="FRAMEWORK_VERIFIED",
2977
+ )
2978
+ )
2979
+
2980
+ res.edges.append(
2981
+ RelationshipRecord(
2982
+ source=tbl_cid,
2983
+ target=col_cid,
2984
+ relationship="MAPS_TO_COLUMN",
2985
+ evidence_class="FRAMEWORK_VERIFIED",
2986
+ confidence="HIGH",
2987
+ file=file_path,
2988
+ start_line=field_line,
2989
+ end_line=field_line,
2990
+ evidence=f"{tbl_name}.{col_name}",
2991
+ status="FACT",
2992
+ parser_version=PARSER_VERSION,
2993
+ index_generation=index_generation,
2994
+ )
2995
+ )
2996
+
2997
+
2998
+ _DRIZZLE_TABLE_RE = re.compile(
2999
+ r"\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(pgTable|mysqlTable|sqliteTable)\s*\(\s*['\"`]([^'\"`]+)['\"`]\s*,\s*\{"
3000
+ )
3001
+
3002
+
3003
+ def _extract_drizzle_schema(
3004
+ content: str,
3005
+ file_path: str,
3006
+ module_name: str,
3007
+ dialect: str,
3008
+ schema: str,
3009
+ index_generation: int,
3010
+ res: DatabaseExtractionResult,
3011
+ ) -> None:
3012
+ for m in _DRIZZLE_TABLE_RE.finditer(content):
3013
+ var_name = m.group(1)
3014
+ helper_fn = m.group(2)
3015
+ tbl_name = m.group(3).strip().lower()
3016
+
3017
+ start_pos = m.end() - 1
3018
+ depth = 0
3019
+ end_pos = start_pos
3020
+ for idx in range(start_pos, len(content)):
3021
+ c = content[idx]
3022
+ if c == "{":
3023
+ depth += 1
3024
+ elif c == "}":
3025
+ depth -= 1
3026
+ if depth == 0:
3027
+ end_pos = idx
3028
+ break
3029
+ body = content[start_pos + 1 : end_pos]
3030
+ line_no = content[: m.start()].count("\n") + 1
3031
+
3032
+ tbl_dialect = "postgres" if helper_fn == "pgTable" else ("mysql" if helper_fn == "mysqlTable" else "sqlite")
3033
+ tbl_cid = build_db_canonical_id(
3034
+ DatabaseEntityKind.TABLE,
3035
+ table=tbl_name,
3036
+ dialect=tbl_dialect,
3037
+ schema=schema,
3038
+ )
3039
+ model_cid = f"drizzle.{var_name}"
3040
+
3041
+ res.entities.append(
3042
+ DatabaseEntity(
3043
+ canonical_id=model_cid,
3044
+ kind=DatabaseEntityKind.ORM_MODEL.value,
3045
+ name=var_name,
3046
+ dialect=tbl_dialect,
3047
+ schema_name=schema,
3048
+ table_name=tbl_name,
3049
+ orm_model_id=model_cid,
3050
+ framework="drizzle",
3051
+ file_path=file_path,
3052
+ start_line=line_no,
3053
+ end_line=line_no,
3054
+ evidence=f"{var_name} = {helper_fn}('{tbl_name}')",
3055
+ evidence_class="FRAMEWORK_VERIFIED",
3056
+ )
3057
+ )
3058
+ res.entities.append(
3059
+ DatabaseEntity(
3060
+ canonical_id=tbl_cid,
3061
+ kind=DatabaseEntityKind.TABLE.value,
3062
+ name=tbl_name,
3063
+ dialect=tbl_dialect,
3064
+ schema_name=schema,
3065
+ table_name=tbl_name,
3066
+ orm_model_id=model_cid,
3067
+ framework="drizzle",
3068
+ file_path=file_path,
3069
+ start_line=line_no,
3070
+ end_line=line_no,
3071
+ evidence=f"{var_name} = {helper_fn}('{tbl_name}')",
3072
+ evidence_class="FRAMEWORK_VERIFIED",
3073
+ )
3074
+ )
3075
+ res.edges.append(
3076
+ RelationshipRecord(
3077
+ source=model_cid,
3078
+ target=tbl_cid,
3079
+ relationship="MAPS_TO_TABLE",
3080
+ evidence_class="FRAMEWORK_VERIFIED",
3081
+ confidence="HIGH",
3082
+ file=file_path,
3083
+ start_line=line_no,
3084
+ end_line=line_no,
3085
+ evidence=f"{var_name} -> {tbl_name}",
3086
+ status="FACT",
3087
+ parser_version=PARSER_VERSION,
3088
+ index_generation=index_generation,
3089
+ )
3090
+ )
3091
+
3092
+ # Parse column definitions supporting multi-line chained method calls
3093
+ col_entries = re.finditer(
3094
+ r"(?:^|\n)\s*([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\s*\(([\s\S]*?)(?=(?:\n\s*[A-Za-z_$][\w$]*\s*:|\Z))",
3095
+ body,
3096
+ )
3097
+ for ce in col_entries:
3098
+ prop_key = ce.group(1)
3099
+ col_type = ce.group(2)
3100
+ rest = ce.group(3)
3101
+
3102
+ arg_m = re.match(r"^\s*['\"`]([^'\"`]+)['\"`]", rest)
3103
+ arg_col_name = arg_m.group(1) if arg_m else None
3104
+ col_name = (arg_col_name if arg_col_name else prop_key).lower()
3105
+
3106
+ is_pk = bool(".primaryKey(" in rest or prop_key == "id")
3107
+ is_unq = bool(".unique(" in rest)
3108
+ nullable = not bool(".notNull(" in rest or is_pk)
3109
+
3110
+ def_val = None
3111
+ if ".defaultNow(" in rest:
3112
+ def_val = "now()"
3113
+ elif ".default(" in rest:
3114
+ dm = re.search(r'\.default\(([^)]*)\)', rest)
3115
+ if dm:
3116
+ def_val = dm.group(1).strip()
3117
+
3118
+ tgt_tbl = None
3119
+ tgt_col = None
3120
+ if ".references(" in rest:
3121
+ ref_m = re.search(r'\.references\(\s*(?:\(\)\s*=>\s*)?([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)', rest)
3122
+ if ref_m:
3123
+ tgt_tbl = ref_m.group(1).lower()
3124
+ tgt_col = ref_m.group(2).lower()
3125
+
3126
+ col_cid = build_db_canonical_id(
3127
+ DatabaseEntityKind.COLUMN,
3128
+ table=tbl_name,
3129
+ column=col_name,
3130
+ dialect=tbl_dialect,
3131
+ schema=schema,
3132
+ )
3133
+ res.entities.append(
3134
+ DatabaseEntity(
3135
+ canonical_id=col_cid,
3136
+ kind=DatabaseEntityKind.COLUMN.value,
3137
+ name=col_name,
3138
+ dialect=tbl_dialect,
3139
+ schema_name=schema,
3140
+ table_name=tbl_name,
3141
+ column_name=col_name,
3142
+ data_type=col_type,
3143
+ nullable=nullable,
3144
+ default_value=def_val,
3145
+ is_primary_key=is_pk,
3146
+ is_foreign_key=bool(tgt_tbl),
3147
+ is_unique=is_unq,
3148
+ target_table=tgt_tbl,
3149
+ target_column=tgt_col,
3150
+ orm_model_id=model_cid,
3151
+ framework="drizzle",
3152
+ file_path=file_path,
3153
+ start_line=line_no,
3154
+ end_line=line_no,
3155
+ evidence=f"{prop_key}: {col_type}",
3156
+ evidence_class="FRAMEWORK_VERIFIED",
3157
+ )
3158
+ )
3159
+ res.edges.append(
3160
+ RelationshipRecord(
3161
+ source=tbl_cid,
3162
+ target=col_cid,
3163
+ relationship="MAPS_TO_COLUMN",
3164
+ evidence_class="FRAMEWORK_VERIFIED",
3165
+ confidence="HIGH",
3166
+ file=file_path,
3167
+ start_line=line_no,
3168
+ end_line=line_no,
3169
+ evidence=f"{tbl_name}.{col_name}",
3170
+ status="FACT",
3171
+ parser_version=PARSER_VERSION,
3172
+ index_generation=index_generation,
3173
+ )
3174
+ )
3175
+
3176
+
3177
+ def _extract_mongoose_schema(
3178
+ content: str,
3179
+ file_path: str,
3180
+ module_name: str,
3181
+ index_generation: int,
3182
+ res: DatabaseExtractionResult,
3183
+ model_to_table: dict[str, str],
3184
+ ) -> None:
3185
+ if "mongoose" not in content and "Schema" not in content and "model" not in content:
3186
+ return
3187
+
3188
+ res.detected_dialects.add("mongodb")
3189
+
3190
+ schema_blocks: dict[str, tuple[str, int, int]] = {}
3191
+ objects: dict[str, tuple[str, int, int]] = {}
3192
+
3193
+ # 1. Discover all top-level object declarations: const/let/var/export const <name>[: type] = { ... }
3194
+ for m in re.finditer(
3195
+ r"(?:(?:export\s+)?(?:const|let|var))\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=;]+)?\s*=\s*\{",
3196
+ content,
3197
+ ):
3198
+ name = m.group(1)
3199
+ start = m.end() - 1
3200
+ depth = 0
3201
+ end = start
3202
+ for idx in range(start, len(content)):
3203
+ if content[idx] == "{":
3204
+ depth += 1
3205
+ elif content[idx] == "}":
3206
+ depth -= 1
3207
+ if depth == 0:
3208
+ end = idx
3209
+ break
3210
+ start_ln = content.count("\n", 0, m.start()) + 1
3211
+ end_ln = content.count("\n", 0, end) + 1
3212
+ objects[name] = (content[start + 1 : end], start_ln, end_ln)
3213
+
3214
+ # 2. Discover all Schema declarations: const/let/var/export <name>[: type] = new (mongoose.)?Schema(...)
3215
+ for m in re.finditer(
3216
+ r"(?:(?:export\s+)?(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)(?:\s*:\s*[^=;]+)?\s*=\s*new\s+(?:mongoose\.)?Schema\s*(?:<[^>]+>)?\s*\(\s*([A-Za-z_$][\w$]*|\{)?",
3217
+ content,
3218
+ ):
3219
+ s_var = m.group(1)
3220
+ arg = m.group(2)
3221
+ if arg == "{":
3222
+ start_pos = m.end() - 1
3223
+ depth = 0
3224
+ end_pos = start_pos
3225
+ for idx in range(start_pos, len(content)):
3226
+ c = content[idx]
3227
+ if c == "{":
3228
+ depth += 1
3229
+ elif c == "}":
3230
+ depth -= 1
3231
+ if depth == 0:
3232
+ end_pos = idx
3233
+ break
3234
+ schema_body = content[start_pos + 1 : end_pos]
3235
+ start_ln = content.count("\n", 0, m.start()) + 1
3236
+ end_ln = content.count("\n", 0, end_pos) + 1
3237
+ schema_blocks[s_var] = (schema_body, start_ln, end_ln)
3238
+ elif arg and arg in objects:
3239
+ schema_blocks[s_var] = objects[arg]
3240
+
3241
+ for mm in re.finditer(
3242
+ r"(?:(?:(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=;]+)?\s*=\s*)|(?:export\s+default\s+))?(?:mongoose\.)?model\s*(?:<[^>]+>)?\s*\(\s*['\"]([A-Za-z0-9_]+)['\"]\s*(?:,\s*([^\),\n]+))?",
3243
+ content,
3244
+ ):
3245
+ var_name = mm.group(1)
3246
+ model_name = mm.group(2)
3247
+ raw_schema_arg = (mm.group(3) or "").strip()
3248
+ schema_var = raw_schema_arg
3249
+
3250
+ coll_name = _pluralize_table(_camel_to_snake(model_name)).lower()
3251
+ if var_name:
3252
+ model_to_table[var_name] = coll_name
3253
+ model_to_table[model_name] = coll_name
3254
+ model_to_table[model_name.lower()] = coll_name
3255
+
3256
+ m_line = content.count("\n", 0, mm.start()) + 1
3257
+ tbl_cid = f"db.UNKNOWN.UNKNOWN.{coll_name}"
3258
+ mdl_cid = f"{tbl_cid}.{model_name}"
3259
+
3260
+ res.entities.append(
3261
+ DatabaseEntity(
3262
+ kind=DatabaseEntityKind.TABLE,
3263
+ dialect="mongodb",
3264
+ schema_name="UNKNOWN",
3265
+ table_name=coll_name,
3266
+ canonical_id=tbl_cid,
3267
+ name=coll_name,
3268
+ file_path=file_path,
3269
+ start_line=m_line,
3270
+ end_line=m_line,
3271
+ confidence="HIGH",
3272
+ evidence=redact_secrets(mm.group(0)),
3273
+ evidence_class="FRAMEWORK_VERIFIED",
3274
+ status="FACT",
3275
+ )
3276
+ )
3277
+
3278
+ res.entities.append(
3279
+ DatabaseEntity(
3280
+ kind=DatabaseEntityKind.ORM_MODEL,
3281
+ dialect="mongodb",
3282
+ schema_name="UNKNOWN",
3283
+ table_name=coll_name,
3284
+ canonical_id=mdl_cid,
3285
+ name=model_name,
3286
+ target_table=coll_name,
3287
+ file_path=file_path,
3288
+ start_line=m_line,
3289
+ end_line=m_line,
3290
+ confidence="HIGH",
3291
+ evidence=redact_secrets(mm.group(0)),
3292
+ evidence_class="FRAMEWORK_VERIFIED",
3293
+ status="FACT",
3294
+ )
3295
+ )
3296
+
3297
+ res.edges.append(
3298
+ RelationshipRecord(
3299
+ source=mdl_cid,
3300
+ target=tbl_cid,
3301
+ relationship="MAPS_TO_COLLECTION",
3302
+ confidence="HIGH",
3303
+ evidence_class="FRAMEWORK_VERIFIED",
3304
+ status="FACT",
3305
+ file=file_path,
3306
+ start_line=m_line,
3307
+ end_line=m_line,
3308
+ evidence=redact_secrets(mm.group(0)),
3309
+ reason=f"mongoose_model_to_collection:{coll_name}",
3310
+ parser_version=PARSER_VERSION,
3311
+ index_generation=index_generation,
3312
+ )
3313
+ )
3314
+
3315
+ # Resolve body: schema_blocks, objects, or inline
3316
+ body_tuple: tuple[str, int, int] | None = None
3317
+ if schema_var in schema_blocks:
3318
+ body_tuple = schema_blocks[schema_var]
3319
+ elif schema_var in objects:
3320
+ body_tuple = objects[schema_var]
3321
+ elif "{" in content[mm.start() : mm.start() + 200]:
3322
+ # Inline schema: new Schema({ ... }) or { ... }
3323
+ inline_start = content.find("{", mm.start())
3324
+ if inline_start != -1 and inline_start < mm.start() + 200:
3325
+ depth = 0
3326
+ end_pos = inline_start
3327
+ for idx in range(inline_start, len(content)):
3328
+ c = content[idx]
3329
+ if c == "{":
3330
+ depth += 1
3331
+ elif c == "}":
3332
+ depth -= 1
3333
+ if depth == 0:
3334
+ end_pos = idx
3335
+ break
3336
+ body_tuple = (
3337
+ content[inline_start + 1 : end_pos],
3338
+ content.count("\n", 0, inline_start) + 1,
3339
+ content.count("\n", 0, end_pos) + 1,
3340
+ )
3341
+
3342
+ if body_tuple is not None:
3343
+ body, s_start, s_end = body_tuple
3344
+ for f_match in re.finditer(
3345
+ r"([A-Za-z_$][\w$]*)\s*:\s*(?:\{\s*([^}]+)\}|([A-Za-z0-9_$.]+))",
3346
+ body,
3347
+ ):
3348
+ f_name = f_match.group(1)
3349
+ f_inner = f_match.group(2)
3350
+ f_direct = f_match.group(3)
3351
+ f_type = "Mixed"
3352
+ f_rest = ""
3353
+ if f_inner:
3354
+ type_m = re.search(r"type\s*:\s*([A-Za-z0-9_$.]+)", f_inner)
3355
+ f_type = type_m.group(1) if type_m else "Mixed"
3356
+ f_rest = f_inner
3357
+ elif f_direct:
3358
+ f_type = f_direct
3359
+
3360
+ col_cid = f"{tbl_cid}.{f_name}"
3361
+ is_req = bool(re.search(r"required\s*:\s*true", f_rest, re.IGNORECASE))
3362
+ is_uniq = bool(re.search(r"unique\s*:\s*true", f_rest, re.IGNORECASE))
3363
+ ref_match = re.search(r"ref\s*:\s*['\"]([A-Za-z0-9_]+)['\"]", f_rest)
3364
+ target_coll: str | None = None
3365
+ if ref_match:
3366
+ ref_model = ref_match.group(1)
3367
+ target_coll = _pluralize_table(_camel_to_snake(ref_model)).lower()
3368
+
3369
+ res.entities.append(
3370
+ DatabaseEntity(
3371
+ kind=DatabaseEntityKind.COLUMN,
3372
+ dialect="mongodb",
3373
+ schema_name="UNKNOWN",
3374
+ table_name=coll_name,
3375
+ column_name=f_name,
3376
+ data_type=f_type,
3377
+ nullable=bool(not is_req),
3378
+ is_primary_key=bool(f_name == "_id"),
3379
+ is_unique=bool(is_uniq),
3380
+ is_foreign_key=bool(target_coll is not None),
3381
+ target_table=target_coll,
3382
+ canonical_id=col_cid,
3383
+ name=f_name,
3384
+ file_path=file_path,
3385
+ start_line=s_start,
3386
+ end_line=s_end,
3387
+ confidence="HIGH",
3388
+ evidence=redact_secrets(f_match.group(0)),
3389
+ evidence_class="FRAMEWORK_VERIFIED",
3390
+ status="FACT",
3391
+ )
3392
+ )
3393
+
3394
+ res.edges.append(
3395
+ RelationshipRecord(
3396
+ source=tbl_cid,
3397
+ target=col_cid,
3398
+ relationship="MAPS_TO_COLUMN",
3399
+ confidence="HIGH",
3400
+ evidence_class="FRAMEWORK_VERIFIED",
3401
+ status="FACT",
3402
+ file=file_path,
3403
+ start_line=s_start,
3404
+ end_line=s_end,
3405
+ evidence=redact_secrets(f_match.group(0)),
3406
+ reason=f"mongoose_schema_field:{f_name}",
3407
+ parser_version=PARSER_VERSION,
3408
+ index_generation=index_generation,
3409
+ )
3410
+ )
3411
+
3412
+ if target_coll:
3413
+ res.edges.append(
3414
+ RelationshipRecord(
3415
+ source=col_cid,
3416
+ target=f"db.UNKNOWN.UNKNOWN.{target_coll}",
3417
+ relationship="FOREIGN_KEY_TO",
3418
+ confidence="HIGH",
3419
+ evidence_class="FRAMEWORK_VERIFIED",
3420
+ status="FACT",
3421
+ file=file_path,
3422
+ start_line=s_start,
3423
+ end_line=s_end,
3424
+ evidence=redact_secrets(f_match.group(0)),
3425
+ reason=f"mongoose_ref:{target_coll}",
3426
+ parser_version=PARSER_VERSION,
3427
+ index_generation=index_generation,
3428
+ )
3429
+ )
3430
+
2879
3431
 
2880
3432
  def _extract_js_ts_database(
2881
3433
  content: str,
@@ -2887,23 +3439,68 @@ def _extract_js_ts_database(
2887
3439
  index_generation: int,
2888
3440
  res: DatabaseExtractionResult,
2889
3441
  ) -> None:
2890
- del model_to_table
3442
+ _extract_drizzle_schema(
3443
+ content,
3444
+ file_path,
3445
+ module_name,
3446
+ dialect,
3447
+ schema,
3448
+ index_generation,
3449
+ res,
3450
+ )
3451
+ _extract_mongoose_schema(
3452
+ content,
3453
+ file_path,
3454
+ module_name,
3455
+ index_generation,
3456
+ res,
3457
+ model_to_table,
3458
+ )
3459
+
2891
3460
  lines = content.splitlines()
3461
+ active_scope: str = module_name
3462
+
2892
3463
  for line_no, raw_line in enumerate(lines, start=1):
2893
3464
  ev = redact_secrets(raw_line.strip())
2894
- # Prisma client: prisma.user.findMany / prisma.order.create / update / delete
3465
+
3466
+ fn_match = re.search(
3467
+ r"(?:async\s+)?function\s+([A-Za-z_$][\w$]*)|(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>",
3468
+ raw_line,
3469
+ )
3470
+ if fn_match:
3471
+ fn_name = fn_match.group(1) or fn_match.group(2)
3472
+ if fn_name:
3473
+ active_scope = f"{module_name}.{fn_name}"
3474
+ else:
3475
+ meth_match = re.search(
3476
+ r"^\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::\s*[^{]+)?\{",
3477
+ raw_line,
3478
+ )
3479
+ if meth_match:
3480
+ m_name = meth_match.group(1)
3481
+ if m_name not in ("if", "for", "while", "switch", "catch", "constructor"):
3482
+ active_scope = f"{module_name}.{m_name}"
3483
+
3484
+ # 1. Prisma Client: (this.|ctx.|db.)?prisma.<model>.<action>(...)
2895
3485
  for pm in re.finditer(
2896
- r"\bprisma\.([a-zA-Z_][a-zA-Z0-9_]*)\.(findMany|findUnique|findFirst|count|create|createMany|update|updateMany|upsert|delete|deleteMany)\b",
3486
+ r"\b(?:this\.|ctx\.|db\.)?prisma\.([a-zA-Z_][a-zA-Z0-9_]*)\.(findMany|findUnique|findUniqueOrThrow|findFirst|findFirstOrThrow|count|aggregate|groupBy|create|createMany|createManyAndReturn|update|updateMany|upsert|delete|deleteMany)\b",
2897
3487
  raw_line,
2898
3488
  ):
2899
3489
  model_prop = pm.group(1)
2900
3490
  method = pm.group(2)
2901
- tbl = _pluralize_table(_camel_to_snake(model_prop))
2902
- is_read = method.startswith(("find", "count"))
3491
+ tbl = model_to_table.get(
3492
+ model_prop,
3493
+ model_to_table.get(
3494
+ model_prop.lower(),
3495
+ _pluralize_table(_camel_to_snake(model_prop)),
3496
+ ),
3497
+ ).lower()
3498
+
3499
+ is_read = method.startswith(("find", "count", "aggregate", "groupBy"))
2903
3500
  op = "SELECT" if is_read else ("INSERT" if "create" in method else ("DELETE" if "delete" in method else "UPDATE"))
2904
3501
  rel = "READS_TABLE" if is_read else "WRITES_TABLE"
2905
3502
  _emit_query_fact_and_edges(
2906
- caller_id=module_name,
3503
+ caller_id=active_scope,
2907
3504
  operation=op,
2908
3505
  relationship=rel,
2909
3506
  table_name=tbl,
@@ -2923,7 +3520,71 @@ def _extract_js_ts_database(
2923
3520
  index_generation=index_generation,
2924
3521
  res=res,
2925
3522
  )
2926
- # Knex: knex("users").select / insert / update / del
3523
+
3524
+ # 2. Drizzle Query Builder: db.select().from(tableVar) / db.insert(tableVar) / db.update / db.delete
3525
+ for dm in re.finditer(
3526
+ r"\bdb(?:\.[a-zA-Z0-9_]+)?\.(select|insert|update|delete)\s*(?:\([^)]*\))?(?:\s*\.from\s*\(\s*([A-Za-z_$][\w$]*)\s*\)|\s*\(\s*([A-Za-z_$][\w$]*)\s*\))",
3527
+ raw_line,
3528
+ ):
3529
+ action = dm.group(1)
3530
+ table_var = dm.group(2) or dm.group(3)
3531
+ if table_var:
3532
+ tbl = model_to_table.get(table_var, table_var).lower()
3533
+ is_read = (action == "select")
3534
+ op = "SELECT" if is_read else ("INSERT" if action == "insert" else ("DELETE" if action == "delete" else "UPDATE"))
3535
+ rel = "READS_TABLE" if is_read else "WRITES_TABLE"
3536
+ _emit_query_fact_and_edges(
3537
+ caller_id=active_scope,
3538
+ operation=op,
3539
+ relationship=rel,
3540
+ table_name=tbl,
3541
+ columns=(),
3542
+ read_columns=(),
3543
+ framework="drizzle",
3544
+ normalized_sql=f"{op} {tbl}",
3545
+ dialect=dialect,
3546
+ schema=schema,
3547
+ file_path=file_path,
3548
+ start_line=line_no,
3549
+ end_line=line_no,
3550
+ evidence=ev,
3551
+ confidence="HIGH",
3552
+ evidence_class="FRAMEWORK_VERIFIED",
3553
+ status="FACT",
3554
+ index_generation=index_generation,
3555
+ res=res,
3556
+ )
3557
+
3558
+ # 3. Drizzle Relational Queries: db.query.<tableVar>.(findMany|findFirst)(...)
3559
+ for dqm in re.finditer(
3560
+ r"\bdb(?:\.[a-zA-Z0-9_]+)?\.query\.([A-Za-z_$][\w$]*)\.(findMany|findFirst)\b",
3561
+ raw_line,
3562
+ ):
3563
+ table_var = dqm.group(1)
3564
+ tbl = model_to_table.get(table_var, table_var).lower()
3565
+ _emit_query_fact_and_edges(
3566
+ caller_id=active_scope,
3567
+ operation="SELECT",
3568
+ relationship="READS_TABLE",
3569
+ table_name=tbl,
3570
+ columns=(),
3571
+ read_columns=(),
3572
+ framework="drizzle",
3573
+ normalized_sql=f"SELECT {tbl}",
3574
+ dialect=dialect,
3575
+ schema=schema,
3576
+ file_path=file_path,
3577
+ start_line=line_no,
3578
+ end_line=line_no,
3579
+ evidence=ev,
3580
+ confidence="HIGH",
3581
+ evidence_class="FRAMEWORK_VERIFIED",
3582
+ status="FACT",
3583
+ index_generation=index_generation,
3584
+ res=res,
3585
+ )
3586
+
3587
+ # 4. Knex: knex("users").select / insert / update / del
2927
3588
  for km in re.finditer(
2928
3589
  r'\bknex\(\s*["\']([a-zA-Z0-9_]+)["\']\s*\)\.(select|where|first|insert|update|del|delete)\b',
2929
3590
  raw_line,
@@ -2934,7 +3595,7 @@ def _extract_js_ts_database(
2934
3595
  op = "SELECT" if is_read else ("INSERT" if method == "insert" else ("DELETE" if method in ("del", "delete") else "UPDATE"))
2935
3596
  rel = "READS_TABLE" if is_read else "WRITES_TABLE"
2936
3597
  _emit_query_fact_and_edges(
2937
- caller_id=module_name,
3598
+ caller_id=active_scope,
2938
3599
  operation=op,
2939
3600
  relationship=rel,
2940
3601
  table_name=tbl,
@@ -2954,3 +3615,37 @@ def _extract_js_ts_database(
2954
3615
  index_generation=index_generation,
2955
3616
  res=res,
2956
3617
  )
3618
+
3619
+ # 5. Mongoose Document Queries: Model.(find|findOne|findById|create|updateOne|deleteOne|...)
3620
+ for mgm in re.finditer(
3621
+ r"\b([A-Z][a-zA-Z0-9_]*)\.(find|findOne|findById|countDocuments|aggregate|create|insertOne|insertMany|updateOne|updateMany|deleteOne|deleteMany|findByIdAndUpdate|findByIdAndDelete)\b",
3622
+ raw_line,
3623
+ ):
3624
+ model_ident = mgm.group(1)
3625
+ action = mgm.group(2)
3626
+ if model_ident in model_to_table or f"{model_ident.lower()}s" in model_to_table.values():
3627
+ tbl = model_to_table.get(model_ident, _pluralize_table(_camel_to_snake(model_ident))).lower()
3628
+ is_read = action.startswith(("find", "count", "aggregate"))
3629
+ op = "SELECT" if is_read else ("INSERT" if "create" in action or "insert" in action else ("DELETE" if "delete" in action else "UPDATE"))
3630
+ rel = "READS_COLLECTION" if is_read else "WRITES_COLLECTION"
3631
+ _emit_query_fact_and_edges(
3632
+ caller_id=active_scope,
3633
+ operation=op,
3634
+ relationship=rel,
3635
+ table_name=tbl,
3636
+ columns=(),
3637
+ read_columns=(),
3638
+ framework="mongoose",
3639
+ normalized_sql=f"{op} {tbl}",
3640
+ dialect="mongodb",
3641
+ schema="UNKNOWN",
3642
+ file_path=file_path,
3643
+ start_line=line_no,
3644
+ end_line=line_no,
3645
+ evidence=ev,
3646
+ confidence="HIGH",
3647
+ evidence_class="FRAMEWORK_VERIFIED",
3648
+ status="FACT",
3649
+ index_generation=index_generation,
3650
+ res=res,
3651
+ )