witan-code 0.2.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.
- witan_code/__init__.py +0 -0
- witan_code/__main__.py +4 -0
- witan_code/_detach.py +27 -0
- witan_code/bridge.py +392 -0
- witan_code/bridge_extractors.py +642 -0
- witan_code/cli.py +722 -0
- witan_code/config.py +65 -0
- witan_code/context.py +98 -0
- witan_code/edges.py +171 -0
- witan_code/elicit.py +92 -0
- witan_code/extensions/pi/codegraph.ts +121 -0
- witan_code/graph.py +271 -0
- witan_code/hooks.py +116 -0
- witan_code/indexer.py +1042 -0
- witan_code/maintenance.py +130 -0
- witan_code/package_map.py +74 -0
- witan_code/queries/bridge.gq +193 -0
- witan_code/queries/code_mutations.gq +81 -0
- witan_code/queries/code_read.gq +154 -0
- witan_code/queries/delete.gq +16 -0
- witan_code/queries_ts/bash.scm +9 -0
- witan_code/queries_ts/hcl.scm +16 -0
- witan_code/queries_ts/python.scm +29 -0
- witan_code/queries_ts/sql.scm +20 -0
- witan_code/queries_ts/typescript.scm +51 -0
- witan_code/queries_ts/yaml.scm +5 -0
- witan_code/repo.py +282 -0
- witan_code/schema/bridge-schema.pg +82 -0
- witan_code/schema/code-schema.pg +51 -0
- witan_code/server.py +859 -0
- witan_code/setup.py +230 -0
- witan_code/skills/witan-code/SKILL.md +97 -0
- witan_code/stitch.py +177 -0
- witan_code/store.py +116 -0
- witan_code/visualize.py +347 -0
- witan_code-0.2.0.dist-info/METADATA +476 -0
- witan_code-0.2.0.dist-info/RECORD +39 -0
- witan_code-0.2.0.dist-info/WHEEL +4 -0
- witan_code-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"""Cross-repo interface-binding extractors for the Layer-2.5 bridge.
|
|
2
|
+
|
|
3
|
+
A *binding* is a point where a repo PROVIDES or CONSUMES a shared contract that
|
|
4
|
+
crosses repo boundaries: an environment variable, a shared package, a service /
|
|
5
|
+
deploy unit, or an HTTP endpoint. Bindings from every indexed repo accumulate in
|
|
6
|
+
one shared bridge store; cross-repo linkages are found by grouping on
|
|
7
|
+
``(kind, key_norm)``.
|
|
8
|
+
|
|
9
|
+
Extraction is HEURISTIC and syntactic — regex over source text for a handful of
|
|
10
|
+
well-formed, high-signal patterns, matching the same best-effort ethos as the
|
|
11
|
+
symbol indexer. It will miss dynamic construction and occasionally over-match.
|
|
12
|
+
|
|
13
|
+
Two tiers:
|
|
14
|
+
* Tier A (``extract_file_bindings``) — per source file; rides the per-file walk
|
|
15
|
+
the indexer already does. Env-var + package *consumers*, endpoint consumers.
|
|
16
|
+
* Tier B (``extract_repo_bindings``) — once per repo over well-known files that
|
|
17
|
+
are not otherwise indexed: OpenAPI specs (endpoint providers), Pulumi stack
|
|
18
|
+
configs (env-var providers), service definitions, package.json (package
|
|
19
|
+
providers).
|
|
20
|
+
|
|
21
|
+
Each consumer ``ParsedBinding`` carries a ``confidence`` score (0.0–1.0) computed
|
|
22
|
+
by stacking independent heuristics at extraction time. Higher scores indicate
|
|
23
|
+
more likely genuine cross-repo calls; callers can filter on ``min_confidence`` to
|
|
24
|
+
suppress phantom edges without hardcoding library names.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# ── Binding record ────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ParsedBinding:
|
|
37
|
+
kind: str # env_var | package | service | endpoint
|
|
38
|
+
key: str # raw as written (METHOD path, pkg name, env NAME)
|
|
39
|
+
key_norm: str # normalized join key
|
|
40
|
+
role: str # provider | consumer | shared
|
|
41
|
+
file: str # repo-relative source path
|
|
42
|
+
sub_kind: str | None = None # service anchor variant: repo | image | name
|
|
43
|
+
symbol_id: str | None = None # filled by the indexer (Tier A) via line lookup
|
|
44
|
+
line: int | None = None
|
|
45
|
+
language: str | None = None
|
|
46
|
+
framework: str | None = None
|
|
47
|
+
generic: bool = False
|
|
48
|
+
# Confidence that this binding represents a genuine cross-repo call (0.0–1.0).
|
|
49
|
+
# 1.0 for provider bindings (always genuine); computed for consumer endpoint
|
|
50
|
+
# bindings by _compute_file_confidence. Defaults to 1.0 so non-endpoint
|
|
51
|
+
# consumers and providers pass through unfiltered.
|
|
52
|
+
confidence: float = 1.0
|
|
53
|
+
# Canonical symbol string (docs/SYMBOL_FORMAT.md), filled at bridge-write
|
|
54
|
+
# time by canonical_symbol() once the repo's package identity is known.
|
|
55
|
+
symbol: str | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Generic env names that appear in unrelated repos — extracted but flagged so
|
|
59
|
+
# cross-repo impact can de-prioritize them (a `DEBUG` edit doesn't "touch" 40 repos).
|
|
60
|
+
GENERIC_ENV = frozenset(
|
|
61
|
+
{
|
|
62
|
+
"DEBUG",
|
|
63
|
+
"PORT",
|
|
64
|
+
"HOST",
|
|
65
|
+
"HOSTNAME",
|
|
66
|
+
"PATH",
|
|
67
|
+
"HOME",
|
|
68
|
+
"USER",
|
|
69
|
+
"ENV",
|
|
70
|
+
"ENVIRONMENT",
|
|
71
|
+
"LOG_LEVEL",
|
|
72
|
+
"LOGLEVEL",
|
|
73
|
+
"TZ",
|
|
74
|
+
"LANG",
|
|
75
|
+
"SECRET_KEY",
|
|
76
|
+
"NODE_ENV",
|
|
77
|
+
"PYTHONPATH",
|
|
78
|
+
"TMPDIR",
|
|
79
|
+
"PWD",
|
|
80
|
+
"SHELL",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── Key normalization ─────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def normalize_endpoint(path: str) -> str:
|
|
89
|
+
"""Collapse path params so producer and consumer URLs join.
|
|
90
|
+
|
|
91
|
+
``/api/v1/articles/{id}/`` · `` `/api/v1/articles/${x}/` `` · ``:id`` all
|
|
92
|
+
normalize to ``/api/v1/articles/{}``. Strips one trailing slash.
|
|
93
|
+
"""
|
|
94
|
+
path = path.strip().strip("`\"'")
|
|
95
|
+
# Drop a leading scheme://host so consumer absolute URLs match relative paths.
|
|
96
|
+
path = re.sub(r"^[a-z]+://[^/]+", "", path)
|
|
97
|
+
path = re.sub(r"\$\{[^}]*\}|\{[^}]*\}|:[^/]+", "{}", path)
|
|
98
|
+
path = re.sub(r"/+", "/", path)
|
|
99
|
+
return path.rstrip("/") or "/"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def normalize_key(kind: str, key: str) -> str:
|
|
103
|
+
if kind == "endpoint":
|
|
104
|
+
# key may be "METHOD /path"; normalize only the path part.
|
|
105
|
+
method, _, rest = key.partition(" ")
|
|
106
|
+
if rest:
|
|
107
|
+
return normalize_endpoint(rest)
|
|
108
|
+
return normalize_endpoint(key)
|
|
109
|
+
return key.strip()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Canonical symbol strings (docs/SYMBOL_FORMAT.md) ──────────────
|
|
113
|
+
#
|
|
114
|
+
# {scheme}:{manager}:{package}:{version}:{descriptor} — five colon-separated
|
|
115
|
+
# fields; descriptor is terminal (parse with maxsplit=4). "." = empty/unknown.
|
|
116
|
+
# Providers are qualified with the repo's PackageIdentity; consumers emit
|
|
117
|
+
# unresolved external symbols (".") except package imports, which name their
|
|
118
|
+
# package at the reference site.
|
|
119
|
+
|
|
120
|
+
_SYMBOL_SCHEME = {
|
|
121
|
+
"endpoint": "http",
|
|
122
|
+
"env_var": "env",
|
|
123
|
+
"package": "pkg",
|
|
124
|
+
"service": "svc",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_MANAGER_BY_FRAMEWORK = {"npm": "npm", "python": "pypi"}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _esc(field: str) -> str:
|
|
131
|
+
return field.replace("%", "%25").replace(":", "%3A")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def canonical_symbol(binding: ParsedBinding, identity=None) -> str:
|
|
135
|
+
"""Build the canonical symbol string for ``binding``.
|
|
136
|
+
|
|
137
|
+
``identity`` is the repo's ``package_map.PackageIdentity``; only provider
|
|
138
|
+
bindings use it (consumers stay unresolved by design).
|
|
139
|
+
"""
|
|
140
|
+
scheme = _SYMBOL_SCHEME.get(binding.kind, binding.kind)
|
|
141
|
+
provider = binding.role == "provider"
|
|
142
|
+
|
|
143
|
+
if binding.kind == "package":
|
|
144
|
+
manager = _MANAGER_BY_FRAMEWORK.get(binding.framework or "", ".")
|
|
145
|
+
package = binding.key_norm
|
|
146
|
+
version = identity.version if provider and identity else "."
|
|
147
|
+
descriptor = "."
|
|
148
|
+
else:
|
|
149
|
+
manager = identity.manager if provider and identity else "."
|
|
150
|
+
package = identity.name if provider and identity else "."
|
|
151
|
+
version = identity.version if provider and identity else "."
|
|
152
|
+
if binding.kind == "endpoint":
|
|
153
|
+
method, _, rest = binding.key.strip().partition(" ")
|
|
154
|
+
method = method.upper() if rest else "*"
|
|
155
|
+
descriptor = f"{method} {binding.key_norm}"
|
|
156
|
+
elif binding.kind == "service":
|
|
157
|
+
descriptor = f"{binding.sub_kind or '.'}/{binding.key_norm}"
|
|
158
|
+
else:
|
|
159
|
+
descriptor = binding.key_norm
|
|
160
|
+
|
|
161
|
+
prefix = ":".join(_esc(f) for f in (scheme, manager, package, version))
|
|
162
|
+
return f"{prefix}:{descriptor}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class ParsedSymbol:
|
|
167
|
+
scheme: str
|
|
168
|
+
manager: str
|
|
169
|
+
package: str
|
|
170
|
+
version: str
|
|
171
|
+
descriptor: str
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _unesc(field: str) -> str:
|
|
175
|
+
# Inverse of _esc: %3A first, %25 last (so an escaped "%253A" round-trips
|
|
176
|
+
# to the literal "%3A" instead of ":").
|
|
177
|
+
return field.replace("%3A", ":").replace("%25", "%")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_symbol(symbol: str) -> ParsedSymbol | None:
|
|
181
|
+
"""Split a canonical symbol string into its five fields.
|
|
182
|
+
|
|
183
|
+
The descriptor is terminal and may contain colons, so split with
|
|
184
|
+
``maxsplit=4``; only the first four fields are percent-decoded
|
|
185
|
+
(descriptors are never encoded). Returns None on a malformed string.
|
|
186
|
+
"""
|
|
187
|
+
parts = symbol.split(":", 4)
|
|
188
|
+
if len(parts) != 5:
|
|
189
|
+
return None
|
|
190
|
+
scheme, manager, package, version = (_unesc(p) for p in parts[:4])
|
|
191
|
+
return ParsedSymbol(scheme, manager, package, version, parts[4])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _binding(kind, key, role, file, *, sub_kind=None, **kw) -> ParsedBinding:
|
|
195
|
+
key_norm = normalize_key(kind, key)
|
|
196
|
+
generic = kind == "env_var" and key in GENERIC_ENV
|
|
197
|
+
return ParsedBinding(
|
|
198
|
+
kind=kind,
|
|
199
|
+
key=key,
|
|
200
|
+
key_norm=key_norm,
|
|
201
|
+
role=role,
|
|
202
|
+
file=file,
|
|
203
|
+
sub_kind=sub_kind,
|
|
204
|
+
generic=generic,
|
|
205
|
+
**kw,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── Tier A: per-file consumers (regex over source text) ───────────
|
|
210
|
+
|
|
211
|
+
_PY_ENV = (
|
|
212
|
+
# get_string("NAME", ...) and friends from the settings-helper convention.
|
|
213
|
+
re.compile(
|
|
214
|
+
r"\bget_(?:string|bool|int|float|list_of_str)\s*\(\s*[\"']([A-Z][A-Z0-9_]*[A-Z0-9])[\"']"
|
|
215
|
+
),
|
|
216
|
+
# os.getenv("NAME") / os.environ.get("NAME")
|
|
217
|
+
re.compile(
|
|
218
|
+
r"\bos\.(?:getenv|environ\.get)\s*\(\s*[\"']([A-Z][A-Z0-9_]*[A-Z0-9])[\"']"
|
|
219
|
+
),
|
|
220
|
+
# os.environ["NAME"]
|
|
221
|
+
re.compile(r"\bos\.environ\s*\[\s*[\"']([A-Z][A-Z0-9_]*[A-Z0-9])[\"']"),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
_TS_ENV = (
|
|
225
|
+
# process.env.NAME / process.env["NAME"]
|
|
226
|
+
re.compile(
|
|
227
|
+
r"\bprocess\.env(?:\.([A-Z][A-Z0-9_]*[A-Z0-9])|\[\s*[\"']([A-Z][A-Z0-9_]*[A-Z0-9])[\"'])"
|
|
228
|
+
),
|
|
229
|
+
# env("NAME") runtime accessor
|
|
230
|
+
re.compile(r"\benv\s*\(\s*[\"']([A-Z][A-Z0-9_]*[A-Z0-9])[\"']"),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Shared internal packages.
|
|
234
|
+
_PY_PKG = re.compile(
|
|
235
|
+
r"\b(?:from|import)\s+(mitol\.[a-z0-9_]+)|include\(\s*[\"'](mitol\.[a-z0-9_]+)"
|
|
236
|
+
)
|
|
237
|
+
_TS_PKG = re.compile(r"(?:from|require\()\s*[\"'](@mitodl/[A-Za-z0-9._-]+)")
|
|
238
|
+
|
|
239
|
+
# Endpoint consumer path literals (string/template literals that look like API
|
|
240
|
+
# paths). Conservative: the value must start with "/" and contain an "api/"
|
|
241
|
+
# segment, to keep noise down. Matches both "/api/v1/x" and "/foo/api/x".
|
|
242
|
+
_TS_ENDPOINT = re.compile(r"""[\"'`](/[\w./${}:()@-]*?api/[\w./${}:()@-]*)[\"'`]""")
|
|
243
|
+
|
|
244
|
+
_PY_FRAMEWORK = "django"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ── Confidence scoring ────────────────────────────────────────────
|
|
248
|
+
#
|
|
249
|
+
# Score starts at 0.5 (neutral). Independent heuristics add or subtract deltas.
|
|
250
|
+
# Final score is clamped to [0.0, 1.0]. Only endpoint consumer bindings are
|
|
251
|
+
# scored; other kinds default to confidence=1.0.
|
|
252
|
+
#
|
|
253
|
+
# File-level heuristics (evaluated at extraction time, no bridge-store access):
|
|
254
|
+
# generated_file -0.8 @generated / DO NOT EDIT / openapi-generator headers
|
|
255
|
+
# load_test_path -0.6 path segment is load_testing, benchmark, perf, k6, locust
|
|
256
|
+
# load_test_framework -0.6 imports k6, locust, artillery, gatling (structural pattern)
|
|
257
|
+
# relative_url -0.5 extracted URL has no scheme (no http:// or https://)
|
|
258
|
+
# same_origin_helper -0.3 relative import (./...) to csrf/fetch/api-client file
|
|
259
|
+
# explicit_hostname +0.3 extracted URL has scheme + host (absolute URL)
|
|
260
|
+
# client_directory +0.2 path segment is clients, integrations, services, api-client
|
|
261
|
+
#
|
|
262
|
+
# Store-level heuristics (applied later in bridge.py by adjust_confidence).
|
|
263
|
+
# The cross-repo half of each comes from OTHER repos' Stage 1 symbol table
|
|
264
|
+
# (RepoSymbol `exported` rows, docs/SYMBOL_TABLE.md) — the deduplicated
|
|
265
|
+
# artifact those repos' own writes already produced, not a re-derivation
|
|
266
|
+
# from raw bindings:
|
|
267
|
+
# self_provided_key -0.5 consumer repo also provides same key_norm
|
|
268
|
+
# known_provider_pkg +0.3 imported package matches a provider package slug from
|
|
269
|
+
# a different repo's Stage 1 symbol table
|
|
270
|
+
|
|
271
|
+
_GENERATED_HEADERS = re.compile(
|
|
272
|
+
r"@generated|Code generated by|DO NOT EDIT|openapi-generator",
|
|
273
|
+
re.IGNORECASE,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
_LOAD_TEST_PATH_SEGMENTS = frozenset(
|
|
277
|
+
["load_testing", "benchmark", "perf", "k6", "locust"]
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Load-test framework import patterns (name-agnostic structural detection).
|
|
281
|
+
# Matches: import ... from 'k6', import ... from 'k6/http', require('locust'),
|
|
282
|
+
# import { ... } from 'artillery', etc.
|
|
283
|
+
_LOAD_TEST_FRAMEWORK = re.compile(
|
|
284
|
+
r"""(?:from|require\()\s*[\"'](?:k6(?:/[^\"']*)?|locust|artillery|gatling)[\"']"""
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
_HAS_SCHEME = re.compile(r"^[a-z][a-z0-9+\-.]*://")
|
|
288
|
+
|
|
289
|
+
# Same-origin helper: relative import (starts with ./ or ../) to a file whose
|
|
290
|
+
# basename suggests a CSRF/fetch/api-client wrapper.
|
|
291
|
+
_SAME_ORIGIN_RELATIVE = re.compile(
|
|
292
|
+
r"""(?:from|require\()\s*[\"'](\.\.?/[^\"']*?(?:csrf|fetch|api[-_]?client|http[-_]?client)[^\"']*)[\"']""",
|
|
293
|
+
re.IGNORECASE,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
_CLIENT_PATH_SEGMENTS = re.compile(
|
|
297
|
+
r"(?:^|/)(?:clients|integrations|services|api[-_]client)(?:/|$)"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _compute_file_confidence(file: str, content: str, url: str) -> float:
|
|
302
|
+
"""Score an endpoint consumer binding using file-level heuristics only.
|
|
303
|
+
|
|
304
|
+
Returns a value in [0.0, 1.0]. Store-dependent adjustments
|
|
305
|
+
(self_provided_key, known_provider_package) are applied later by
|
|
306
|
+
``adjust_confidence`` in bridge.py after the bridge store is queried.
|
|
307
|
+
"""
|
|
308
|
+
score = 0.5
|
|
309
|
+
|
|
310
|
+
# Penalties
|
|
311
|
+
if _GENERATED_HEADERS.search(content):
|
|
312
|
+
score -= 0.8
|
|
313
|
+
|
|
314
|
+
parts = set(file.replace("\\", "/").split("/"))
|
|
315
|
+
if parts & _LOAD_TEST_PATH_SEGMENTS:
|
|
316
|
+
score -= 0.6
|
|
317
|
+
|
|
318
|
+
if _LOAD_TEST_FRAMEWORK.search(content):
|
|
319
|
+
score -= 0.6
|
|
320
|
+
|
|
321
|
+
if not _HAS_SCHEME.match(url):
|
|
322
|
+
score -= 0.5
|
|
323
|
+
|
|
324
|
+
if _SAME_ORIGIN_RELATIVE.search(content):
|
|
325
|
+
score -= 0.3
|
|
326
|
+
|
|
327
|
+
# Boosts
|
|
328
|
+
if _HAS_SCHEME.match(url):
|
|
329
|
+
score += 0.3
|
|
330
|
+
|
|
331
|
+
file_lower = "/" + file.lower().replace("\\", "/")
|
|
332
|
+
if _CLIENT_PATH_SEGMENTS.search(file_lower):
|
|
333
|
+
score += 0.2
|
|
334
|
+
|
|
335
|
+
return max(0.0, min(1.0, score))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def adjust_confidence(
|
|
339
|
+
binding: "ParsedBinding",
|
|
340
|
+
*,
|
|
341
|
+
consumer_repo: str,
|
|
342
|
+
provider_keys: "frozenset[tuple[str, str]]",
|
|
343
|
+
has_known_provider_package: bool = False,
|
|
344
|
+
) -> "ParsedBinding":
|
|
345
|
+
"""Apply store-dependent confidence adjustments in-place and return the binding.
|
|
346
|
+
|
|
347
|
+
Only adjusts endpoint consumer bindings; all others are returned untouched.
|
|
348
|
+
|
|
349
|
+
``provider_keys`` — set of ``(repo, key_norm)`` pairs for ALL
|
|
350
|
+
provider records, including rows where
|
|
351
|
+
repo==consumer_repo (so the self_provided_key
|
|
352
|
+
penalty fires when the consumer repo also
|
|
353
|
+
provides the same key_norm).
|
|
354
|
+
``has_known_provider_package``— True when the consumer file is known to import
|
|
355
|
+
a package tracked as a provider in a different
|
|
356
|
+
repo in the bridge store (derived from
|
|
357
|
+
co-located package consumer bindings in the
|
|
358
|
+
same file).
|
|
359
|
+
"""
|
|
360
|
+
if binding.kind != "endpoint" or binding.role != "consumer":
|
|
361
|
+
return binding
|
|
362
|
+
|
|
363
|
+
score = binding.confidence
|
|
364
|
+
|
|
365
|
+
if (consumer_repo, binding.key_norm) in provider_keys:
|
|
366
|
+
score -= 0.5
|
|
367
|
+
|
|
368
|
+
if has_known_provider_package:
|
|
369
|
+
score += 0.3
|
|
370
|
+
|
|
371
|
+
binding.confidence = max(0.0, min(1.0, score))
|
|
372
|
+
return binding
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def extract_file_bindings(text: str, language: str, file: str) -> list[ParsedBinding]:
|
|
376
|
+
"""Tier A — consumer bindings from one source file's text.
|
|
377
|
+
|
|
378
|
+
``symbol_id`` is left None here; the indexer fills it by line containment.
|
|
379
|
+
|
|
380
|
+
Endpoint consumer bindings receive a ``confidence`` score computed from
|
|
381
|
+
file-level heuristics (``_compute_file_confidence``). Store-dependent
|
|
382
|
+
adjustments (self_provided_key, known_provider_package) are applied later
|
|
383
|
+
by ``adjust_confidence`` in bridge.py.
|
|
384
|
+
|
|
385
|
+
All bindings are emitted regardless of their confidence score; the
|
|
386
|
+
``min_confidence`` filter in ``cross_repo_edges`` / ``build_graph`` decides
|
|
387
|
+
which ones become graph edges.
|
|
388
|
+
"""
|
|
389
|
+
out: list[ParsedBinding] = []
|
|
390
|
+
if language == "python":
|
|
391
|
+
for pat in _PY_ENV:
|
|
392
|
+
for m in pat.finditer(text):
|
|
393
|
+
name = m.group(1)
|
|
394
|
+
out.append(
|
|
395
|
+
_binding(
|
|
396
|
+
"env_var",
|
|
397
|
+
name,
|
|
398
|
+
"consumer",
|
|
399
|
+
file,
|
|
400
|
+
line=_line_of(text, m.start()),
|
|
401
|
+
language=language,
|
|
402
|
+
framework=_PY_FRAMEWORK,
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
for m in _PY_PKG.finditer(text):
|
|
406
|
+
pkg = m.group(1) or m.group(2)
|
|
407
|
+
out.append(
|
|
408
|
+
_binding(
|
|
409
|
+
"package",
|
|
410
|
+
pkg,
|
|
411
|
+
"consumer",
|
|
412
|
+
file,
|
|
413
|
+
line=_line_of(text, m.start()),
|
|
414
|
+
language=language,
|
|
415
|
+
framework="python",
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
elif language in ("typescript", "javascript"):
|
|
419
|
+
for pat in _TS_ENV:
|
|
420
|
+
for m in pat.finditer(text):
|
|
421
|
+
name = next(g for g in m.groups() if g)
|
|
422
|
+
out.append(
|
|
423
|
+
_binding(
|
|
424
|
+
"env_var",
|
|
425
|
+
name,
|
|
426
|
+
"consumer",
|
|
427
|
+
file,
|
|
428
|
+
line=_line_of(text, m.start()),
|
|
429
|
+
language=language,
|
|
430
|
+
framework="nextjs",
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
for m in _TS_PKG.finditer(text):
|
|
434
|
+
out.append(
|
|
435
|
+
_binding(
|
|
436
|
+
"package",
|
|
437
|
+
m.group(1),
|
|
438
|
+
"consumer",
|
|
439
|
+
file,
|
|
440
|
+
line=_line_of(text, m.start()),
|
|
441
|
+
language=language,
|
|
442
|
+
framework="npm",
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
for m in _TS_ENDPOINT.finditer(text):
|
|
446
|
+
raw = m.group(1)
|
|
447
|
+
conf = _compute_file_confidence(file, text, raw)
|
|
448
|
+
b = _binding(
|
|
449
|
+
"endpoint",
|
|
450
|
+
raw,
|
|
451
|
+
"consumer",
|
|
452
|
+
file,
|
|
453
|
+
line=_line_of(text, m.start()),
|
|
454
|
+
language=language,
|
|
455
|
+
framework="nextjs",
|
|
456
|
+
)
|
|
457
|
+
b.confidence = conf
|
|
458
|
+
out.append(b)
|
|
459
|
+
return out
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _line_of(text: str, offset: int) -> int:
|
|
463
|
+
return text.count("\n", 0, offset) + 1
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ── Tier B: repo-level providers (well-known files) ───────────────
|
|
467
|
+
|
|
468
|
+
_SKIP_DIRS = {
|
|
469
|
+
".git",
|
|
470
|
+
"node_modules",
|
|
471
|
+
".venv",
|
|
472
|
+
"venv",
|
|
473
|
+
"__pycache__",
|
|
474
|
+
"dist",
|
|
475
|
+
"build",
|
|
476
|
+
".mypy_cache",
|
|
477
|
+
".pytest_cache",
|
|
478
|
+
".ruff_cache",
|
|
479
|
+
".tox",
|
|
480
|
+
".next",
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def extract_repo_bindings(base: Path, repo: str) -> list[ParsedBinding]:
|
|
485
|
+
"""Tier B — provider bindings from a repo's well-known files.
|
|
486
|
+
|
|
487
|
+
Each sub-extractor is best-effort; a malformed file yields nothing rather
|
|
488
|
+
than aborting. ``repo`` is the canonical HTTPS slug (used to self-join
|
|
489
|
+
service deploy targets).
|
|
490
|
+
"""
|
|
491
|
+
out: list[ParsedBinding] = []
|
|
492
|
+
for path in _walk(base):
|
|
493
|
+
rel = _rel(path, base)
|
|
494
|
+
name = path.name
|
|
495
|
+
try:
|
|
496
|
+
if name.startswith("Pulumi.") and path.suffix in (".yaml", ".yml"):
|
|
497
|
+
out.extend(_pulumi_env_vars(path, rel))
|
|
498
|
+
elif name == "__main__.py":
|
|
499
|
+
out.extend(_service_anchors(path, rel))
|
|
500
|
+
elif name == "package.json":
|
|
501
|
+
out.extend(_package_provider(path, rel))
|
|
502
|
+
elif _looks_like_openapi(path):
|
|
503
|
+
out.extend(_openapi_endpoints(path, rel))
|
|
504
|
+
except Exception: # noqa: BLE001 — one bad file must not abort the repo
|
|
505
|
+
continue
|
|
506
|
+
return out
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _walk(base: Path):
|
|
510
|
+
for path in base.rglob("*"):
|
|
511
|
+
if path.is_dir() or any(part in _SKIP_DIRS for part in path.parts):
|
|
512
|
+
continue
|
|
513
|
+
yield path
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _rel(path: Path, base: Path) -> str:
|
|
517
|
+
try:
|
|
518
|
+
return path.resolve().relative_to(base.resolve()).as_posix()
|
|
519
|
+
except ValueError:
|
|
520
|
+
return path.name
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _looks_like_openapi(path: Path) -> bool:
|
|
524
|
+
if path.suffix not in (".json", ".yaml", ".yml"):
|
|
525
|
+
return False
|
|
526
|
+
if path.name.lower() in (
|
|
527
|
+
"openapi.json",
|
|
528
|
+
"openapi.yaml",
|
|
529
|
+
"openapi.yml",
|
|
530
|
+
"schema.yaml",
|
|
531
|
+
):
|
|
532
|
+
return True
|
|
533
|
+
return "openapi" in path.parts
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _pulumi_env_vars(path: Path, rel: str) -> list[ParsedBinding]:
|
|
537
|
+
"""Env-var providers: keys of any `<project>:env_vars` map in a stack config."""
|
|
538
|
+
import yaml
|
|
539
|
+
|
|
540
|
+
data = yaml.safe_load(path.read_text()) or {}
|
|
541
|
+
config = data.get("config", data) if isinstance(data, dict) else {}
|
|
542
|
+
out: list[ParsedBinding] = []
|
|
543
|
+
for cfg_key, value in (config or {}).items():
|
|
544
|
+
if cfg_key.endswith(":env_vars") and isinstance(value, dict):
|
|
545
|
+
for env_name in value:
|
|
546
|
+
if isinstance(env_name, str):
|
|
547
|
+
out.append(
|
|
548
|
+
_binding(
|
|
549
|
+
"env_var", env_name, "provider", rel, framework="pulumi"
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
return out
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
_SVC_REPO = re.compile(
|
|
556
|
+
r"github\.get_repository\(\s*full_name\s*=\s*[\"']([^\"']+)[\"']"
|
|
557
|
+
)
|
|
558
|
+
_SVC_IMAGE = re.compile(r"application_image_repository\s*=\s*[\"']([^\"']+)[\"']")
|
|
559
|
+
_SVC_NAME = re.compile(r"application_name\s*=\s*[\"']([^\"']+)[\"']")
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _service_anchors(path: Path, rel: str) -> list[ParsedBinding]:
|
|
563
|
+
"""Service/deploy anchors from an ol-infrastructure application __main__.py.
|
|
564
|
+
|
|
565
|
+
The repo full_name is normalized to its canonical HTTPS URI so it joins
|
|
566
|
+
against the deployed repo's own slug (kind=service, key_norm=<that URI>).
|
|
567
|
+
sub_kind ("repo" | "image" | "name") distinguishes anchor types without
|
|
568
|
+
packing the variant into the key string — joining on (kind, key_norm) works
|
|
569
|
+
even when the consumer records only the bare URI/name.
|
|
570
|
+
"""
|
|
571
|
+
text = path.read_text()
|
|
572
|
+
out: list[ParsedBinding] = []
|
|
573
|
+
for m in _SVC_REPO.finditer(text):
|
|
574
|
+
raw = m.group(1)
|
|
575
|
+
uri = f"https://github.com/{raw}" if "/" in raw and "://" not in raw else raw
|
|
576
|
+
out.append(
|
|
577
|
+
_binding(
|
|
578
|
+
"service",
|
|
579
|
+
uri,
|
|
580
|
+
"provider",
|
|
581
|
+
rel,
|
|
582
|
+
sub_kind="repo",
|
|
583
|
+
line=_line_of(text, m.start()),
|
|
584
|
+
framework="pulumi",
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
for m in _SVC_IMAGE.finditer(text):
|
|
588
|
+
out.append(
|
|
589
|
+
_binding(
|
|
590
|
+
"service",
|
|
591
|
+
m.group(1),
|
|
592
|
+
"provider",
|
|
593
|
+
rel,
|
|
594
|
+
sub_kind="image",
|
|
595
|
+
line=_line_of(text, m.start()),
|
|
596
|
+
framework="pulumi",
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
for m in _SVC_NAME.finditer(text):
|
|
600
|
+
out.append(
|
|
601
|
+
_binding(
|
|
602
|
+
"service",
|
|
603
|
+
m.group(1),
|
|
604
|
+
"provider",
|
|
605
|
+
rel,
|
|
606
|
+
sub_kind="name",
|
|
607
|
+
line=_line_of(text, m.start()),
|
|
608
|
+
framework="pulumi",
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
return out
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _package_provider(path: Path, rel: str) -> list[ParsedBinding]:
|
|
615
|
+
"""A repo that publishes an @mitodl/* package provides it."""
|
|
616
|
+
data = json.loads(path.read_text())
|
|
617
|
+
name = data.get("name")
|
|
618
|
+
if isinstance(name, str) and name.startswith("@mitodl/"):
|
|
619
|
+
return [_binding("package", name, "provider", rel, framework="npm")]
|
|
620
|
+
return []
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
_HTTP_METHODS = ("get", "post", "put", "patch", "delete", "head", "options")
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _openapi_endpoints(path: Path, rel: str) -> list[ParsedBinding]:
|
|
627
|
+
"""Endpoint providers: every path+method in a drf-spectacular OpenAPI spec."""
|
|
628
|
+
import yaml
|
|
629
|
+
|
|
630
|
+
raw = path.read_text()
|
|
631
|
+
data = json.loads(raw) if path.suffix == ".json" else yaml.safe_load(raw)
|
|
632
|
+
if not isinstance(data, dict) or "paths" not in data:
|
|
633
|
+
return []
|
|
634
|
+
out: list[ParsedBinding] = []
|
|
635
|
+
for url, methods in (data.get("paths") or {}).items():
|
|
636
|
+
if not isinstance(methods, dict):
|
|
637
|
+
continue
|
|
638
|
+
for method in methods:
|
|
639
|
+
if method.lower() in _HTTP_METHODS:
|
|
640
|
+
key = f"{method.upper()} {url}"
|
|
641
|
+
out.append(_binding("endpoint", key, "provider", rel, framework="drf"))
|
|
642
|
+
return out
|