arcgraph 0.1.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 (165) hide show
  1. arcgraph/__init__.py +41 -0
  2. arcgraph/_build_provenance.json +10 -0
  3. arcgraph/adapters/__init__.py +6 -0
  4. arcgraph/adapters/arq_adapter.py +369 -0
  5. arcgraph/adapters/celery_adapter.py +440 -0
  6. arcgraph/adapters/common.py +436 -0
  7. arcgraph/adapters/django_adapter.py +765 -0
  8. arcgraph/adapters/fastapi_adapter.py +415 -0
  9. arcgraph/adapters/logging_adapter.py +335 -0
  10. arcgraph/adapters/mcp_adapter.py +100 -0
  11. arcgraph/adapters/pydantic_adapter.py +290 -0
  12. arcgraph/adapters/pytest_adapter.py +221 -0
  13. arcgraph/adapters/pytest_collection.py +96 -0
  14. arcgraph/adapters/pytest_fixtures.py +371 -0
  15. arcgraph/adapters/pytest_types.py +251 -0
  16. arcgraph/adapters/registry.py +34 -0
  17. arcgraph/adapters/service_container_adapter.py +548 -0
  18. arcgraph/adapters/sqlalchemy_adapter.py +446 -0
  19. arcgraph/adapters/typer_click_adapter.py +292 -0
  20. arcgraph/analyzers/__init__.py +1 -0
  21. arcgraph/analyzers/bindings.py +782 -0
  22. arcgraph/analyzers/calls/__init__.py +6 -0
  23. arcgraph/analyzers/calls/analyzer.py +2921 -0
  24. arcgraph/analyzers/calls/constants.py +702 -0
  25. arcgraph/analyzers/calls/context.py +298 -0
  26. arcgraph/analyzers/calls/lexical.py +237 -0
  27. arcgraph/analyzers/coverage.py +826 -0
  28. arcgraph/analyzers/exports.py +46 -0
  29. arcgraph/analyzers/har_trace.py +323 -0
  30. arcgraph/analyzers/imports.py +190 -0
  31. arcgraph/analyzers/otel_trace.py +352 -0
  32. arcgraph/analyzers/precise_references.py +1199 -0
  33. arcgraph/analyzers/python_scopes.py +56 -0
  34. arcgraph/analyzers/runtime_trace.py +516 -0
  35. arcgraph/analyzers/similarity.py +430 -0
  36. arcgraph/analyzers/symbols.py +363 -0
  37. arcgraph/analyzers/type_unions.py +89 -0
  38. arcgraph/analyzers/types.py +2293 -0
  39. arcgraph/assets/__init__.py +1 -0
  40. arcgraph/assets/workbench/__init__.py +1 -0
  41. arcgraph/assets/workbench/index.html +2398 -0
  42. arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
  43. arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
  44. arcgraph/change/__init__.py +10 -0
  45. arcgraph/change/baseline.py +145 -0
  46. arcgraph/change/changed_regions.py +728 -0
  47. arcgraph/change/contracts.py +927 -0
  48. arcgraph/change/errors.py +88 -0
  49. arcgraph/change/evidence.py +289 -0
  50. arcgraph/change/graph_delta.py +1422 -0
  51. arcgraph/change/identities.py +1277 -0
  52. arcgraph/change/paths.py +234 -0
  53. arcgraph/change/pins.py +426 -0
  54. arcgraph/change/planner.py +504 -0
  55. arcgraph/change/protected_surfaces.py +328 -0
  56. arcgraph/change/provider.py +46 -0
  57. arcgraph/change/service.py +1030 -0
  58. arcgraph/change/source_provider.py +311 -0
  59. arcgraph/change/store.py +1138 -0
  60. arcgraph/change/trusted_runners.py +154 -0
  61. arcgraph/change/verdicts.py +141 -0
  62. arcgraph/change/verification_plan.py +113 -0
  63. arcgraph/change/verifier.py +579 -0
  64. arcgraph/core/__init__.py +1 -0
  65. arcgraph/core/assurance.py +343 -0
  66. arcgraph/core/binding_metrics.py +75 -0
  67. arcgraph/core/cleanup.py +701 -0
  68. arcgraph/core/confidence_profiles.py +108 -0
  69. arcgraph/core/evidence_manifest.py +862 -0
  70. arcgraph/core/expression_kinds.py +11 -0
  71. arcgraph/core/force_graph_export.py +1125 -0
  72. arcgraph/core/freshness.py +94 -0
  73. arcgraph/core/graph_store.py +1099 -0
  74. arcgraph/core/ids.py +206 -0
  75. arcgraph/core/language_tiers.py +150 -0
  76. arcgraph/core/merge.py +649 -0
  77. arcgraph/core/metadata.py +30 -0
  78. arcgraph/core/operation_lock.py +218 -0
  79. arcgraph/core/passes.py +142 -0
  80. arcgraph/core/payload_policy.py +708 -0
  81. arcgraph/core/precise_references.py +124 -0
  82. arcgraph/core/query_engine.py +3078 -0
  83. arcgraph/core/recovery.py +18 -0
  84. arcgraph/core/scanner.py +1454 -0
  85. arcgraph/core/schemas.py +700 -0
  86. arcgraph/core/semantic.py +263 -0
  87. arcgraph/core/semantic_metrics.py +619 -0
  88. arcgraph/core/sharing_retry.py +84 -0
  89. arcgraph/core/structural.py +260 -0
  90. arcgraph/core/target_resolver.py +486 -0
  91. arcgraph/core/type_metrics.py +67 -0
  92. arcgraph/core/unresolved_classification.py +471 -0
  93. arcgraph/core/utils.py +100 -0
  94. arcgraph/core/visual_contract.py +240 -0
  95. arcgraph/core/visual_slice_engine.py +933 -0
  96. arcgraph/interfaces/__init__.py +1 -0
  97. arcgraph/interfaces/agent_capabilities.py +493 -0
  98. arcgraph/interfaces/agent_help.py +313 -0
  99. arcgraph/interfaces/benchmark.py +548 -0
  100. arcgraph/interfaces/ci.py +1992 -0
  101. arcgraph/interfaces/cli.py +2799 -0
  102. arcgraph/interfaces/cli_change.py +690 -0
  103. arcgraph/interfaces/cli_feedback.py +176 -0
  104. arcgraph/interfaces/cli_ops.py +69 -0
  105. arcgraph/interfaces/cli_support.py +113 -0
  106. arcgraph/interfaces/cli_trial.py +42 -0
  107. arcgraph/interfaces/cli_visual.py +601 -0
  108. arcgraph/interfaces/cli_workspace.py +300 -0
  109. arcgraph/interfaces/client_setup.py +481 -0
  110. arcgraph/interfaces/docs.py +1255 -0
  111. arcgraph/interfaces/human_output.py +409 -0
  112. arcgraph/interfaces/local_state.py +577 -0
  113. arcgraph/interfaces/mcp_server.py +457 -0
  114. arcgraph/interfaces/mcp_tools.py +1209 -0
  115. arcgraph/interfaces/metrics.py +571 -0
  116. arcgraph/interfaces/ops.py +330 -0
  117. arcgraph/interfaces/precision.py +1670 -0
  118. arcgraph/interfaces/project_probe.py +75 -0
  119. arcgraph/interfaces/reports.py +1121 -0
  120. arcgraph/interfaces/stdio_encoding.py +29 -0
  121. arcgraph/interfaces/trace.py +592 -0
  122. arcgraph/interfaces/trial_feedback.py +519 -0
  123. arcgraph/interfaces/trial_setup.py +535 -0
  124. arcgraph/interfaces/visual_server.py +354 -0
  125. arcgraph/interfaces/visual_slice.py +168 -0
  126. arcgraph/interfaces/visual_smoke.py +666 -0
  127. arcgraph/interfaces/workbench.py +576 -0
  128. arcgraph/interfaces/workspace.py +1513 -0
  129. arcgraph/pipeline/__init__.py +1 -0
  130. arcgraph/pipeline/contracts.py +122 -0
  131. arcgraph/pipeline/external_frontend.py +451 -0
  132. arcgraph/pipeline/external_l3_frontends.py +368 -0
  133. arcgraph/pipeline/external_protocol.py +358 -0
  134. arcgraph/pipeline/frontends.py +437 -0
  135. arcgraph/pipeline/indexer.py +661 -0
  136. arcgraph/pipeline/openapi_frontend.py +708 -0
  137. arcgraph/pipeline/python_frontend.py +298 -0
  138. arcgraph/pipeline/reindexer.py +1333 -0
  139. arcgraph/pipeline/scip_frontend.py +726 -0
  140. arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
  141. arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
  142. arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
  143. arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
  144. arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
  145. arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
  146. arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
  147. arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
  148. arcgraph/pipeline/typescript_extractor.mjs +177 -0
  149. arcgraph/pipeline/typescript_frameworks.py +947 -0
  150. arcgraph/pipeline/typescript_frontend.py +656 -0
  151. arcgraph/pipeline/typescript_references.mjs +167 -0
  152. arcgraph/providers/__init__.py +27 -0
  153. arcgraph/providers/context_provider.py +1258 -0
  154. arcgraph/providers/flow_payload.py +117 -0
  155. arcgraph/providers/memory_connector.py +216 -0
  156. arcgraph/providers/orchestrator.py +475 -0
  157. arcgraph/providers/relationship_payload.py +102 -0
  158. arcgraph/providers/risk_provider.py +843 -0
  159. arcgraph/providers/why_provider.py +349 -0
  160. arcgraph/version_info.py +246 -0
  161. arcgraph-0.1.0.dist-info/METADATA +676 -0
  162. arcgraph-0.1.0.dist-info/RECORD +165 -0
  163. arcgraph-0.1.0.dist-info/WHEEL +4 -0
  164. arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
  165. arcgraph-0.1.0.dist-info/licenses/LICENSE +203 -0
@@ -0,0 +1,437 @@
1
+ """Language frontend contracts and the current Python compatibility frontend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from pathlib import Path
7
+
8
+ from arcgraph.core.ids import diagnostic_id
9
+ from arcgraph.core.language_tiers import language_tier_capabilities
10
+ from arcgraph.core.passes import (
11
+ AnalysisPassContext,
12
+ AnalysisPassResult,
13
+ PassDispatcher,
14
+ )
15
+ from arcgraph.pipeline.contracts import FrontendGraphFragment
16
+ from arcgraph.pipeline.python_frontend import PythonGraphAnalyzer
17
+ from arcgraph.core.schemas import (
18
+ BuildWarning,
19
+ Edge,
20
+ FileRecord,
21
+ FrontendAnalyzeRequest,
22
+ FrontendAnalyzeResult,
23
+ FrontendCapabilities,
24
+ IndexMetadata,
25
+ Node,
26
+ SemanticDiagnostic,
27
+ )
28
+ from arcgraph.core.semantic import (
29
+ COMPAT_FRONTEND_NAME,
30
+ COMPAT_FRONTEND_VERSION,
31
+ diagnostics_from_warnings,
32
+ semantic_facts_from_graph,
33
+ )
34
+ from arcgraph.pipeline.typescript_frontend import (
35
+ TYPESCRIPT_FILE_EXTENSIONS,
36
+ TYPESCRIPT_FRONTEND_NAME,
37
+ TYPESCRIPT_FRONTEND_VERSION,
38
+ TypeScriptGraphAnalyzer,
39
+ )
40
+
41
+
42
+ class LanguageFrontend(ABC):
43
+ name: str
44
+ language_ids: tuple[str, ...]
45
+ version: str
46
+
47
+ @property
48
+ def file_extensions(self) -> tuple[str, ...]:
49
+ """File extensions this frontend can analyze (e.g. ('.py',)).
50
+
51
+ Used by FileScanner to dynamically determine which files to scan.
52
+ Default returns all extensions based on language_ids.
53
+ """
54
+ _LANG_EXTENSIONS: dict[str, tuple[str, ...]] = {
55
+ "python": (".py",),
56
+ "typescript": (".ts", ".tsx"),
57
+ "javascript": (".js", ".jsx"),
58
+ "java": (".java",),
59
+ "go": (".go",),
60
+ }
61
+ extensions: list[str] = []
62
+ for lang in self.language_ids:
63
+ extensions.extend(_LANG_EXTENSIONS.get(lang, ()))
64
+ return tuple(extensions) or (".py",) # Default to Python
65
+
66
+ def accepts(self, file: FileRecord) -> bool:
67
+ """Return whether this frontend should analyze an indexed file."""
68
+ return Path(file.path).suffix in self.file_extensions
69
+
70
+ @abstractmethod
71
+ def capabilities(self) -> FrontendCapabilities:
72
+ """Return the frontend's declared capabilities."""
73
+
74
+ @abstractmethod
75
+ def detect(self, request: FrontendAnalyzeRequest) -> bool:
76
+ """Return whether this frontend should analyze the request."""
77
+
78
+ @abstractmethod
79
+ def analyze(self, request: FrontendAnalyzeRequest) -> FrontendAnalyzeResult:
80
+ """Analyze the request and return semantic facts."""
81
+
82
+ @abstractmethod
83
+ def analyze_to_graph(
84
+ self,
85
+ files: list[FileRecord],
86
+ warnings: list[BuildWarning] | None = None,
87
+ module_names: set[str] | None = None,
88
+ call_context_nodes: list[Node] | None = None,
89
+ incremental_similarity_buckets: set[str] | None = None,
90
+ ) -> FrontendGraphFragment:
91
+ """Analyze files and return a raw graph fragment."""
92
+
93
+ def analyze_incremental(
94
+ self, request: FrontendAnalyzeRequest
95
+ ) -> FrontendAnalyzeResult:
96
+ return self.analyze(request)
97
+
98
+
99
+ class PythonSemanticFrontend(LanguageFrontend):
100
+ """Compatibility wrapper for the existing Python V1 graph analyzers."""
101
+
102
+ name = COMPAT_FRONTEND_NAME
103
+ language_ids = ("python",)
104
+ version = COMPAT_FRONTEND_VERSION
105
+
106
+ def __init__(
107
+ self,
108
+ *,
109
+ enable_v2_call_resolution: bool = True,
110
+ adapter_registry: object | None = None,
111
+ ) -> None:
112
+ self._enable_v2 = enable_v2_call_resolution
113
+ self._adapter_registry = adapter_registry
114
+
115
+ def capabilities(self) -> FrontendCapabilities:
116
+ return FrontendCapabilities(
117
+ name=self.name,
118
+ version=self.version,
119
+ language="python",
120
+ capabilities={
121
+ "mode": "v1-compat",
122
+ "fact_kinds": "node,edge,diagnostic",
123
+ "confidence": "canonical",
124
+ "incremental": "file",
125
+ **language_tier_capabilities(
126
+ tier="L3",
127
+ source="native-python",
128
+ scope=(
129
+ "Python semantic graph facts with receiver-aware static "
130
+ "analysis; runtime evidence remains separate."
131
+ ),
132
+ languages=("python",),
133
+ ),
134
+ },
135
+ file_extensions=list(self.file_extensions),
136
+ )
137
+
138
+ def detect(self, request: FrontendAnalyzeRequest) -> bool:
139
+ repo_root = Path(request.repo_root)
140
+ candidate_paths = request.changed_files or request.source_roots or ["."]
141
+ for raw_path in candidate_paths:
142
+ path = repo_root / raw_path
143
+ if path.is_file() and path.suffix == ".py":
144
+ return True
145
+ if path.is_dir():
146
+ try:
147
+ if next(path.rglob("*.py"), None) is not None:
148
+ return True
149
+ except OSError:
150
+ continue
151
+ return False
152
+
153
+ def analyze(self, request: FrontendAnalyzeRequest) -> FrontendAnalyzeResult:
154
+ return FrontendAnalyzeResult(
155
+ frontend=self.capabilities(),
156
+ diagnostics=[
157
+ SemanticDiagnostic(
158
+ diagnostic_id=diagnostic_id(
159
+ request.repo_id,
160
+ "frontend_graph_context_note",
161
+ self.name,
162
+ request.index_version or "working-tree",
163
+ ),
164
+ repo_id=request.repo_id,
165
+ index_version=request.index_version or "working-tree",
166
+ diagnostic_kind="frontend_graph_context_note",
167
+ message=(
168
+ "PythonSemanticFrontend.analyze() generates semantic "
169
+ "facts via analyze_graph(); use analyze_to_graph() for "
170
+ "raw graph fragments."
171
+ ),
172
+ severity="info",
173
+ frontend_name=self.name,
174
+ )
175
+ ],
176
+ status="available",
177
+ )
178
+
179
+ def analyze_to_graph(
180
+ self,
181
+ files: list[FileRecord],
182
+ warnings: list[BuildWarning] | None = None,
183
+ module_names: set[str] | None = None,
184
+ call_context_nodes: list[Node] | None = None,
185
+ incremental_similarity_buckets: set[str] | None = None,
186
+ ) -> FrontendGraphFragment:
187
+ """Run the Python graph analysis pipeline and return a raw fragment.
188
+
189
+ This is the primary entry point for the indexer to get
190
+ Python-specific graph data without going through the semantic
191
+ fact / merge pipeline.
192
+ """
193
+ analyzer = PythonGraphAnalyzer(
194
+ enable_v2_call_resolution=self._enable_v2,
195
+ adapter_registry=self._adapter_registry,
196
+ )
197
+ return analyzer.analyze(
198
+ files,
199
+ warnings=warnings,
200
+ module_names=module_names,
201
+ call_context_nodes=call_context_nodes,
202
+ incremental_similarity_buckets=incremental_similarity_buckets,
203
+ )
204
+
205
+ def analyze_graph(
206
+ self,
207
+ metadata: IndexMetadata,
208
+ nodes: list[Node],
209
+ edges: list[Edge],
210
+ warnings: list[BuildWarning],
211
+ *,
212
+ repo_id: str = "default",
213
+ ) -> FrontendAnalyzeResult:
214
+ facts = semantic_facts_from_graph(metadata, nodes, edges, repo_id=repo_id)
215
+ diagnostics = diagnostics_from_warnings(metadata, warnings, repo_id=repo_id)
216
+ return FrontendAnalyzeResult(
217
+ frontend=self.capabilities(),
218
+ facts=facts,
219
+ diagnostics=diagnostics,
220
+ metrics={
221
+ "node_facts": sum(1 for fact in facts if fact.fact_kind == "node"),
222
+ "edge_facts": sum(1 for fact in facts if fact.fact_kind == "edge"),
223
+ "diagnostics": len(diagnostics),
224
+ },
225
+ )
226
+
227
+
228
+ class TypeScriptSemanticFrontend(LanguageFrontend):
229
+ """Static TypeScript/React frontend using the TypeScript compiler API."""
230
+
231
+ name = TYPESCRIPT_FRONTEND_NAME
232
+ language_ids = ("typescript", "javascript")
233
+ version = TYPESCRIPT_FRONTEND_VERSION
234
+
235
+ def __init__(
236
+ self,
237
+ repo_root: Path | None = None,
238
+ *,
239
+ timeout_seconds: float = 60,
240
+ ) -> None:
241
+ self._repo_root = repo_root.resolve() if repo_root is not None else None
242
+ self._timeout_seconds = timeout_seconds
243
+
244
+ def capabilities(self) -> FrontendCapabilities:
245
+ return FrontendCapabilities(
246
+ name=self.name,
247
+ version=self.version,
248
+ language="typescript",
249
+ capabilities={
250
+ "mode": "typescript-compiler-ast",
251
+ "fact_kinds": "node,edge,diagnostic",
252
+ "confidence": "confirmed-inferred-and-heuristic",
253
+ "incremental": "file",
254
+ **language_tier_capabilities(
255
+ tier="L3",
256
+ source="typescript-compiler-api",
257
+ scope=(
258
+ "TypeScript and JavaScript semantic static facts from "
259
+ "the compiler API; typechecker call targets are inferred."
260
+ ),
261
+ languages=("typescript", "javascript"),
262
+ ),
263
+ },
264
+ file_extensions=list(self.file_extensions),
265
+ )
266
+
267
+ @property
268
+ def file_extensions(self) -> tuple[str, ...]:
269
+ return TYPESCRIPT_FILE_EXTENSIONS
270
+
271
+ def detect(self, request: FrontendAnalyzeRequest) -> bool:
272
+ repo_root = Path(request.repo_root)
273
+ candidate_paths = request.changed_files or request.source_roots or ["."]
274
+ for raw_path in candidate_paths:
275
+ path = repo_root / raw_path
276
+ if path.is_file() and path.suffix in self.file_extensions:
277
+ return True
278
+ if path.is_dir():
279
+ try:
280
+ if any(
281
+ next(path.rglob(f"*{extension}"), None) is not None
282
+ for extension in self.file_extensions
283
+ ):
284
+ return True
285
+ except OSError:
286
+ continue
287
+ return False
288
+
289
+ def analyze(self, request: FrontendAnalyzeRequest) -> FrontendAnalyzeResult:
290
+ return FrontendAnalyzeResult(
291
+ frontend=self.capabilities(),
292
+ diagnostics=[
293
+ SemanticDiagnostic(
294
+ diagnostic_id=diagnostic_id(
295
+ request.repo_id,
296
+ "frontend_graph_context_note",
297
+ self.name,
298
+ request.index_version or "working-tree",
299
+ ),
300
+ repo_id=request.repo_id,
301
+ index_version=request.index_version or "working-tree",
302
+ diagnostic_kind="frontend_graph_context_note",
303
+ message=(
304
+ "TypeScriptSemanticFrontend.analyze() generates graph "
305
+ "fragments via analyze_to_graph()."
306
+ ),
307
+ severity="info",
308
+ frontend_name=self.name,
309
+ )
310
+ ],
311
+ status="available",
312
+ )
313
+
314
+ def analyze_to_graph(
315
+ self,
316
+ files: list[FileRecord],
317
+ warnings: list[BuildWarning] | None = None,
318
+ module_names: set[str] | None = None,
319
+ call_context_nodes: list[Node] | None = None,
320
+ incremental_similarity_buckets: set[str] | None = None,
321
+ ) -> FrontendGraphFragment:
322
+ repo_root = self._repo_root or self._infer_repo_root(files)
323
+ analyzer = TypeScriptGraphAnalyzer(
324
+ repo_root,
325
+ timeout_seconds=self._timeout_seconds,
326
+ )
327
+ return analyzer.analyze(
328
+ files,
329
+ warnings=warnings,
330
+ module_names=module_names,
331
+ call_context_nodes=call_context_nodes,
332
+ incremental_similarity_buckets=incremental_similarity_buckets,
333
+ )
334
+
335
+ @staticmethod
336
+ def _infer_repo_root(files: list[FileRecord]) -> Path:
337
+ if not files:
338
+ return Path(".").resolve()
339
+ first_abs = Path(files[0].abs_path).resolve()
340
+ for parent in [first_abs.parent, *first_abs.parents]:
341
+ if (parent / ".git").exists() or (
342
+ parent / "frontend" / "package.json"
343
+ ).exists():
344
+ return parent
345
+ return first_abs.parent
346
+
347
+
348
+ class PythonSemanticGraphPass:
349
+ name = "p1-python-v1-compat"
350
+
351
+ def __init__(self, frontend: PythonSemanticFrontend | None = None) -> None:
352
+ self.frontend = frontend or PythonSemanticFrontend()
353
+
354
+ def run(self, context: AnalysisPassContext) -> AnalysisPassResult:
355
+ return AnalysisPassResult(
356
+ name=self.name,
357
+ result=self.frontend.analyze_graph(
358
+ context.metadata,
359
+ context.nodes,
360
+ context.edges,
361
+ context.warnings,
362
+ repo_id=context.repo_id,
363
+ ),
364
+ )
365
+
366
+
367
+ def python_compat_pass_dispatcher() -> PassDispatcher:
368
+ return PassDispatcher([PythonSemanticGraphPass()])
369
+
370
+
371
+ class FrontendRegistry:
372
+ """Minimal registry for language frontend discovery and dispatch."""
373
+
374
+ def __init__(self, frontends: list[LanguageFrontend] | None = None) -> None:
375
+ self._frontends: list[LanguageFrontend] = []
376
+ for frontend in frontends or []:
377
+ self.register(frontend)
378
+
379
+ def register(self, frontend: LanguageFrontend) -> None:
380
+ if any(existing.name == frontend.name for existing in self._frontends):
381
+ raise ValueError(f"Frontend already registered: {frontend.name}")
382
+ self._frontends.append(frontend)
383
+
384
+ def detect_frontends(
385
+ self, request: FrontendAnalyzeRequest
386
+ ) -> list[LanguageFrontend]:
387
+ return [frontend for frontend in self._frontends if frontend.detect(request)]
388
+
389
+ def build_plan(self, request: FrontendAnalyzeRequest) -> list[LanguageFrontend]:
390
+ """Return detected frontends in registration order for build context flow."""
391
+ return self.detect_frontends(request)
392
+
393
+ def analyze(self, request: FrontendAnalyzeRequest) -> list[FrontendAnalyzeResult]:
394
+ return [
395
+ frontend.analyze(request) for frontend in self.detect_frontends(request)
396
+ ]
397
+
398
+ @property
399
+ def frontends(self) -> tuple[LanguageFrontend, ...]:
400
+ return tuple(self._frontends)
401
+
402
+ def inventory(self) -> tuple[dict[str, object], ...]:
403
+ """Return a stable, JSON-serializable inventory of registered frontends."""
404
+ entries: list[dict[str, object]] = []
405
+ for frontend in self._frontends:
406
+ capabilities = frontend.capabilities()
407
+ entries.append(
408
+ {
409
+ "name": frontend.name,
410
+ "version": frontend.version,
411
+ "language_ids": list(frontend.language_ids),
412
+ "file_extensions": list(frontend.file_extensions),
413
+ "capabilities": capabilities.model_dump(mode="json"),
414
+ }
415
+ )
416
+ return tuple(entries)
417
+
418
+
419
+ def default_frontend_registry(
420
+ *,
421
+ repo_root: Path | None = None,
422
+ typescript_timeout_seconds: float = 60,
423
+ enable_v2_call_resolution: bool = True,
424
+ adapter_registry: object | None = None,
425
+ ) -> FrontendRegistry:
426
+ return FrontendRegistry(
427
+ [
428
+ PythonSemanticFrontend(
429
+ enable_v2_call_resolution=enable_v2_call_resolution,
430
+ adapter_registry=adapter_registry,
431
+ ),
432
+ TypeScriptSemanticFrontend(
433
+ repo_root=repo_root,
434
+ timeout_seconds=typescript_timeout_seconds,
435
+ ),
436
+ ]
437
+ )