adl-toolkit 0.0.0-stage → 0.1.1

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 (104) hide show
  1. package/INSTALL_TOOLKIT.md +163 -0
  2. package/README.md +269 -2
  3. package/adl_drift/__init__.py +6 -0
  4. package/adl_drift/cli/__init__.py +1 -0
  5. package/adl_drift/cli/commands.py +993 -0
  6. package/adl_drift/cli/main.py +744 -0
  7. package/adl_drift/cortex/__init__.py +1 -0
  8. package/adl_drift/cortex/embeddings.py +96 -0
  9. package/adl_drift/cortex/memory.py +1749 -0
  10. package/adl_drift/cortex/patterns.py +89 -0
  11. package/adl_drift/cortex/telemetry.py +192 -0
  12. package/adl_drift/detectors/__init__.py +1 -0
  13. package/adl_drift/detectors/async_patterns.py +115 -0
  14. package/adl_drift/detectors/base.py +121 -0
  15. package/adl_drift/detectors/config.py +122 -0
  16. package/adl_drift/detectors/data_access.py +128 -0
  17. package/adl_drift/detectors/decorators.py +122 -0
  18. package/adl_drift/detectors/dynamic_patterns.py +260 -0
  19. package/adl_drift/detectors/errors.py +134 -0
  20. package/adl_drift/detectors/imports.py +132 -0
  21. package/adl_drift/detectors/logging_patterns.py +108 -0
  22. package/adl_drift/detectors/routes.py +257 -0
  23. package/adl_drift/detectors/security.py +236 -0
  24. package/adl_drift/detectors/taint.py +406 -0
  25. package/adl_drift/detectors/tests.py +95 -0
  26. package/adl_drift/detectors/type_conventions.py +81 -0
  27. package/adl_drift/graph/__init__.py +1 -0
  28. package/adl_drift/graph/builder.py +2670 -0
  29. package/adl_drift/graph/cfg.py +461 -0
  30. package/adl_drift/graph/cluster.py +40 -0
  31. package/adl_drift/graph/edges.py +86 -0
  32. package/adl_drift/graph/node.py +90 -0
  33. package/adl_drift/harness/__init__.py +10 -0
  34. package/adl_drift/harness/setup.py +832 -0
  35. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  36. package/adl_drift/harness/skills/drift-generic.md +19 -0
  37. package/adl_drift/harness/skills/drift.md +25 -0
  38. package/adl_drift/harness/watch.py +84 -0
  39. package/adl_drift/lang/__init__.py +8 -0
  40. package/adl_drift/lang/base.py +464 -0
  41. package/adl_drift/lang/java/__init__.py +6 -0
  42. package/adl_drift/lang/java/backend.py +97 -0
  43. package/adl_drift/lang/java/cfg.py +1424 -0
  44. package/adl_drift/lang/java/config.py +707 -0
  45. package/adl_drift/lang/java/detectors/__init__.py +29 -0
  46. package/adl_drift/lang/java/detectors/additional.py +445 -0
  47. package/adl_drift/lang/java/detectors/base.py +154 -0
  48. package/adl_drift/lang/java/detectors/data_access.py +148 -0
  49. package/adl_drift/lang/java/detectors/imports.py +53 -0
  50. package/adl_drift/lang/java/detectors/routes.py +159 -0
  51. package/adl_drift/lang/java/detectors/security.py +386 -0
  52. package/adl_drift/lang/java/detectors/tests.py +74 -0
  53. package/adl_drift/lang/java/extractor.py +582 -0
  54. package/adl_drift/lang/java/ids.py +99 -0
  55. package/adl_drift/lang/java/parser.py +49 -0
  56. package/adl_drift/lang/java/resolution/__init__.py +1 -0
  57. package/adl_drift/lang/java/resolution/constants.py +1619 -0
  58. package/adl_drift/lang/java/resolution/constructors.py +278 -0
  59. package/adl_drift/lang/java/resolution/context.py +305 -0
  60. package/adl_drift/lang/java/resolution/dispatch.py +540 -0
  61. package/adl_drift/lang/java/resolution/functional.py +1318 -0
  62. package/adl_drift/lang/java/resolution/generated.py +95 -0
  63. package/adl_drift/lang/java/resolution/model.py +153 -0
  64. package/adl_drift/lang/java/resolution/orchestration.py +866 -0
  65. package/adl_drift/lang/java/resolution/overloads.py +1275 -0
  66. package/adl_drift/lang/java/resolution/scopes.py +1111 -0
  67. package/adl_drift/lang/java/resolution/types.py +2947 -0
  68. package/adl_drift/lang/java/resolver.py +1401 -0
  69. package/adl_drift/lang/java/scopes.py +69 -0
  70. package/adl_drift/lang/java/taint.py +500 -0
  71. package/adl_drift/lang/java/type_provenance.py +342 -0
  72. package/adl_drift/lang/python/__init__.py +8 -0
  73. package/adl_drift/lang/python/backend.py +94 -0
  74. package/adl_drift/lang/python/cfg.py +537 -0
  75. package/adl_drift/lang/python/extractor.py +454 -0
  76. package/adl_drift/lang/python/parser.py +41 -0
  77. package/adl_drift/lang/python/resolver.py +1789 -0
  78. package/adl_drift/lang/python/taint.py +365 -0
  79. package/adl_drift/lang/registry.py +117 -0
  80. package/adl_drift/parser/__init__.py +1 -0
  81. package/adl_drift/parser/cache.py +38 -0
  82. package/adl_drift/parser/core.py +33 -0
  83. package/adl_drift/parser/extractor.py +17 -0
  84. package/adl_drift/parser/literal_eval.py +326 -0
  85. package/adl_drift/scanner/__init__.py +1 -0
  86. package/adl_drift/scanner/discovery.py +338 -0
  87. package/adl_drift/scanner/incremental.py +261 -0
  88. package/adl_drift/scanner/parallel.py +224 -0
  89. package/adl_drift/server/__init__.py +1 -0
  90. package/adl_drift/server/handlers.py +2849 -0
  91. package/adl_drift/server/mcp_server.py +127 -0
  92. package/adl_drift/server/mcp_stdio.py +335 -0
  93. package/adl_drift/server/protocol.py +71 -0
  94. package/adl_drift/server/static/index.html +696 -0
  95. package/adl_drift/server/web.py +350 -0
  96. package/adl_drift/utils/__init__.py +1 -0
  97. package/adl_drift/utils/config.py +77 -0
  98. package/adl_drift/utils/fswalk.py +52 -0
  99. package/adl_drift/utils/types.py +345 -0
  100. package/bin/adl-drift.mjs +35 -0
  101. package/bin/adl-toolkit.mjs +488 -0
  102. package/package.json +29 -4
  103. package/pyproject.toml +49 -0
  104. package/scripts/install_toolkit.py +3584 -0
@@ -0,0 +1,365 @@
1
+ """Python intra-function taint mechanics (P2.19, ported unchanged onto the
2
+ P4.6 seam).
3
+
4
+ `PythonTaintStrategy` is the exact pre-P4.6 `detectors.taint._analyze_one`
5
+ CST walk, now a `detectors.taint.TaintStrategy` subclass reading its
6
+ source/sink tables from `self.config` (a `CFGConfig`) instead of hardcoded
7
+ module constants. No behavioral change for Python — `default_python_taint_
8
+ strategy()` below is what `lang/python/cfg.py::default_python_cfg_config()`
9
+ packages `TAINT_SINKS`/`SINK_METHODS` into, so every existing call site sees
10
+ byte-identical output.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Dict, List, Optional, Set, Tuple
16
+
17
+ from tree_sitter import Node
18
+
19
+ from adl_drift.detectors.taint import TaintPath, TaintStrategy, _OutCall
20
+ from adl_drift.lang.base import CFGConfig
21
+ from adl_drift.utils.types import SourcePosition
22
+
23
+ __all__ = [
24
+ "TAINT_SINKS",
25
+ "SINK_METHODS",
26
+ "PythonTaintStrategy",
27
+ "default_python_taint_strategy",
28
+ ]
29
+
30
+ # ─── sinks ────────────────────────────────────────────────────────────────────
31
+
32
+ # Exact dotted/identifier callable text → vulnerability category.
33
+ TAINT_SINKS: Dict[str, str] = {
34
+ "eval": "code_injection",
35
+ "exec": "code_injection",
36
+ "os.system": "command_injection",
37
+ "os.popen": "command_injection",
38
+ "subprocess.run": "command_injection",
39
+ "subprocess.Popen": "command_injection",
40
+ "subprocess.call": "command_injection",
41
+ "subprocess.check_call": "command_injection",
42
+ "subprocess.check_output": "command_injection",
43
+ "pickle.load": "unsafe_deserialization",
44
+ "pickle.loads": "unsafe_deserialization",
45
+ "yaml.load": "unsafe_deserialization",
46
+ }
47
+
48
+ # Final attribute name (any receiver) → category. Matches `db.execute(q)`,
49
+ # `conn.cursor().execute(q)`, `self.session.execute(q)`, etc.
50
+ SINK_METHODS: Dict[str, str] = {
51
+ "execute": "sql_injection",
52
+ "executemany": "sql_injection",
53
+ "executescript": "sql_injection",
54
+ "raw": "sql_injection",
55
+ }
56
+
57
+
58
+ # ─── CST helpers (self-contained; mirror lang/python/cfg.py conventions) ─────
59
+
60
+ def _text(node: Node, source: bytes) -> str:
61
+ try:
62
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
63
+ except Exception:
64
+ return ""
65
+
66
+
67
+ def _pos(node: Node) -> SourcePosition:
68
+ return SourcePosition(
69
+ line=node.start_point[0] + 1,
70
+ column=node.start_point[1],
71
+ end_line=node.end_point[0] + 1,
72
+ end_column=node.end_point[1],
73
+ )
74
+
75
+
76
+ def _ids_in(node: Optional[Node], source: bytes) -> Set[str]:
77
+ """Identifier *reads* under *node* (attribute receivers only, not field names)."""
78
+ out: Set[str] = set()
79
+ if node is None:
80
+ return out
81
+ stack = [node]
82
+ while stack:
83
+ n = stack.pop()
84
+ t = n.type
85
+ if t == "attribute":
86
+ obj = n.child_by_field_name("object")
87
+ if obj is not None:
88
+ stack.append(obj)
89
+ continue
90
+ if t == "keyword_argument":
91
+ val = n.child_by_field_name("value")
92
+ if val is not None:
93
+ stack.append(val)
94
+ continue
95
+ if t in ("function_definition", "class_definition", "lambda"):
96
+ continue
97
+ if t == "identifier":
98
+ txt = _text(n, source)
99
+ if txt:
100
+ out.add(txt)
101
+ continue
102
+ stack.extend(n.children)
103
+ return out
104
+
105
+
106
+ def _lhs_names(node: Node, source: bytes) -> List[str]:
107
+ if node.type == "identifier":
108
+ return [_text(node, source)]
109
+ names: List[str] = []
110
+ for child in node.children:
111
+ if child.type not in (",", "(", ")", "[", "]", "*", "."):
112
+ names.extend(_lhs_names(child, source))
113
+ return names
114
+
115
+
116
+ def _param_names(func_node: Node, source: bytes) -> List[str]:
117
+ """Ordered parameter identifiers (includes ``self``/``cls``)."""
118
+ params = func_node.child_by_field_name("parameters")
119
+ if params is None:
120
+ return []
121
+ names: List[str] = []
122
+ for child in params.children:
123
+ ct = child.type
124
+ if ct == "identifier":
125
+ names.append(_text(child, source))
126
+ elif ct in ("typed_parameter", "default_parameter", "typed_default_parameter",
127
+ "list_splat_pattern", "dictionary_splat_pattern"):
128
+ inner = child.child_by_field_name("name") or (
129
+ child.children[0] if child.children else None
130
+ )
131
+ if inner is not None and inner.type == "identifier":
132
+ names.append(_text(inner, source))
133
+ return names
134
+
135
+
136
+ def _collect_calls(body: Node) -> List[Node]:
137
+ """Every ``call`` node under *body*, not descending into nested functions."""
138
+ out: List[Node] = []
139
+ stack = [body]
140
+ while stack:
141
+ node = stack.pop()
142
+ if node.type == "call":
143
+ out.append(node)
144
+ if node.type == "function_definition" and node is not body:
145
+ continue
146
+ stack.extend(node.children)
147
+ return out
148
+
149
+
150
+ def _positional_args(call: Node) -> List[Node]:
151
+ args = call.child_by_field_name("arguments")
152
+ if args is None:
153
+ return []
154
+ return [c for c in args.children
155
+ if c.type not in ("(", ")", ",", "comment", "keyword_argument",
156
+ "list_splat", "dictionary_splat")]
157
+
158
+
159
+ def _keyword_args(call: Node, source: bytes) -> List[Tuple[str, Node]]:
160
+ args = call.child_by_field_name("arguments")
161
+ if args is None:
162
+ return []
163
+ out: List[Tuple[str, Node]] = []
164
+ for c in args.children:
165
+ if c.type == "keyword_argument":
166
+ name = c.child_by_field_name("name")
167
+ val = c.child_by_field_name("value")
168
+ if name is not None and val is not None:
169
+ out.append((_text(name, source), val))
170
+ return out
171
+
172
+
173
+ def _callee_simple_name(call: Node, source: bytes) -> Optional[str]:
174
+ """Final identifier of the call target: ``f`` / ``m.f`` / ``a.b.f`` → ``f``."""
175
+ fn = call.child_by_field_name("function")
176
+ if fn is None:
177
+ return None
178
+ if fn.type == "identifier":
179
+ return _text(fn, source)
180
+ if fn.type == "attribute":
181
+ attr = fn.child_by_field_name("attribute")
182
+ if attr is not None and attr.type == "identifier":
183
+ return _text(attr, source)
184
+ return None
185
+
186
+
187
+ class PythonTaintStrategy(TaintStrategy):
188
+ """Python's `TaintStrategy` (P2.19/P4.6)."""
189
+
190
+ def param_names(self, func_node: Node, source: bytes) -> List[str]:
191
+ return _param_names(func_node, source)
192
+
193
+ def _source_prefix(self, text: str) -> Optional[str]:
194
+ """Return the taint-source label if *text* begins with a known source."""
195
+ t = text.strip()
196
+ for src in self.config.taint_sources:
197
+ if t.startswith(src):
198
+ return src
199
+ return None
200
+
201
+ def _sink_info(self, call: Node, source: bytes) -> Optional[Tuple[str, str, Node]]:
202
+ """If *call* is a taint sink, return ``(callable_text, category, arg_node)``.
203
+
204
+ ``arg_node`` is the first positional argument (the data the sink consumes).
205
+ """
206
+ fn = call.child_by_field_name("function")
207
+ if fn is None:
208
+ return None
209
+ func_text = _text(fn, source).strip()
210
+ category = self.config.taint_sinks.get(func_text)
211
+ if category is None and fn.type == "attribute":
212
+ attr = fn.child_by_field_name("attribute")
213
+ if attr is not None:
214
+ category = self.config.sink_methods.get(_text(attr, source))
215
+ if category is None:
216
+ return None
217
+ pos_args = _positional_args(call)
218
+ if not pos_args:
219
+ return None
220
+ return func_text, category, pos_args[0]
221
+
222
+ def analyze_one(
223
+ self,
224
+ func_node: Node,
225
+ source: bytes,
226
+ function_id: str,
227
+ file_path: str,
228
+ seed_params: Set[str],
229
+ ) -> Tuple[Set[str], List[TaintPath], List[_OutCall]]:
230
+ body = func_node.child_by_field_name("body")
231
+ if body is None:
232
+ return set(), [], []
233
+
234
+ tainted: Set[str] = set(seed_params)
235
+ source_of: Dict[str, str] = {p: "parameter" for p in seed_params}
236
+
237
+ # ── collect assignment records (lhs names, rhs identifier reads, src label) ──
238
+ assigns: List[Tuple[List[str], Set[str], Optional[str]]] = []
239
+
240
+ def _record_assign(lhs: Optional[Node], rhs: Optional[Node]) -> None:
241
+ if lhs is None or rhs is None:
242
+ return
243
+ names = [n for n in _lhs_names(lhs, source) if n]
244
+ if not names:
245
+ return
246
+ assigns.append((names, _ids_in(rhs, source),
247
+ self._source_prefix(_text(rhs, source))))
248
+
249
+ stack = [body]
250
+ while stack:
251
+ node = stack.pop()
252
+ t = node.type
253
+ if t == "function_definition" and node is not body:
254
+ continue # nested function — separate analysis
255
+ if t == "assignment":
256
+ _record_assign(node.child_by_field_name("left"),
257
+ node.child_by_field_name("right"))
258
+ elif t == "augmented_assignment":
259
+ lhs = node.child_by_field_name("left")
260
+ rhs = node.child_by_field_name("right")
261
+ if lhs is not None and rhs is not None:
262
+ names = [n for n in _lhs_names(lhs, source) if n]
263
+ rhs_ids = _ids_in(rhs, source) | set(names) # x += y keeps x
264
+ assigns.append((names, rhs_ids, self._source_prefix(_text(rhs, source))))
265
+ elif t == "named_expression":
266
+ _record_assign(node.child_by_field_name("name"),
267
+ node.child_by_field_name("value"))
268
+ elif t == "for_statement":
269
+ # `for x in <iter>` taints x when the iterable is tainted
270
+ _record_assign(node.child_by_field_name("left"),
271
+ node.child_by_field_name("right"))
272
+ elif t == "with_item":
273
+ _record_assign(node.child_by_field_name("alias"),
274
+ node.child_by_field_name("value"))
275
+ stack.extend(node.children)
276
+
277
+ # ── propagate to fixpoint ───────────────────────────────────────────────
278
+ changed = True
279
+ while changed:
280
+ changed = False
281
+ for names, rhs_ids, lbl in assigns:
282
+ tainted_rhs = lbl or any(i in tainted for i in rhs_ids)
283
+ if not tainted_rhs:
284
+ continue
285
+ label = lbl or next(
286
+ (source_of.get(i) for i in rhs_ids if i in tainted), None
287
+ )
288
+ for n in names:
289
+ if n not in tainted:
290
+ tainted.add(n)
291
+ source_of[n] = label or "tainted"
292
+ changed = True
293
+
294
+ # ── sink detection ──────────────────────────────────────────────────────
295
+ paths: List[TaintPath] = []
296
+ seen_sites: Set[Tuple[int, int]] = set()
297
+ for call in _collect_calls(body):
298
+ info = self._sink_info(call, source)
299
+ if info is None:
300
+ continue
301
+ callable_text, category, arg_node = info
302
+ site = _pos(call)
303
+ site_key = (site.line, site.column)
304
+
305
+ direct = self._source_prefix(_text(arg_node, source))
306
+ if direct:
307
+ binding: Optional[str] = None
308
+ label = direct
309
+ cross = False
310
+ else:
311
+ arg_ids = _ids_in(arg_node, source)
312
+ hit = next((i for i in arg_ids if i in tainted), None)
313
+ if hit is None:
314
+ continue
315
+ binding = hit
316
+ label = source_of.get(hit, "tainted")
317
+ cross = label == "parameter"
318
+
319
+ if site_key in seen_sites:
320
+ continue
321
+ seen_sites.add(site_key)
322
+
323
+ chain = [label]
324
+ if binding:
325
+ chain.append(binding)
326
+ chain.append(callable_text)
327
+ paths.append(TaintPath(
328
+ function_id=function_id,
329
+ file_path=file_path,
330
+ sink=callable_text,
331
+ sink_category=category,
332
+ source_label=label,
333
+ binding_name=binding,
334
+ sink_site=site,
335
+ chain=chain,
336
+ confidence=0.7 if cross else 0.85,
337
+ cross_function=cross,
338
+ ))
339
+
340
+ # ── outgoing tainted calls (for cross-function propagation) ─────────────
341
+ out_calls: List[_OutCall] = []
342
+ for call in _collect_calls(body):
343
+ callee = _callee_simple_name(call, source)
344
+ if not callee:
345
+ continue
346
+ pos_t: Set[int] = set()
347
+ for idx, arg in enumerate(_positional_args(call)):
348
+ if self._source_prefix(_text(arg, source)) or (_ids_in(arg, source) & tainted):
349
+ pos_t.add(idx)
350
+ kw_t: Set[str] = set()
351
+ for kw_name, val in _keyword_args(call, source):
352
+ if self._source_prefix(_text(val, source)) or (_ids_in(val, source) & tainted):
353
+ kw_t.add(kw_name)
354
+ if pos_t or kw_t:
355
+ out_calls.append(_OutCall(
356
+ callee, pos_t, kw_t,
357
+ site_line=call.start_point[0] + 1,
358
+ site_column=call.start_point[1],
359
+ ))
360
+
361
+ return tainted, paths, out_calls
362
+
363
+
364
+ def default_python_taint_strategy(config: CFGConfig) -> PythonTaintStrategy:
365
+ return PythonTaintStrategy(config)
@@ -0,0 +1,117 @@
1
+ """Backend registry — dispatch by name / file extension / project root.
2
+
3
+ The engine layer (scanner, cli/commands, server/handlers) asks this registry
4
+ for a `LanguageBackend` instead of importing a concrete implementation
5
+ (P4.0/P4.3, PROBLEMS.md). File discovery is still Python-only until P4.8.
6
+
7
+ A simple dict-keyed registry is deliberate — no plugin discovery, no entry
8
+ points. Backends register themselves via `register_backend()`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional
15
+
16
+ from adl_drift.lang.base import LanguageBackend
17
+ from adl_drift.utils.fswalk import iter_tree
18
+
19
+ _REGISTRY: Dict[str, LanguageBackend] = {}
20
+
21
+
22
+ def register_backend(backend: LanguageBackend) -> None:
23
+ """Register (or replace) a backend under `backend.name`."""
24
+ _REGISTRY[backend.name] = backend
25
+
26
+
27
+ def get_backend(name: str) -> LanguageBackend:
28
+ """Look up a backend by name (e.g. "python", "java").
29
+
30
+ Raises `KeyError` if no backend is registered under that name — there is
31
+ no silent Python fallback, so an unregistered language fails loudly.
32
+ """
33
+ try:
34
+ return _REGISTRY[name]
35
+ except KeyError:
36
+ raise KeyError(
37
+ f"No language backend registered for {name!r}. "
38
+ f"Registered backends: {sorted(_REGISTRY)}"
39
+ ) from None
40
+
41
+
42
+ def backend_for_file(path: Path) -> Optional[LanguageBackend]:
43
+ """Dispatch a file path to the backend claiming its extension.
44
+
45
+ Returns `None` when no registered backend claims the extension (e.g.
46
+ `.java` today, since no Java backend is registered yet) — callers decide
47
+ whether that's an error or a file to skip.
48
+ """
49
+ suffix = Path(path).suffix
50
+ for backend in _REGISTRY.values():
51
+ if suffix in backend.file_extensions:
52
+ return backend
53
+ return None
54
+
55
+
56
+ def detect_backends(root: Path) -> List[LanguageBackend]:
57
+ """Scan a project root for which registered languages are present.
58
+
59
+ A backend is considered present if any of its `project_markers` exist
60
+ directly under `root`, or if any file with one of its
61
+ `file_extensions` exists anywhere under `root`.
62
+
63
+ MCP_GRAPH_CACHE_PLAN.md Lớp 2: backends without a root marker are
64
+ probed together in one ``os.walk`` that stops as soon as every pending
65
+ backend has been seen, instead of one full ``rglob`` per backend per
66
+ extension (proving a language is *absent* used to cost a whole-tree
67
+ walk each). ``rglob`` matched directory entries too, so directory names
68
+ count as well.
69
+ """
70
+ root = Path(root)
71
+ backends = list(_REGISTRY.values())
72
+ present = {
73
+ backend.name for backend in backends
74
+ if any((root / marker).exists() for marker in backend.project_markers)
75
+ }
76
+ pending: Dict[str, str] = {
77
+ ext: backend.name
78
+ for backend in backends if backend.name not in present
79
+ for ext in backend.file_extensions
80
+ }
81
+ if pending:
82
+ for _dirpath, dirs, others in iter_tree(str(root)):
83
+ for entry in dirs + others:
84
+ for ext, backend_name in list(pending.items()):
85
+ if entry.name.endswith(ext):
86
+ present.add(backend_name)
87
+ pending = {
88
+ e: b for e, b in pending.items() if b != backend_name
89
+ }
90
+ if not pending:
91
+ break
92
+ return [backend for backend in backends if backend.name in present]
93
+
94
+
95
+ def _register_default_backends() -> None:
96
+ """Best-effort registration of bundled backends.
97
+
98
+ Wrapped in try/except so importing `adl_drift.lang.registry` never
99
+ fails outright even if the shim's optional dependencies are missing —
100
+ matches this module's job as pure dispatch scaffolding, not a hard
101
+ dependency on any concrete backend.
102
+ """
103
+ try:
104
+ from adl_drift.lang.python.backend import PythonBackend
105
+ if "python" not in _REGISTRY:
106
+ register_backend(PythonBackend())
107
+ except Exception:
108
+ pass
109
+ try:
110
+ from adl_drift.lang.java.backend import JavaBackend
111
+ if "java" not in _REGISTRY:
112
+ register_backend(JavaBackend())
113
+ except Exception:
114
+ pass
115
+
116
+
117
+ _register_default_backends()
@@ -0,0 +1 @@
1
+ """Tree-sitter parser wrapper for Python source files."""
@@ -0,0 +1,38 @@
1
+ """Minimal in-memory parse cache (SHA256-keyed). Disk persistence stubbed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional
8
+
9
+
10
+ class ParseCache:
11
+ def __init__(self, cache_dir: Optional[Path] = None) -> None:
12
+ self.cache_dir = cache_dir
13
+ self._mem: Dict[str, Any] = {}
14
+
15
+ @staticmethod
16
+ def _file_hash(path: Path) -> str:
17
+ return hashlib.sha256(path.read_bytes()).hexdigest()
18
+
19
+ def get(self, path: Path) -> Optional[Any]:
20
+ try:
21
+ key = self._file_hash(path)
22
+ except OSError:
23
+ return None
24
+ return self._mem.get(key)
25
+
26
+ def put(self, path: Path, value: Any) -> None:
27
+ try:
28
+ key = self._file_hash(path)
29
+ except OSError:
30
+ return
31
+ self._mem[key] = value
32
+
33
+ def invalidate(self, path: Path) -> None:
34
+ try:
35
+ key = self._file_hash(path)
36
+ except OSError:
37
+ return
38
+ self._mem.pop(key, None)
@@ -0,0 +1,33 @@
1
+ """Tree-sitter Python parser wrapper.
2
+
3
+ Generalized into `lang/base.py::LanguageParser` (P4.1, PROBLEMS.md) — the
4
+ grammar-agnostic parsing logic (error-node collection, byte-offset ->
5
+ `SourcePosition` conversion, `node_text`) now lives there, constructed with a
6
+ `tree_sitter.Language` + query map instead of a hardcoded global. This module
7
+ re-exports the Python-configured parser under its original names
8
+ (`PythonParser`, `ParseResult`, `PYTHON_LANGUAGE`, `QUERIES`) so existing call
9
+ sites (`DriftCommands`, `MCPHandlers`, `scanner/parallel.py::_make_components`,
10
+ and everything else importing from here) keep working unchanged.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from adl_drift.lang.base import LanguageParser, ParseResult
16
+ from adl_drift.lang.python.parser import PYTHON_LANGUAGE, QUERIES
17
+
18
+ __all__ = ["ParseResult", "PythonParser", "PYTHON_LANGUAGE", "QUERIES"]
19
+
20
+
21
+ class PythonParser(LanguageParser):
22
+ """Python-configured `LanguageParser`.
23
+
24
+ Kept as a subclass (rather than a bare factory function) so direct
25
+ `PythonParser()` construction keeps working for every existing caller.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ if PYTHON_LANGUAGE is None:
30
+ raise RuntimeError(
31
+ "tree-sitter-python not installed. pip install tree-sitter tree-sitter-python"
32
+ )
33
+ super().__init__(PYTHON_LANGUAGE, QUERIES)
@@ -0,0 +1,17 @@
1
+ """Symbol table extractor — walks CST and produces Symbol records.
2
+
3
+ Moved to `lang/python/extractor.py` (P4.2, PROBLEMS.md) as part of
4
+ generalizing symbol extraction behind the `LanguageBackend` contract: the
5
+ shared parse/dedupe/`AnalysisResult`-assembly plumbing now lives in
6
+ `lang/base.py::SymbolExtractor`, and the Python-specific CST walk lives in
7
+ `lang/python/extractor.py`. This module re-exports the Python-configured
8
+ extractor under its original name so existing call sites (`DriftCommands`,
9
+ `MCPHandlers`, `scanner/parallel.py::_make_components`, and everything else
10
+ importing from here) keep working unchanged.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from adl_drift.lang.python.extractor import SymbolExtractor
16
+
17
+ __all__ = ["SymbolExtractor"]