codegraph-engine 2.1.2__tar.gz → 2.1.3__tar.gz

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 (115) hide show
  1. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/PKG-INFO +1 -1
  2. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/pyproject.toml +1 -1
  3. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/__init__.py +1 -1
  4. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/frameworks.py +13 -7
  5. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/indexer.py +27 -1
  6. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/PKG-INFO +1 -1
  7. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/SOURCES.txt +1 -0
  8. codegraph_engine-2.1.3/tests/test_framework_routes_persistence.py +363 -0
  9. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_platform_resources.py +1 -1
  10. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/LICENSE +0 -0
  11. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/README.md +0 -0
  12. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/setup.cfg +0 -0
  13. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/agent.py +0 -0
  14. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/architecture.py +0 -0
  15. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/audit.py +0 -0
  16. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/cache.py +0 -0
  17. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/cli.py +0 -0
  18. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/config.py +0 -0
  19. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/constraints.py +0 -0
  20. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/context.py +0 -0
  21. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/epistemic.py +0 -0
  22. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/errors.py +0 -0
  23. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/evidence/__init__.py +0 -0
  24. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/evidence/citations.py +0 -0
  25. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/freshness.py +0 -0
  26. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/git.py +0 -0
  27. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/graph/__init__.py +0 -0
  28. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/graph/models.py +0 -0
  29. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/graph/traversal.py +0 -0
  30. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/__init__.py +0 -0
  31. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/classifier.py +0 -0
  32. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/models.py +0 -0
  33. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/parser.py +0 -0
  34. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/scanner.py +0 -0
  35. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/test_framework.py +0 -0
  36. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/interrogation.py +0 -0
  37. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/llm/__init__.py +0 -0
  38. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/llm/base.py +0 -0
  39. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/llm/context.py +0 -0
  40. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/mcp/__init__.py +0 -0
  41. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/mcp/server.py +0 -0
  42. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/memory/__init__.py +0 -0
  43. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/memory/store.py +0 -0
  44. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/models.py +0 -0
  45. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/observability.py +0 -0
  46. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/optimizer.py +0 -0
  47. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/planner.py +0 -0
  48. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/py.typed +0 -0
  49. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/query_expansion.py +0 -0
  50. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/ranking.py +0 -0
  51. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resolver.py +0 -0
  52. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/__init__.py +0 -0
  53. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/cache.py +0 -0
  54. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/coalescer.py +0 -0
  55. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/debouncer.py +0 -0
  56. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/governor.py +0 -0
  57. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/monitor.py +0 -0
  58. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/policy.py +0 -0
  59. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/retrieval_policy.py +0 -0
  60. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/search/__init__.py +0 -0
  61. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/search/hybrid.py +0 -0
  62. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/search/semantic.py +0 -0
  63. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/security/__init__.py +0 -0
  64. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/security/paths.py +0 -0
  65. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/target_resolver.py +0 -0
  66. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/task.py +0 -0
  67. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/dependency_links.txt +0 -0
  68. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/entry_points.txt +0 -0
  69. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/requires.txt +0 -0
  70. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/top_level.txt +0 -0
  71. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_active_coding_protection.py +0 -0
  72. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_adaptive_planning.py +0 -0
  73. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_adversarial_edge_cases.py +0 -0
  74. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_agent_ux_hardening.py +0 -0
  75. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_benchmark_infra.py +0 -0
  76. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_bounded_caches.py +0 -0
  77. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_cli_doctor_privacy.py +0 -0
  78. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_context_budget.py +0 -0
  79. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_context_cache.py +0 -0
  80. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_context_compiler_v2.py +0 -0
  81. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_core.py +0 -0
  82. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_database_integrity.py +0 -0
  83. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_debouncer.py +0 -0
  84. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_determinism_and_soak.py +0 -0
  85. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_developer_audit.py +0 -0
  86. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_evaluation_framework.py +0 -0
  87. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_framework_analyzers.py +0 -0
  88. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_git_intelligence.py +0 -0
  89. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_hardening.py +0 -0
  90. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_interrogation_contracts.py +0 -0
  91. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_latency_modes_and_parallel.py +0 -0
  92. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_mcp_integration.py +0 -0
  93. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_phase2.py +0 -0
  94. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_ranking_engine.py +0 -0
  95. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_reference_resolution.py +0 -0
  96. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_request_coalescer.py +0 -0
  97. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_resource_governor.py +0 -0
  98. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_retrieval_planner.py +0 -0
  99. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_scanner_security.py +0 -0
  100. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_single_pass_parser.py +0 -0
  101. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_symbol_identity.py +0 -0
  102. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_ambiguity.py +0 -0
  103. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_mcp_tools.py +0 -0
  104. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_normalization.py +0 -0
  105. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_spec.py +0 -0
  106. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_factory_resolution.py +0 -0
  107. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_features.py +0 -0
  108. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_imports_dependents_cli.py +0 -0
  109. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_recursive_frameworks.py +0 -0
  110. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_diagnostics.py +0 -0
  111. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_hard_exclusions.py +0 -0
  112. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_query_expansion.py +0 -0
  113. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_retrieval_policy.py +0 -0
  114. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_target_resolver.py +0 -0
  115. {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_verify_evidence.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraph-engine
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: Evidence-backed local codebase intelligence for MCP clients
5
5
  Author: CodeGraph contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codegraph-engine"
7
- version = "2.1.2"
7
+ version = "2.1.3"
8
8
  description = "Evidence-backed local codebase intelligence for MCP clients"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  """CodeGraph MCP: local, evidence-backed codebase intelligence."""
2
2
 
3
- __version__ = "2.1.2"
3
+ __version__ = "2.1.3"
4
4
  from codegraph.errors import (
5
5
  CodeGraphError,
6
6
  ErrorCode,
@@ -79,6 +79,7 @@ class RouteDetection:
79
79
  resolution_status: str = "RESOLVED" # RESOLVED | UNRESOLVED | AMBIGUOUS
80
80
  evidence: str = ""
81
81
  module: str = ""
82
+ endpoint_id_override: str | None = None
82
83
 
83
84
  @property
84
85
  def route_signature(self) -> str:
@@ -87,13 +88,18 @@ class RouteDetection:
87
88
 
88
89
  @property
89
90
  def endpoint_id(self) -> str:
90
- """Stable, globally unique endpoint identifier scoped by module to prevent collisions."""
91
- if self.module:
92
- mod = self.module
93
- else:
94
- from codegraph.indexing.models import normalize_module
95
- mod = normalize_module(self.file_path)
96
- return f"API_ENDPOINT:{mod}:{self.http_method} {self.normalized_route}"
91
+ """Stable, deterministic, globally unique endpoint identifier for a route declaration."""
92
+ if self.endpoint_id_override:
93
+ return self.endpoint_id_override
94
+
95
+ clean_path = self.file_path.replace("\\", "/").strip()
96
+ while clean_path.startswith("./"):
97
+ clean_path = clean_path[2:]
98
+ clean_path = clean_path.lstrip("/") or "root"
99
+
100
+ handler_part = f"#{self.handler_name}" if self.handler_name else ""
101
+ fw_part = f":{self.framework}" if self.framework else ""
102
+ return f"API_ENDPOINT{fw_part}:{clean_path}:{self.line}:{self.http_method} {self.normalized_route}{handler_part}"
97
103
 
98
104
  def as_dict(self) -> dict[str, object]:
99
105
  return {
@@ -655,7 +655,18 @@ class Indexer:
655
655
  con.executemany(
656
656
  "INSERT INTO framework_routes(endpoint_id, framework, http_method, route_path, "
657
657
  "normalized_route, handler_name, handler_canonical_id, file_path, line, evidence, confidence) "
658
- "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
658
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?) "
659
+ "ON CONFLICT(endpoint_id) DO UPDATE SET "
660
+ "framework=excluded.framework, "
661
+ "http_method=excluded.http_method, "
662
+ "route_path=excluded.route_path, "
663
+ "normalized_route=excluded.normalized_route, "
664
+ "handler_name=excluded.handler_name, "
665
+ "handler_canonical_id=excluded.handler_canonical_id, "
666
+ "file_path=excluded.file_path, "
667
+ "line=excluded.line, "
668
+ "evidence=excluded.evidence, "
669
+ "confidence=excluded.confidence",
659
670
  [
660
671
  (
661
672
  r.endpoint_id,
@@ -794,6 +805,7 @@ class Indexer:
794
805
  line=int(r["line"]),
795
806
  evidence=str(r["evidence"]),
796
807
  confidence=str(r["confidence"]),
808
+ endpoint_id_override=str(r["endpoint_id"]),
797
809
  )
798
810
  for r in rt_rows
799
811
  ]
@@ -930,6 +942,20 @@ def check_database_health(con: sqlite3.Connection, repository: Path | None = Non
930
942
  if parser_ver and parser_ver != PARSER_VERSION:
931
943
  issues.append(f"Parser version mismatch: indexed with {parser_ver}, active parser is {PARSER_VERSION}.")
932
944
 
945
+ # 8. Orphan framework routes
946
+ orphan_routes = con.execute(
947
+ "SELECT count(*) FROM framework_routes r LEFT JOIN files f ON r.file_path = f.path WHERE f.path IS NULL"
948
+ ).fetchone()[0]
949
+ if orphan_routes > 0:
950
+ issues.append(f"Found {orphan_routes} orphan route(s) referencing non-existent files.")
951
+
952
+ # 9. Duplicate endpoint IDs
953
+ dup_routes = con.execute(
954
+ "SELECT endpoint_id, count(*) FROM framework_routes GROUP BY endpoint_id HAVING count(*) > 1"
955
+ ).fetchall()
956
+ if dup_routes:
957
+ issues.append(f"Found {len(dup_routes)} duplicate route endpoint ID(s).")
958
+
933
959
  status = "OK" if not issues else "ISSUES_FOUND"
934
960
  return {
935
961
  "status": status,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraph-engine
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: Evidence-backed local codebase intelligence for MCP clients
5
5
  Author: CodeGraph contributors
6
6
  License-Expression: MIT
@@ -81,6 +81,7 @@ tests/test_determinism_and_soak.py
81
81
  tests/test_developer_audit.py
82
82
  tests/test_evaluation_framework.py
83
83
  tests/test_framework_analyzers.py
84
+ tests/test_framework_routes_persistence.py
84
85
  tests/test_git_intelligence.py
85
86
  tests/test_hardening.py
86
87
  tests/test_interrogation_contracts.py
@@ -0,0 +1,363 @@
1
+ """Regression tests for framework route identity, collision prevention, and idempotent persistence."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from typer.testing import CliRunner
7
+
8
+ from codegraph.cli import app
9
+ from codegraph.frameworks import RouteDetection, analyze_js_ts_line
10
+ from codegraph.indexing import Indexer
11
+ from codegraph.indexing.indexer import check_database_health
12
+ from codegraph.target_resolver import TargetType, resolve_target
13
+
14
+
15
+ def test_deterministic_endpoint_ids() -> None:
16
+ """Verify endpoint_id is stable, deterministic, and includes declaration evidence."""
17
+ r1 = RouteDetection(
18
+ framework="express",
19
+ http_method="GET",
20
+ route_path="/products",
21
+ normalized_route="/products",
22
+ handler_name="getProducts",
23
+ handler_canonical_id="routes.products.getProducts",
24
+ file_path="src/routes/products.ts",
25
+ line=12,
26
+ column=4,
27
+ confidence="HIGH",
28
+ resolution_status="RESOLVED",
29
+ evidence="router.get('/products', getProducts)",
30
+ module="src.routes.products",
31
+ )
32
+ r2 = RouteDetection(
33
+ framework="express",
34
+ http_method="GET",
35
+ route_path="/products",
36
+ normalized_route="/products",
37
+ handler_name="getProducts",
38
+ handler_canonical_id="routes.products.getProducts",
39
+ file_path="src/routes/products.ts",
40
+ line=12,
41
+ column=4,
42
+ confidence="HIGH",
43
+ resolution_status="RESOLVED",
44
+ evidence="router.get('/products', getProducts)",
45
+ module="src.routes.products",
46
+ )
47
+ # Must be deterministic and stable
48
+ assert r1.endpoint_id == r2.endpoint_id
49
+ assert r1.endpoint_id == "API_ENDPOINT:express:src/routes/products.ts:12:GET /products#getProducts"
50
+
51
+ # Override takes precedence if provided (e.g. from existing DB records)
52
+ r_override = RouteDetection(
53
+ framework="express",
54
+ http_method="GET",
55
+ route_path="/products",
56
+ normalized_route="/products",
57
+ handler_name="getProducts",
58
+ handler_canonical_id="routes.products.getProducts",
59
+ file_path="src/routes/products.ts",
60
+ line=12,
61
+ endpoint_id_override="CUSTOM_ID_123",
62
+ )
63
+ assert r_override.endpoint_id == "CUSTOM_ID_123"
64
+
65
+
66
+ def test_endpoint_id_collision_prevention_same_file() -> None:
67
+ """Verify distinct route declarations in the same file for same path produce distinct endpoint_ids."""
68
+ lines = [
69
+ "router.get('/products', authCheck);",
70
+ "router.get('/products', listProducts);",
71
+ ]
72
+ routes: list[RouteDetection] = []
73
+ for idx, line in enumerate(lines):
74
+ routes.extend(analyze_js_ts_line(line, idx + 1, "routes/products.js", "routes.products", set()))
75
+
76
+ assert len(routes) == 2
77
+ assert routes[0].route_signature == "GET /products"
78
+ assert routes[1].route_signature == "GET /products"
79
+ # Route signature is identical, but endpoint_id must be distinct!
80
+ assert routes[0].endpoint_id != routes[1].endpoint_id
81
+ assert "authCheck" in routes[0].endpoint_id
82
+ assert "listProducts" in routes[1].endpoint_id
83
+
84
+
85
+ def test_two_files_with_same_method_and_path(tmp_path: Path) -> None:
86
+ """Verify two distinct files with identical method + path can coexist without collisions."""
87
+ routes_dir = tmp_path / "routes"
88
+ routes_dir.mkdir(parents=True)
89
+
90
+ file_a = routes_dir / "a.ts"
91
+ file_a.write_text("router.get('/products', handlerA);\n", encoding="utf-8")
92
+
93
+ file_b = routes_dir / "b.ts"
94
+ file_b.write_text("router.get('/products', handlerB);\n", encoding="utf-8")
95
+
96
+ indexer = Indexer(tmp_path)
97
+ res = indexer.index()
98
+ assert res["indexed"] == 2
99
+
100
+ with indexer.session() as con:
101
+ rows = con.execute(
102
+ "SELECT endpoint_id, file_path, handler_name, line FROM framework_routes ORDER BY file_path"
103
+ ).fetchall()
104
+ assert len(rows) == 2
105
+ assert rows[0]["file_path"] == "routes/a.ts"
106
+ assert rows[0]["handler_name"] == "handlerA"
107
+ assert rows[1]["file_path"] == "routes/b.ts"
108
+ assert rows[1]["handler_name"] == "handlerB"
109
+ # Neither overwrote the other!
110
+ assert rows[0]["endpoint_id"] != rows[1]["endpoint_id"]
111
+
112
+
113
+ def test_same_route_declaration_discovered_twice(tmp_path: Path) -> None:
114
+ """Verify exact duplicate route declaration handled idempotently via ON CONFLICT without crash."""
115
+ indexer = Indexer(tmp_path)
116
+ indexer.index()
117
+
118
+ with indexer.session() as con:
119
+ # File must exist to satisfy foreign key
120
+ con.execute(
121
+ "INSERT INTO files(path, hash, language, indexed_at, status) VALUES ('app.js', 'h1', 'javascript', 0, 'ok')"
122
+ )
123
+ r = RouteDetection(
124
+ framework="express",
125
+ http_method="GET",
126
+ route_path="/items",
127
+ normalized_route="/items",
128
+ handler_name="getItems",
129
+ handler_canonical_id="app.getItems",
130
+ file_path="app.js",
131
+ line=10,
132
+ evidence="router.get('/items', getItems)",
133
+ )
134
+ query = (
135
+ "INSERT INTO framework_routes(endpoint_id, framework, http_method, route_path, "
136
+ "normalized_route, handler_name, handler_canonical_id, file_path, line, evidence, confidence) "
137
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?) "
138
+ "ON CONFLICT(endpoint_id) DO UPDATE SET "
139
+ "framework=excluded.framework, "
140
+ "http_method=excluded.http_method, "
141
+ "route_path=excluded.route_path, "
142
+ "normalized_route=excluded.normalized_route, "
143
+ "handler_name=excluded.handler_name, "
144
+ "handler_canonical_id=excluded.handler_canonical_id, "
145
+ "file_path=excluded.file_path, "
146
+ "line=excluded.line, "
147
+ "evidence=excluded.evidence, "
148
+ "confidence=excluded.confidence"
149
+ )
150
+ # Execute twice with identical endpoint_id in same transaction
151
+ con.execute(query, (r.endpoint_id, r.framework, r.http_method, r.route_path, r.normalized_route, r.handler_name, r.handler_canonical_id, "app.js", r.line, r.evidence, r.confidence))
152
+ con.execute(query, (r.endpoint_id, r.framework, r.http_method, r.route_path, r.normalized_route, r.handler_name, r.handler_canonical_id, "app.js", r.line, "updated evidence", r.confidence))
153
+
154
+ row = con.execute("SELECT count(*), evidence FROM framework_routes WHERE endpoint_id=?", (r.endpoint_id,)).fetchone()
155
+ assert row[0] == 1
156
+ assert row[1] == "updated evidence"
157
+
158
+
159
+ def test_repeated_indexing(tmp_path: Path) -> None:
160
+ """Verify repeated indexing does not crash and produces consistent route count."""
161
+ (tmp_path / "server.js").write_text(
162
+ "app.get('/health', getHealth);\n"
163
+ "app.post('/login', postLogin);\n",
164
+ encoding="utf-8",
165
+ )
166
+ indexer = Indexer(tmp_path)
167
+
168
+ # First index
169
+ res1 = indexer.index()
170
+ assert res1["indexed"] == 1
171
+
172
+ with indexer.session() as con:
173
+ count1 = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
174
+ assert count1 == 2
175
+
176
+ # Second index immediately (files unchanged)
177
+ res2 = indexer.index()
178
+ assert res2["unchanged"] == 1
179
+
180
+ with indexer.session() as con:
181
+ count2 = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
182
+ assert count2 == 2
183
+
184
+ # Third index with force_reparse
185
+ with indexer.session() as con:
186
+ # Force reparse by setting parser version to dummy
187
+ con.execute("UPDATE metadata SET value='old_parser' WHERE key='parser_version'")
188
+
189
+ res3 = indexer.index()
190
+ assert res3["indexed"] == 1
191
+
192
+ with indexer.session() as con:
193
+ count3 = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
194
+ assert count3 == 2
195
+
196
+
197
+ def test_incremental_route_reindex(tmp_path: Path) -> None:
198
+ """Verify modifying a route file replaces old routes cleanly without orphan rows."""
199
+ file_path = tmp_path / "routes.js"
200
+ file_path.write_text("router.get('/products', oldHandler);\n", encoding="utf-8")
201
+
202
+ indexer = Indexer(tmp_path)
203
+ indexer.index()
204
+
205
+ with indexer.session() as con:
206
+ rows = con.execute("SELECT handler_name, line FROM framework_routes WHERE file_path='routes.js'").fetchall()
207
+ assert len(rows) == 1
208
+ assert rows[0]["handler_name"] == "oldHandler"
209
+ assert rows[0]["line"] == 1
210
+
211
+ # Modify file: shift line down and add another route
212
+ file_path.write_text(
213
+ "// added comment header\n"
214
+ "// line 2\n"
215
+ "router.get('/products', newHandler);\n"
216
+ "router.delete('/products', deleteHandler);\n",
217
+ encoding="utf-8",
218
+ )
219
+
220
+ indexer.index()
221
+
222
+ with indexer.session() as con:
223
+ rows = con.execute("SELECT handler_name, http_method, line FROM framework_routes WHERE file_path='routes.js' ORDER BY line").fetchall()
224
+ assert len(rows) == 2
225
+ # Old route removed, new routes added at correct lines
226
+ assert rows[0]["handler_name"] == "newHandler"
227
+ assert rows[0]["line"] == 3
228
+ assert rows[1]["handler_name"] == "deleteHandler"
229
+ assert rows[1]["line"] == 4
230
+
231
+
232
+ def test_route_foreign_key_and_evidence_integrity(tmp_path: Path) -> None:
233
+ """Verify foreign key integrity on framework_routes and evidence persistence."""
234
+ (tmp_path / "app.py").write_text(
235
+ "from fastapi import FastAPI\n"
236
+ "app = FastAPI()\n"
237
+ "@app.get('/users/{user_id}')\n"
238
+ "def get_user(user_id: int):\n"
239
+ " return {'id': user_id}\n",
240
+ encoding="utf-8",
241
+ )
242
+ indexer = Indexer(tmp_path)
243
+ indexer.index()
244
+
245
+ with indexer.session() as con:
246
+ # Check foreign key to files table
247
+ row = con.execute(
248
+ "SELECT r.endpoint_id, r.file_path, f.path, r.evidence "
249
+ "FROM framework_routes r JOIN files f ON r.file_path = f.path"
250
+ ).fetchone()
251
+ assert row is not None
252
+ assert row["file_path"] == "app.py"
253
+ assert "FastAPI decorator" in row["evidence"]
254
+
255
+ # Doctor health check
256
+ health = check_database_health(con, tmp_path)
257
+ assert health["status"] == "OK"
258
+ assert health["issues"] == []
259
+
260
+
261
+ def test_framework_route_queries_after_deduplication(tmp_path: Path) -> None:
262
+ """Verify CLI and target resolver queries against framework_routes work accurately."""
263
+ (tmp_path / "routes.js").write_text(
264
+ "router.get('/items', getItems);\n"
265
+ "router.post('/items', createItem);\n",
266
+ encoding="utf-8",
267
+ )
268
+ indexer = Indexer(tmp_path)
269
+ indexer.index()
270
+
271
+ runner = CliRunner()
272
+ res = runner.invoke(app, ["routes", str(tmp_path), "--json"])
273
+ assert res.exit_code == 0
274
+ assert "/items" in res.stdout
275
+ assert "getItems" in res.stdout
276
+ assert "createItem" in res.stdout
277
+
278
+ with indexer.session() as con:
279
+ target_res = resolve_target("/items", con)
280
+ assert target_res.target_type == TargetType.API_ENDPOINT
281
+ assert target_res.confidence == "HIGH"
282
+
283
+
284
+ def test_check_database_health_detects_route_issues(tmp_path: Path) -> None:
285
+ """Verify check_database_health flags orphan routes and duplicate endpoint IDs."""
286
+ indexer = Indexer(tmp_path)
287
+ indexer.index()
288
+
289
+ with indexer.session() as con:
290
+ # 1. Initially healthy
291
+ assert check_database_health(con, tmp_path)["status"] == "OK"
292
+
293
+ # 2. Insert orphan route referencing non-existent file
294
+ con.execute("PRAGMA foreign_keys = OFF")
295
+ con.execute(
296
+ "INSERT INTO framework_routes(endpoint_id, framework, http_method, route_path, normalized_route, handler_name, handler_canonical_id, file_path, line, evidence) "
297
+ "VALUES ('ep_orphan', 'express', 'GET', '/orphan', '/orphan', 'h', 'h', 'non_existent.js', 1, 'ev')"
298
+ )
299
+ h_orphan = check_database_health(con, tmp_path)
300
+ assert h_orphan["status"] == "ISSUES_FOUND"
301
+ assert any("orphan route" in issue for issue in h_orphan["issues"])
302
+
303
+
304
+ def test_fastapi_duplicate_overlapping_routes(tmp_path: Path) -> None:
305
+ """Verify FastAPI routes with duplicate/overlapping route paths in same file index without collisions."""
306
+ app_file = tmp_path / "app.py"
307
+ app_file.write_text(
308
+ "from fastapi import FastAPI\n"
309
+ "app = FastAPI()\n"
310
+ "@app.get('/items')\n"
311
+ "def get_items_summary():\n"
312
+ " return []\n"
313
+ "@app.get('/items')\n"
314
+ "def get_items_detail():\n"
315
+ " return []\n",
316
+ encoding="utf-8",
317
+ )
318
+ indexer = Indexer(tmp_path)
319
+ res = indexer.index()
320
+ assert res["indexed"] == 1
321
+
322
+ with indexer.session() as con:
323
+ rows = con.execute(
324
+ "SELECT endpoint_id, handler_name, line FROM framework_routes WHERE file_path='app.py' ORDER BY line"
325
+ ).fetchall()
326
+ assert len(rows) == 2
327
+ assert rows[0]["handler_name"] == "get_items_summary"
328
+ assert rows[1]["handler_name"] == "get_items_detail"
329
+ assert rows[0]["endpoint_id"] != rows[1]["endpoint_id"]
330
+ assert "get_items_summary" in rows[0]["endpoint_id"]
331
+ assert "get_items_detail" in rows[1]["endpoint_id"]
332
+
333
+
334
+ def test_fastapi_and_express_both_work_in_same_repo(tmp_path: Path) -> None:
335
+ """Verify both FastAPI (Python) and Express (JS/TS) routes index and coexist without collisions."""
336
+ (tmp_path / "main.py").write_text(
337
+ "from fastapi import FastAPI\n"
338
+ "app = FastAPI()\n"
339
+ "@app.get('/api/health')\n"
340
+ "def py_health():\n"
341
+ " return {'ok': True}\n",
342
+ encoding="utf-8",
343
+ )
344
+ (tmp_path / "server.js").write_text(
345
+ "app.get('/api/health', jsHealth);\n",
346
+ encoding="utf-8",
347
+ )
348
+ indexer = Indexer(tmp_path)
349
+ res = indexer.index()
350
+ assert res["indexed"] == 2
351
+
352
+ with indexer.session() as con:
353
+ rows = con.execute(
354
+ "SELECT endpoint_id, framework, file_path, handler_name FROM framework_routes ORDER BY framework"
355
+ ).fetchall()
356
+ assert len(rows) == 2
357
+ assert rows[0]["framework"] == "express"
358
+ assert rows[0]["handler_name"] == "jsHealth"
359
+ assert rows[1]["framework"] == "fastapi"
360
+ assert rows[1]["handler_name"] == "py_health"
361
+ assert "express" in rows[0]["endpoint_id"]
362
+ assert "fastapi" in rows[1]["endpoint_id"]
363
+ assert rows[0]["endpoint_id"] != rows[1]["endpoint_id"]
@@ -152,7 +152,7 @@ def test_cli_startup_on_windows(tmp_path: Path) -> None:
152
152
  # 1. Version command
153
153
  res_ver = runner.invoke(app, ["--version"])
154
154
  assert res_ver.exit_code == 0
155
- assert "2.1.2" in res_ver.stdout
155
+ assert "2.1.3" in res_ver.stdout
156
156
 
157
157
  # 2. Help command
158
158
  res_help = runner.invoke(app, ["--help"], prog_name="codegraph")