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,345 @@
1
+ """Core dataclasses shared across the ADL Drift pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum, auto
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+
12
+ # Exceptions a single bad file may raise during parse/extract/detect (P2.5).
13
+ # Scanners catch these per file and report them; anything else is a bug in
14
+ # Drift itself and should propagate.
15
+ SCAN_FILE_ERRORS = (UnicodeDecodeError, OSError, ValueError, RuntimeError)
16
+
17
+ # P1.10 step 2: scope-chain segment inserted after every *function* segment,
18
+ # mirroring CPython's __qualname__ semantics. Separates a class's method
19
+ # (`m.Foo.bar`) from a closure inside a same-named function
20
+ # (`m.Foo.<locals>.bar`).
21
+ LOCALS_MARKER = "<locals>"
22
+
23
+
24
+ class PatternCategory(Enum):
25
+ ROUTES = auto()
26
+ ERRORS = auto()
27
+ DATA_ACCESS = auto()
28
+ DECORATORS = auto()
29
+ ASYNC = auto()
30
+ IMPORTS = auto()
31
+ SECURITY = auto()
32
+ TESTS = auto()
33
+ CONFIG = auto()
34
+ LOGGING = auto()
35
+ TYPES = auto()
36
+ DYNAMIC = auto() # P2.12: dynamic dispatch / reflection patterns
37
+ TAINT = auto() # P2.19: source→sink taint paths (confirmed injection)
38
+
39
+
40
+ class ConfidenceLevel(float, Enum):
41
+ LOW = 0.3
42
+ MEDIUM = 0.6
43
+ HIGH = 0.9
44
+
45
+
46
+ @dataclass
47
+ class SourcePosition:
48
+ line: int
49
+ column: int
50
+ end_line: int = 0
51
+ end_column: int = 0
52
+
53
+ def to_lsp_range(self) -> Dict[str, Dict[str, int]]:
54
+ return {
55
+ "start": {"line": max(self.line - 1, 0), "character": self.column},
56
+ "end": {"line": max(self.end_line - 1, 0), "character": self.end_column},
57
+ }
58
+
59
+ def to_dict(self) -> Dict[str, Any]:
60
+ return {
61
+ "line": self.line,
62
+ "column": self.column,
63
+ "end_line": self.end_line,
64
+ "end_column": self.end_column,
65
+ }
66
+
67
+ @classmethod
68
+ def from_dict(cls, d: Dict[str, Any]) -> "SourcePosition":
69
+ return cls(
70
+ line=d["line"],
71
+ column=d["column"],
72
+ end_line=d.get("end_line", 0),
73
+ end_column=d.get("end_column", 0),
74
+ )
75
+
76
+
77
+ @dataclass
78
+ class Symbol:
79
+ name: str
80
+ kind: str # "function", "class", "method", "variable"
81
+ file_path: Path
82
+ position: SourcePosition
83
+ decorators: List[str] = field(default_factory=list)
84
+ docstring: Optional[str] = None
85
+ signature: Optional[str] = None
86
+ is_async: bool = False
87
+ parent: Optional[str] = None # qualified name of enclosing class
88
+ superclasses: List[str] = field(default_factory=list) # raw base class names for classes
89
+ param_types: Dict[str, str] = field(default_factory=dict) # param name -> annotation text
90
+ return_type: Optional[str] = None # function return annotation text
91
+ is_test: bool = False # pytest/unittest test function or fixture
92
+ metadata: Dict[str, Any] = field(default_factory=dict) # arbitrary per-symbol tags (e.g. synthetic=exec)
93
+ # P2.17/P1.10: full lexical scope chain — all enclosing class/function names,
94
+ # outermost first, with LOCALS_MARKER ("<locals>") inserted after every
95
+ # *function* segment. Mirrors CPython's __qualname__ minus the module prefix.
96
+ # e.g. for `def outer(): def inner():`, inner.scope_chain = ["outer", "<locals>"]
97
+ # e.g. for `class Foo: def bar(): def inner():`,
98
+ # inner.scope_chain = ["Foo", "bar", "<locals>"]
99
+ scope_chain: List[str] = field(default_factory=list)
100
+ implements: List[str] = field(default_factory=list) # raw interface names for classes
101
+ throws: List[str] = field(default_factory=list) # declared exception types for callables
102
+ modifiers: List[str] = field(default_factory=list) # language declaration modifiers
103
+ signature_key: Optional[str] = None # overload discriminator, e.g. "(String,int)"
104
+
105
+ @property
106
+ def qualified_name(self) -> str:
107
+ return f"{self.parent}.{self.name}" if self.parent else self.name
108
+
109
+ def to_dict(self) -> Dict[str, Any]:
110
+ return {
111
+ "name": self.name,
112
+ "kind": self.kind,
113
+ "file_path": str(self.file_path),
114
+ "position": self.position.to_dict(),
115
+ "decorators": self.decorators,
116
+ "docstring": self.docstring,
117
+ "signature": self.signature,
118
+ "is_async": self.is_async,
119
+ "parent": self.parent,
120
+ "superclasses": self.superclasses,
121
+ "implements": self.implements,
122
+ "throws": self.throws,
123
+ "modifiers": self.modifiers,
124
+ "signature_key": self.signature_key,
125
+ "param_types": self.param_types,
126
+ "return_type": self.return_type,
127
+ "is_test": self.is_test,
128
+ "metadata": self.metadata,
129
+ "scope_chain": self.scope_chain,
130
+ }
131
+
132
+ @classmethod
133
+ def from_dict(cls, d: Dict[str, Any]) -> "Symbol":
134
+ return cls(
135
+ name=d["name"],
136
+ kind=d["kind"],
137
+ file_path=Path(d["file_path"]),
138
+ position=SourcePosition.from_dict(d["position"]),
139
+ decorators=d.get("decorators", []),
140
+ docstring=d.get("docstring"),
141
+ signature=d.get("signature"),
142
+ is_async=d.get("is_async", False),
143
+ parent=d.get("parent"),
144
+ superclasses=d.get("superclasses", []),
145
+ implements=d.get("implements", []),
146
+ throws=d.get("throws", []),
147
+ modifiers=d.get("modifiers", []),
148
+ signature_key=d.get("signature_key"),
149
+ param_types=d.get("param_types", {}),
150
+ return_type=d.get("return_type"),
151
+ is_test=d.get("is_test", False),
152
+ metadata=d.get("metadata", {}),
153
+ scope_chain=d.get("scope_chain", []),
154
+ )
155
+
156
+
157
+ @dataclass
158
+ class ImportRecord:
159
+ """Structured import binding (P2.1). One record per imported name.
160
+
161
+ Examples:
162
+ import a.b → module="a.b", name=None (binds "a")
163
+ import a.b as c → module="a.b", name=None, alias="c"
164
+ from x.y import z → module="x.y", name="z"
165
+ from x import z as q → module="x", name="z", alias="q"
166
+ from . import m → module="", name="m", level=1
167
+ from ..utils import h → module="utils", name="h", level=2
168
+ from x import * → module="x", name="*", is_wildcard=True
169
+ """
170
+ module: str
171
+ name: Optional[str] = None
172
+ alias: Optional[str] = None
173
+ level: int = 0 # number of leading dots for relative imports
174
+ is_wildcard: bool = False
175
+ is_static: bool = False
176
+
177
+ @property
178
+ def binding(self) -> Optional[str]:
179
+ """Local name this import introduces (None for wildcard)."""
180
+ if self.is_wildcard:
181
+ return None
182
+ if self.alias:
183
+ return self.alias
184
+ if self.name:
185
+ return self.name
186
+ # plain `import a.b` binds the leftmost segment
187
+ return self.module.split(".")[0] if self.module else None
188
+
189
+ def to_dict(self) -> Dict[str, Any]:
190
+ return {
191
+ "module": self.module,
192
+ "name": self.name,
193
+ "alias": self.alias,
194
+ "level": self.level,
195
+ "is_wildcard": self.is_wildcard,
196
+ "is_static": self.is_static,
197
+ }
198
+
199
+ @classmethod
200
+ def from_dict(cls, d: Dict[str, Any]) -> "ImportRecord":
201
+ return cls(
202
+ module=d.get("module", ""),
203
+ name=d.get("name"),
204
+ alias=d.get("alias"),
205
+ level=d.get("level", 0),
206
+ is_wildcard=d.get("is_wildcard", False),
207
+ is_static=d.get("is_static", False),
208
+ )
209
+
210
+
211
+ @dataclass
212
+ class Pattern:
213
+ category: PatternCategory
214
+ name: str
215
+ description: str
216
+ position: SourcePosition
217
+ file_path: Path
218
+ confidence: float = 0.5
219
+ symbol: Optional[Symbol] = None
220
+ metadata: Dict[str, Any] = field(default_factory=dict)
221
+ is_approved: bool = False
222
+
223
+ # Metadata keys that name the pattern's symbolic object, in priority
224
+ # order (same list the CLI scan table's Object column resolves from).
225
+ _HASH_OBJECT_KEYS = (
226
+ "handler", "function", "decorator", "name", "class",
227
+ "key", "key_name", "route_path", "module", "type", "role",
228
+ )
229
+
230
+ def compute_hash(self) -> str:
231
+ """Structural identity hash (P2.4).
232
+
233
+ Identity is `category:name:anchor` where the anchor is the symbolic
234
+ object the pattern is about — the enclosing symbol (`Class.method`)
235
+ when present, else the most specific metadata object, else the
236
+ filename stem. Line numbers and directory paths never participate,
237
+ so a pattern keeps its identity (and approval status) when code
238
+ moves to a different line or the file is relocated.
239
+ """
240
+ if self.symbol is not None:
241
+ anchor = (
242
+ f"{self.symbol.parent}.{self.symbol.name}"
243
+ if self.symbol.parent else self.symbol.name
244
+ )
245
+ else:
246
+ anchor = ""
247
+ for k in self._HASH_OBJECT_KEYS:
248
+ v = self.metadata.get(k)
249
+ if v:
250
+ anchor = str(v)
251
+ break
252
+ if not anchor:
253
+ anchor = Path(self.file_path).stem
254
+ key = f"{self.category.name}:{self.name}:{anchor}"
255
+ return hashlib.sha256(key.encode()).hexdigest()[:16]
256
+
257
+ def to_dict(self) -> Dict[str, Any]:
258
+ return {
259
+ "category": self.category.name,
260
+ "name": self.name,
261
+ "description": self.description,
262
+ "position": self.position.to_dict(),
263
+ "file_path": str(self.file_path),
264
+ "confidence": self.confidence,
265
+ "metadata": self.metadata,
266
+ "is_approved": self.is_approved,
267
+ }
268
+
269
+ @classmethod
270
+ def from_dict(cls, d: Dict[str, Any]) -> "Pattern":
271
+ return cls(
272
+ category=PatternCategory[d["category"]],
273
+ name=d["name"],
274
+ description=d["description"],
275
+ position=SourcePosition.from_dict(d["position"]),
276
+ file_path=Path(d["file_path"]),
277
+ confidence=d.get("confidence", 0.5),
278
+ metadata=d.get("metadata", {}),
279
+ is_approved=d.get("is_approved", False),
280
+ )
281
+
282
+
283
+ @dataclass
284
+ class AnalysisResult:
285
+ file_path: Path
286
+ symbols: List[Symbol] = field(default_factory=list)
287
+ patterns: List[Pattern] = field(default_factory=list)
288
+ imports: List[str] = field(default_factory=list)
289
+ import_records: List[ImportRecord] = field(default_factory=list)
290
+ errors: List[str] = field(default_factory=list)
291
+ parse_time_ms: float = 0.0
292
+ language: str = "python"
293
+ module_name: Optional[str] = None
294
+
295
+ def to_dict(self) -> Dict[str, Any]:
296
+ return {
297
+ "file_path": str(self.file_path),
298
+ "symbols": [s.to_dict() for s in self.symbols],
299
+ "patterns": [p.to_dict() for p in self.patterns],
300
+ "imports": self.imports,
301
+ "import_records": [r.to_dict() for r in self.import_records],
302
+ "errors": self.errors,
303
+ "parse_time_ms": self.parse_time_ms,
304
+ "language": self.language,
305
+ "module_name": self.module_name,
306
+ }
307
+
308
+ @classmethod
309
+ def from_dict(cls, d: Dict[str, Any]) -> "AnalysisResult":
310
+ return cls(
311
+ file_path=Path(d["file_path"]),
312
+ symbols=[Symbol.from_dict(s) for s in d.get("symbols", [])],
313
+ patterns=[Pattern.from_dict(p) for p in d.get("patterns", [])],
314
+ imports=d.get("imports", []),
315
+ import_records=[ImportRecord.from_dict(r) for r in d.get("import_records", [])],
316
+ errors=d.get("errors", []),
317
+ parse_time_ms=d.get("parse_time_ms", 0.0),
318
+ language=d.get("language", "python"),
319
+ module_name=d.get("module_name"),
320
+ )
321
+
322
+
323
+ @dataclass
324
+ class ProjectContext:
325
+ root_path: Path
326
+ files_analyzed: int = 0
327
+ total_symbols: int = 0
328
+ total_patterns: int = 0
329
+ frameworks: List[str] = field(default_factory=list)
330
+ naming_convention: Optional[str] = None
331
+ # Project-size baseline metrics (P3.15 follow-up).
332
+ lines_of_code: int = 0 # total physical lines across scanned files
333
+ non_blank_lines: int = 0 # non-empty / non-whitespace lines
334
+ total_functions: int = 0
335
+ total_classes: int = 0
336
+ total_variables: int = 0
337
+ total_modules: int = 0
338
+ entry_points: int = 0
339
+ calls_edges: int = 0
340
+ patterns_by_category: Dict[str, int] = field(default_factory=dict)
341
+ # Files that exist on disk but were correctly out of scope for this
342
+ # scan (a Maven module not in the active reactor, a resources
343
+ # directory), each `{"path", "reason", "detail"}` — see
344
+ # `JavaProjectConfig.excluded_source_files`. Empty for non-Java projects.
345
+ excluded_files: List[Dict[str, Any]] = field(default_factory=list)
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { homedir, constants } from 'node:os';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ const args = process.argv.slice(2);
9
+ let projectArgument;
10
+ for (let i = 0; i < args.length; i++) {
11
+ if (args[i] === '-p' || args[i] === '--project') projectArgument = args[++i];
12
+ else if (args[i].startsWith('--project=')) projectArgument = args[i].slice('--project='.length);
13
+ }
14
+ const expand = path => path === '~' ? homedir() : path.startsWith('~/') || path.startsWith('~\\') ? join(homedir(), path.slice(2)) : path;
15
+ let project = resolve(expand(projectArgument || process.cwd()));
16
+ const executableFor = path => join(path, '.adl-toolkit', 'venv', process.platform === 'win32' ? 'Scripts' : 'bin', process.platform === 'win32' ? 'adl-drift.exe' : 'adl-drift');
17
+ while (!existsSync(executableFor(project)) && !projectArgument && dirname(project) !== project) project = dirname(project);
18
+ const executable = executableFor(project);
19
+ if (!existsSync(executable)) {
20
+ console.error('No managed ADL-Drift runtime found. Run adl-toolkit install in this project first.');
21
+ process.exitCode = 1;
22
+ } else {
23
+ const child = spawn(executable, projectArgument ? args : ['-p', project, ...args], { stdio: 'inherit' });
24
+ const interrupt = () => child.kill('SIGINT');
25
+ process.on('SIGINT', interrupt);
26
+ child.on('error', error => {
27
+ console.error(`Cannot run ADL-Drift: ${error.message}`);
28
+ process.exitCode = 1;
29
+ process.off('SIGINT', interrupt);
30
+ });
31
+ child.on('close', (code, signal) => {
32
+ process.off('SIGINT', interrupt);
33
+ process.exitCode = signal ? 128 + (constants.signals[signal] || 0) : code ?? 1;
34
+ });
35
+ }