context-archive 1.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.
- context_archive/__init__.py +49 -0
- context_archive/adapters/__init__.py +20 -0
- context_archive/adapters/base.py +40 -0
- context_archive/adapters/fastapi.py +307 -0
- context_archive/adapters/pipeline.py +54 -0
- context_archive/adapters/python_ast.py +845 -0
- context_archive/adapters/sqlalchemy.py +188 -0
- context_archive/adapters/typescript.py +394 -0
- context_archive/cli.py +915 -0
- context_archive/config.py +212 -0
- context_archive/context/__init__.py +69 -0
- context_archive/context/assembler.py +151 -0
- context_archive/context/compiler.py +438 -0
- context_archive/context/differ.py +331 -0
- context_archive/context/reconciler.py +207 -0
- context_archive/context/tokenizer.py +89 -0
- context_archive/context/verifier.py +177 -0
- context_archive/exceptions.py +56 -0
- context_archive/git/__init__.py +17 -0
- context_archive/git/state.py +152 -0
- context_archive/integrations/__init__.py +15 -0
- context_archive/integrations/mcp_server.py +365 -0
- context_archive/lock.py +165 -0
- context_archive/model/__init__.py +51 -0
- context_archive/model/graph.py +239 -0
- context_archive/model/ids.py +38 -0
- context_archive/model/manifest.py +127 -0
- context_archive/model/provenance.py +39 -0
- context_archive/notes/__init__.py +25 -0
- context_archive/notes/boundaries.py +67 -0
- context_archive/notes/manager.py +285 -0
- context_archive/py.typed +0 -0
- context_archive/renderers/__init__.py +39 -0
- context_archive/renderers/json_exporter.py +39 -0
- context_archive/renderers/markdown.py +335 -0
- context_archive/renderers/mermaid.py +78 -0
- context_archive/scanner/__init__.py +24 -0
- context_archive/scanner/discovery.py +89 -0
- context_archive/scanner/report.py +74 -0
- context_archive/scanner/walker.py +462 -0
- context_archive/sdk.py +121 -0
- context_archive/state/__init__.py +60 -0
- context_archive/state/cache.py +89 -0
- context_archive/state/compactor.py +136 -0
- context_archive/state/detector.py +359 -0
- context_archive/state/invalidator.py +183 -0
- context_archive/state/journal.py +197 -0
- context_archive/state/snapshot.py +108 -0
- context_archive-1.0.0.dist-info/METADATA +75 -0
- context_archive-1.0.0.dist-info/RECORD +53 -0
- context_archive-1.0.0.dist-info/WHEEL +4 -0
- context_archive-1.0.0.dist-info/entry_points.txt +3 -0
- context_archive-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Portable AI Project Context Compiler (context-archive / ctxa)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
|
|
7
|
+
from context_archive.config import CtxaConfig, load_config
|
|
8
|
+
from context_archive.context.compiler import TaskPacket
|
|
9
|
+
from context_archive.context.verifier import VerificationReport
|
|
10
|
+
from context_archive.exceptions import (
|
|
11
|
+
ConfigError,
|
|
12
|
+
CtxaError,
|
|
13
|
+
CtxaWarningError,
|
|
14
|
+
ExitCode,
|
|
15
|
+
IntegrityError,
|
|
16
|
+
LockError,
|
|
17
|
+
StaleContextError,
|
|
18
|
+
)
|
|
19
|
+
from context_archive.lock import (
|
|
20
|
+
ContextLock,
|
|
21
|
+
atomic_write_bytes,
|
|
22
|
+
atomic_write_text,
|
|
23
|
+
)
|
|
24
|
+
from context_archive.sdk import (
|
|
25
|
+
ContextArchiveEngine,
|
|
26
|
+
HandoffResult,
|
|
27
|
+
UpdateResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ConfigError",
|
|
32
|
+
"ContextArchiveEngine",
|
|
33
|
+
"ContextLock",
|
|
34
|
+
"CtxaConfig",
|
|
35
|
+
"CtxaError",
|
|
36
|
+
"CtxaWarningError",
|
|
37
|
+
"ExitCode",
|
|
38
|
+
"HandoffResult",
|
|
39
|
+
"IntegrityError",
|
|
40
|
+
"LockError",
|
|
41
|
+
"StaleContextError",
|
|
42
|
+
"TaskPacket",
|
|
43
|
+
"UpdateResult",
|
|
44
|
+
"VerificationReport",
|
|
45
|
+
"__version__",
|
|
46
|
+
"atomic_write_bytes",
|
|
47
|
+
"atomic_write_text",
|
|
48
|
+
"load_config",
|
|
49
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""adapters subsystem for context-archive."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from context_archive.adapters.base import AdapterExtractionResult, LanguageAdapter
|
|
6
|
+
from context_archive.adapters.fastapi import FastApiContractExtractor
|
|
7
|
+
from context_archive.adapters.pipeline import extract_all_adapters
|
|
8
|
+
from context_archive.adapters.python_ast import PythonAstAdapter
|
|
9
|
+
from context_archive.adapters.sqlalchemy import SqlAlchemyEntityExtractor
|
|
10
|
+
from context_archive.adapters.typescript import TypeScriptAdapter
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AdapterExtractionResult",
|
|
14
|
+
"FastApiContractExtractor",
|
|
15
|
+
"LanguageAdapter",
|
|
16
|
+
"PythonAstAdapter",
|
|
17
|
+
"SqlAlchemyEntityExtractor",
|
|
18
|
+
"TypeScriptAdapter",
|
|
19
|
+
"extract_all_adapters",
|
|
20
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Protocol definitions for language and framework static analysis adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from context_archive.model.graph import Diagnostic, Edge, Node
|
|
12
|
+
from context_archive.scanner.report import ScannedFile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class AdapterExtractionResult:
|
|
17
|
+
"""Container for Nodes, Edges, and Diagnostics produced by an adapter."""
|
|
18
|
+
|
|
19
|
+
nodes: tuple[Node, ...] = field(default=())
|
|
20
|
+
edges: tuple[Edge, ...] = field(default=())
|
|
21
|
+
diagnostics: tuple[Diagnostic, ...] = field(default=())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class LanguageAdapter(Protocol):
|
|
26
|
+
"""Protocol implemented by static source language analyzers."""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def adapter_id(self) -> str:
|
|
30
|
+
"""Return versioned extractor identifier (e.g., `python_ast:1.0`)."""
|
|
31
|
+
|
|
32
|
+
def can_handle(self, scanned_file: ScannedFile) -> bool:
|
|
33
|
+
"""Return True if this adapter can analyze the given ScannedFile."""
|
|
34
|
+
|
|
35
|
+
def extract_repository(
|
|
36
|
+
self,
|
|
37
|
+
repo_root: Path,
|
|
38
|
+
files: tuple[ScannedFile, ...] | list[ScannedFile],
|
|
39
|
+
) -> AdapterExtractionResult:
|
|
40
|
+
"""Statically analyze all matching files in the repository."""
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""FastAPI route, Depends, and Pydantic BaseModel contract extractor (SRS FR-06)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from context_archive.adapters.base import AdapterExtractionResult
|
|
9
|
+
from context_archive.adapters.python_ast import (
|
|
10
|
+
compute_python_module_name,
|
|
11
|
+
sanitize_docstring_summary,
|
|
12
|
+
)
|
|
13
|
+
from context_archive.model.graph import Diagnostic, Edge, Node
|
|
14
|
+
from context_archive.model.ids import make_edge_id, make_symbol_id, normalize_posix_path
|
|
15
|
+
from context_archive.model.provenance import Provenance
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from context_archive.scanner.report import ScannedFile
|
|
21
|
+
|
|
22
|
+
EXTRACTOR_ID = "fastapi_pydantic:1.0"
|
|
23
|
+
HTTP_METHODS = frozenset({"get", "post", "put", "delete", "patch"})
|
|
24
|
+
PYDANTIC_BASE_NAMES = frozenset({"BaseModel", "SQLModel"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _unparse(node: ast.AST | None) -> str:
|
|
28
|
+
if node is None:
|
|
29
|
+
return ""
|
|
30
|
+
try:
|
|
31
|
+
return ast.unparse(node)
|
|
32
|
+
except Exception: # pragma: no cover
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _extract_route_decorator(
|
|
37
|
+
dec: ast.expr,
|
|
38
|
+
) -> tuple[str, str, str | None, int | None] | None:
|
|
39
|
+
"""Inspect a decorator for `@app.<method>("/path", response_model=..., status_code=...)`.
|
|
40
|
+
|
|
41
|
+
Returns `(HTTP_METHOD, route_path, response_model_name, status_code)` or `None`.
|
|
42
|
+
"""
|
|
43
|
+
if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
method_lower = dec.func.attr.lower()
|
|
47
|
+
if method_lower not in HTTP_METHODS:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
route_path = "/"
|
|
51
|
+
if dec.args and isinstance(dec.args[0], ast.Constant) and isinstance(dec.args[0].value, str):
|
|
52
|
+
route_path = dec.args[0].value
|
|
53
|
+
|
|
54
|
+
response_model: str | None = None
|
|
55
|
+
status_code: int | None = None
|
|
56
|
+
for kw in dec.keywords:
|
|
57
|
+
if kw.arg == "response_model":
|
|
58
|
+
response_model = _unparse(kw.value).split(".")[-1] or None
|
|
59
|
+
elif (
|
|
60
|
+
kw.arg == "status_code"
|
|
61
|
+
and isinstance(kw.value, ast.Constant)
|
|
62
|
+
and isinstance(kw.value.value, int)
|
|
63
|
+
):
|
|
64
|
+
status_code = kw.value.value
|
|
65
|
+
|
|
66
|
+
return method_lower.upper(), route_path, response_model, status_code
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _extract_depends_target(default_expr: ast.expr) -> str | None:
|
|
70
|
+
"""Return dependency callable name if `default_expr` is `Depends(callable_name)`."""
|
|
71
|
+
if not isinstance(default_expr, ast.Call):
|
|
72
|
+
return None
|
|
73
|
+
func_name = _unparse(default_expr.func).split(".")[-1]
|
|
74
|
+
if func_name != "Depends" or not default_expr.args:
|
|
75
|
+
return None
|
|
76
|
+
target = _unparse(default_expr.args[0]).split(".")[-1]
|
|
77
|
+
return target or None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FastApiContractExtractor:
|
|
81
|
+
"""Static AST extractor for FastAPI endpoints and Pydantic schemas."""
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def adapter_id(self) -> str:
|
|
85
|
+
return EXTRACTOR_ID
|
|
86
|
+
|
|
87
|
+
def can_handle(self, scanned_file: ScannedFile) -> bool:
|
|
88
|
+
return scanned_file.language == "python" and scanned_file.category == "source_code"
|
|
89
|
+
|
|
90
|
+
def extract_repository(
|
|
91
|
+
self,
|
|
92
|
+
repo_root: Path,
|
|
93
|
+
files: tuple[ScannedFile, ...] | list[ScannedFile],
|
|
94
|
+
*,
|
|
95
|
+
existing_nodes: tuple[Node, ...] | list[Node] = (),
|
|
96
|
+
) -> AdapterExtractionResult:
|
|
97
|
+
"""Extract Schema and Endpoint nodes and link EXPOSES, VALIDATES_WITH, and DEPENDS_ON."""
|
|
98
|
+
nodes: list[Node] = []
|
|
99
|
+
edges: list[Edge] = []
|
|
100
|
+
diagnostics: list[Diagnostic] = []
|
|
101
|
+
|
|
102
|
+
py_files = sorted(
|
|
103
|
+
(f for f in files if self.can_handle(f)),
|
|
104
|
+
key=lambda item: item.path,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Lookup map of short symbol name -> Node ID for linking handlers, schemas, and dependencies
|
|
108
|
+
symbol_by_short_name: dict[str, str] = {}
|
|
109
|
+
for existing in existing_nodes:
|
|
110
|
+
if existing.kind in ("Function", "Method", "Class"):
|
|
111
|
+
symbol_by_short_name.setdefault(existing.name, existing.id)
|
|
112
|
+
|
|
113
|
+
schema_id_by_name: dict[str, str] = {}
|
|
114
|
+
pending_endpoints: list[tuple[str, str, str, list[str], list[str], Provenance]] = []
|
|
115
|
+
|
|
116
|
+
for scanned in py_files:
|
|
117
|
+
rel_posix = normalize_posix_path(scanned.path)
|
|
118
|
+
try:
|
|
119
|
+
source_text = (repo_root / rel_posix).read_text(encoding="utf-8")
|
|
120
|
+
tree = ast.parse(source_text, filename=rel_posix)
|
|
121
|
+
except (OSError, SyntaxError, ValueError):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
mod_name = compute_python_module_name(rel_posix)
|
|
125
|
+
|
|
126
|
+
for stmt in tree.body:
|
|
127
|
+
if isinstance(stmt, ast.ClassDef):
|
|
128
|
+
base_names = {_unparse(b).split(".")[-1] for b in stmt.bases}
|
|
129
|
+
if base_names & PYDANTIC_BASE_NAMES:
|
|
130
|
+
fields_info: list[dict[str, str]] = []
|
|
131
|
+
for body_item in stmt.body:
|
|
132
|
+
if isinstance(body_item, ast.AnnAssign) and isinstance(
|
|
133
|
+
body_item.target, ast.Name
|
|
134
|
+
):
|
|
135
|
+
fname = body_item.target.id
|
|
136
|
+
if fname.startswith("_"):
|
|
137
|
+
continue
|
|
138
|
+
ftype = _unparse(body_item.annotation)
|
|
139
|
+
fdefault = (
|
|
140
|
+
_unparse(body_item.value) if body_item.value is not None else ""
|
|
141
|
+
)
|
|
142
|
+
fields_info.append(
|
|
143
|
+
{"name": fname, "type": ftype, "default": fdefault}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
line_start = getattr(stmt, "lineno", 1)
|
|
147
|
+
line_end = getattr(stmt, "end_lineno", line_start) or line_start
|
|
148
|
+
schema_id = make_symbol_id("schema:py", rel_posix, stmt.name)
|
|
149
|
+
prov = Provenance(
|
|
150
|
+
source_path=rel_posix,
|
|
151
|
+
line_start=line_start,
|
|
152
|
+
line_end=line_end,
|
|
153
|
+
extractor=EXTRACTOR_ID,
|
|
154
|
+
evidence_kind="deterministic",
|
|
155
|
+
confidence="high",
|
|
156
|
+
)
|
|
157
|
+
field_summary = ", ".join(f"{f['name']}: {f['type']}" for f in fields_info)
|
|
158
|
+
nodes.append(
|
|
159
|
+
Node(
|
|
160
|
+
id=schema_id,
|
|
161
|
+
kind="Schema",
|
|
162
|
+
name=stmt.name,
|
|
163
|
+
qualified_name=f"{mod_name}.{stmt.name}",
|
|
164
|
+
attributes={
|
|
165
|
+
"fields": fields_info,
|
|
166
|
+
"signature": f"({field_summary})",
|
|
167
|
+
"docstring_summary": sanitize_docstring_summary(
|
|
168
|
+
ast.get_docstring(stmt)
|
|
169
|
+
),
|
|
170
|
+
},
|
|
171
|
+
provenance=prov,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
schema_id_by_name[stmt.name] = schema_id
|
|
175
|
+
|
|
176
|
+
elif isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
177
|
+
for dec in stmt.decorator_list:
|
|
178
|
+
route_info = _extract_route_decorator(dec)
|
|
179
|
+
if route_info is None:
|
|
180
|
+
continue
|
|
181
|
+
http_method, route_path, resp_model, status_code = route_info
|
|
182
|
+
line_start = getattr(stmt, "lineno", 1)
|
|
183
|
+
line_end = getattr(stmt, "end_lineno", line_start) or line_start
|
|
184
|
+
endpoint_name = f"{http_method} {route_path}"
|
|
185
|
+
endpoint_id = make_symbol_id(
|
|
186
|
+
"endpoint:py", rel_posix, f"{http_method}:{route_path}"
|
|
187
|
+
)
|
|
188
|
+
prov = Provenance(
|
|
189
|
+
source_path=rel_posix,
|
|
190
|
+
line_start=line_start,
|
|
191
|
+
line_end=line_end,
|
|
192
|
+
extractor=EXTRACTOR_ID,
|
|
193
|
+
evidence_kind="deterministic",
|
|
194
|
+
confidence="high",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Inspect parameters for Pydantic request schemas and Depends(...)
|
|
198
|
+
schema_refs: list[str] = []
|
|
199
|
+
if resp_model:
|
|
200
|
+
schema_refs.append(resp_model)
|
|
201
|
+
|
|
202
|
+
all_args = list(stmt.args.posonlyargs) + list(stmt.args.args)
|
|
203
|
+
for arg in all_args:
|
|
204
|
+
if arg.annotation is not None:
|
|
205
|
+
ann_short = _unparse(arg.annotation).split(".")[-1]
|
|
206
|
+
if ann_short:
|
|
207
|
+
schema_refs.append(ann_short)
|
|
208
|
+
|
|
209
|
+
dep_refs: list[str] = []
|
|
210
|
+
for default_expr in list(stmt.args.defaults) + [
|
|
211
|
+
d for d in stmt.args.kw_defaults if d is not None
|
|
212
|
+
]:
|
|
213
|
+
dep_target = _extract_depends_target(default_expr)
|
|
214
|
+
if dep_target:
|
|
215
|
+
dep_refs.append(dep_target)
|
|
216
|
+
|
|
217
|
+
nodes.append(
|
|
218
|
+
Node(
|
|
219
|
+
id=endpoint_id,
|
|
220
|
+
kind="Endpoint",
|
|
221
|
+
name=endpoint_name,
|
|
222
|
+
qualified_name=endpoint_name,
|
|
223
|
+
attributes={
|
|
224
|
+
"http_method": http_method,
|
|
225
|
+
"route_path": route_path,
|
|
226
|
+
"handler": f"{mod_name}.{stmt.name}",
|
|
227
|
+
"response_model": resp_model,
|
|
228
|
+
"status_code": status_code or 200,
|
|
229
|
+
"signature": (
|
|
230
|
+
f"[{http_method} {route_path}] -> " f"{resp_model or 'Any'}"
|
|
231
|
+
),
|
|
232
|
+
"docstring_summary": sanitize_docstring_summary(
|
|
233
|
+
ast.get_docstring(stmt)
|
|
234
|
+
),
|
|
235
|
+
},
|
|
236
|
+
provenance=prov,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
handler_sym_id = make_symbol_id("py", rel_posix, stmt.name)
|
|
240
|
+
pending_endpoints.append(
|
|
241
|
+
(
|
|
242
|
+
endpoint_id,
|
|
243
|
+
handler_sym_id,
|
|
244
|
+
stmt.name,
|
|
245
|
+
schema_refs,
|
|
246
|
+
dep_refs,
|
|
247
|
+
prov,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Link EXPOSES, VALIDATES_WITH, and DEPENDS_ON edges
|
|
252
|
+
for (
|
|
253
|
+
endpoint_id,
|
|
254
|
+
handler_sym_id,
|
|
255
|
+
_handler_name,
|
|
256
|
+
schema_refs,
|
|
257
|
+
dep_refs,
|
|
258
|
+
prov,
|
|
259
|
+
) in pending_endpoints:
|
|
260
|
+
edges.append(
|
|
261
|
+
Edge(
|
|
262
|
+
id=make_edge_id("EXPOSES", handler_sym_id, endpoint_id),
|
|
263
|
+
kind="EXPOSES",
|
|
264
|
+
source_id=handler_sym_id,
|
|
265
|
+
target_id=endpoint_id,
|
|
266
|
+
provenance=prov,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
for s_ref in sorted(set(schema_refs)):
|
|
270
|
+
if s_ref in schema_id_by_name:
|
|
271
|
+
s_id = schema_id_by_name[s_ref]
|
|
272
|
+
edges.append(
|
|
273
|
+
Edge(
|
|
274
|
+
id=make_edge_id("VALIDATES_WITH", endpoint_id, s_id),
|
|
275
|
+
kind="VALIDATES_WITH",
|
|
276
|
+
source_id=endpoint_id,
|
|
277
|
+
target_id=s_id,
|
|
278
|
+
provenance=prov,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
edges.append(
|
|
282
|
+
Edge(
|
|
283
|
+
id=make_edge_id("VALIDATES_WITH", handler_sym_id, s_id),
|
|
284
|
+
kind="VALIDATES_WITH",
|
|
285
|
+
source_id=handler_sym_id,
|
|
286
|
+
target_id=s_id,
|
|
287
|
+
provenance=prov,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
for d_ref in sorted(set(dep_refs)):
|
|
291
|
+
if d_ref in symbol_by_short_name:
|
|
292
|
+
d_id = symbol_by_short_name[d_ref]
|
|
293
|
+
edges.append(
|
|
294
|
+
Edge(
|
|
295
|
+
id=make_edge_id("DEPENDS_ON", endpoint_id, d_id),
|
|
296
|
+
kind="DEPENDS_ON",
|
|
297
|
+
source_id=endpoint_id,
|
|
298
|
+
target_id=d_id,
|
|
299
|
+
provenance=prov,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return AdapterExtractionResult(
|
|
304
|
+
nodes=tuple(nodes),
|
|
305
|
+
edges=tuple(edges),
|
|
306
|
+
diagnostics=tuple(diagnostics),
|
|
307
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Unified multi-language and framework contract extraction pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from context_archive.adapters.base import AdapterExtractionResult
|
|
8
|
+
from context_archive.adapters.fastapi import FastApiContractExtractor
|
|
9
|
+
from context_archive.adapters.python_ast import PythonAstAdapter
|
|
10
|
+
from context_archive.adapters.sqlalchemy import SqlAlchemyEntityExtractor
|
|
11
|
+
from context_archive.adapters.typescript import TypeScriptAdapter
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from context_archive.scanner.report import ScannedFile
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_all_adapters(
|
|
20
|
+
repo_root: Path,
|
|
21
|
+
files: tuple[ScannedFile, ...] | list[ScannedFile],
|
|
22
|
+
) -> AdapterExtractionResult:
|
|
23
|
+
"""Run all active language and framework adapters and link cross-stack contracts."""
|
|
24
|
+
py_adapter = PythonAstAdapter()
|
|
25
|
+
py_res = py_adapter.extract_repository(repo_root, files)
|
|
26
|
+
|
|
27
|
+
fastapi_extractor = FastApiContractExtractor()
|
|
28
|
+
fastapi_res = fastapi_extractor.extract_repository(
|
|
29
|
+
repo_root,
|
|
30
|
+
files,
|
|
31
|
+
existing_nodes=py_res.nodes,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
sqlalchemy_extractor = SqlAlchemyEntityExtractor()
|
|
35
|
+
sa_res = sqlalchemy_extractor.extract_repository(repo_root, files)
|
|
36
|
+
|
|
37
|
+
combined_backend_nodes = (*py_res.nodes, *fastapi_res.nodes, *sa_res.nodes)
|
|
38
|
+
ts_adapter = TypeScriptAdapter()
|
|
39
|
+
ts_res = ts_adapter.extract_repository(
|
|
40
|
+
repo_root,
|
|
41
|
+
files,
|
|
42
|
+
existing_nodes=combined_backend_nodes,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return AdapterExtractionResult(
|
|
46
|
+
nodes=(*py_res.nodes, *fastapi_res.nodes, *sa_res.nodes, *ts_res.nodes),
|
|
47
|
+
edges=(*py_res.edges, *fastapi_res.edges, *sa_res.edges, *ts_res.edges),
|
|
48
|
+
diagnostics=(
|
|
49
|
+
*py_res.diagnostics,
|
|
50
|
+
*fastapi_res.diagnostics,
|
|
51
|
+
*sa_res.diagnostics,
|
|
52
|
+
*ts_res.diagnostics,
|
|
53
|
+
),
|
|
54
|
+
)
|