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
witan_code/visualize.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Cross-repo dependency visualization from the bridge store.
|
|
2
|
+
|
|
3
|
+
Builds a directed "A depends on B" graph from interface bindings (A consumes a
|
|
4
|
+
contract B provides; for ``service`` bindings, the deploying repo depends on the
|
|
5
|
+
repo it deploys) and renders it as a Rich terminal summary and/or a self-contained
|
|
6
|
+
interactive HTML graph.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
# Per-kind edge colours, reused by the Rich legend and the HTML graph.
|
|
15
|
+
KIND_COLORS = {
|
|
16
|
+
"env_var": "#e8a33d",
|
|
17
|
+
"endpoint": "#4c9be8",
|
|
18
|
+
"package": "#56b870",
|
|
19
|
+
"service": "#b86fd1",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Edge:
|
|
25
|
+
src: str # depends on …
|
|
26
|
+
dst: str # … this repo
|
|
27
|
+
kinds: dict[str, int] = field(default_factory=lambda: defaultdict(int))
|
|
28
|
+
# the individual cross-repo linkages backing this edge: {"kind", "key"}
|
|
29
|
+
contracts: list[dict] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
def add(self, kind: str, key: str, confidence: float = 1.0) -> None:
|
|
32
|
+
self.kinds[kind] += 1
|
|
33
|
+
self.contracts.append({"kind": kind, "key": key, "confidence": confidence})
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def weight(self) -> int:
|
|
37
|
+
return sum(self.kinds.values())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DepGraph:
|
|
42
|
+
repos: set[str] = field(default_factory=set)
|
|
43
|
+
edges: dict[tuple[str, str], Edge] = field(default_factory=dict)
|
|
44
|
+
# (kind, key_norm) -> {"providers": {repo}, "consumers": {repo}}
|
|
45
|
+
contracts: dict[tuple[str, str], dict] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
def edge(self, src: str, dst: str) -> Edge:
|
|
48
|
+
key = (src, dst)
|
|
49
|
+
if key not in self.edges:
|
|
50
|
+
self.edges[key] = Edge(src, dst)
|
|
51
|
+
self.repos.update((src, dst))
|
|
52
|
+
return self.edges[key]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def short_repo(repo: str) -> str:
|
|
56
|
+
"""``https://github.com/mitodl/mit-learn`` → ``mitodl/mit-learn``."""
|
|
57
|
+
repo = repo.rstrip("/")
|
|
58
|
+
parts = repo.split("/")
|
|
59
|
+
return "/".join(parts[-2:]) if len(parts) >= 2 else repo
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cross_repo_edges(
|
|
63
|
+
rows: list[dict],
|
|
64
|
+
*,
|
|
65
|
+
kind: str | None = None,
|
|
66
|
+
min_confidence: float = 0.5,
|
|
67
|
+
) -> list[dict]:
|
|
68
|
+
"""Return the subset of binding rows that form genuine cross-repo edges.
|
|
69
|
+
|
|
70
|
+
Filters consumer bindings by ``min_confidence`` (default 0.5). Provider
|
|
71
|
+
bindings and non-endpoint consumers are always included (their confidence
|
|
72
|
+
defaults to 1.0 in the store). Original row dicts are returned unchanged;
|
|
73
|
+
the ``confidence`` key is already present on rows that have it (written by
|
|
74
|
+
the indexer). Legacy store records without a ``confidence`` key are treated
|
|
75
|
+
as 1.0 during filtering but are not mutated.
|
|
76
|
+
|
|
77
|
+
``kind`` narrows to one contract kind if provided.
|
|
78
|
+
"""
|
|
79
|
+
out: list[dict] = []
|
|
80
|
+
for row in rows:
|
|
81
|
+
if kind and row.get("kind") != kind:
|
|
82
|
+
continue
|
|
83
|
+
raw_conf = row.get("confidence")
|
|
84
|
+
conf = float(raw_conf if raw_conf is not None else 1.0)
|
|
85
|
+
if row.get("role") == "consumer" and row.get("kind") == "endpoint":
|
|
86
|
+
if conf < min_confidence:
|
|
87
|
+
continue
|
|
88
|
+
out.append(row)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_graph(
|
|
93
|
+
rows: list[dict],
|
|
94
|
+
*,
|
|
95
|
+
kind: str | None = None,
|
|
96
|
+
repo: str | None = None,
|
|
97
|
+
min_confidence: float = 0.5,
|
|
98
|
+
min_precision: str = "heuristic",
|
|
99
|
+
repo_symbol_rows: list[dict] | None = None,
|
|
100
|
+
) -> DepGraph:
|
|
101
|
+
"""Compute the cross-repo dependency graph from raw binding rows.
|
|
102
|
+
|
|
103
|
+
``kind`` filters to one contract kind; ``repo`` keeps only edges touching a
|
|
104
|
+
repo whose slug contains that substring. ``min_confidence`` (default 0.5)
|
|
105
|
+
suppresses low-confidence endpoint consumer rows before graph construction —
|
|
106
|
+
only endpoint consumers are filtered; all providers and non-endpoint consumers
|
|
107
|
+
pass through unconditionally.
|
|
108
|
+
|
|
109
|
+
``min_precision`` (docs/EDGE_PRECISION_TIERS.md) defaults to ``"heuristic"``
|
|
110
|
+
— every edge this function has always produced, unchanged. Pass
|
|
111
|
+
``"precise"`` plus ``repo_symbol_rows`` (a full ``all_repo_symbols`` dump)
|
|
112
|
+
to keep only edges also covered by a Stage-2 canonical-symbol join;
|
|
113
|
+
``"fuzzy"`` is currently identical to ``"heuristic"`` (no fuzzy tier
|
|
114
|
+
exists yet). The special ``service`` "repo depends on what it deploys"
|
|
115
|
+
edge is unaffected by ``min_precision`` — it isn't a symbol-joined
|
|
116
|
+
consumer/provider relationship. Raises ``ValueError`` for any other
|
|
117
|
+
value, matching ``edges.cross_repo_edges``.
|
|
118
|
+
"""
|
|
119
|
+
from . import edges as edges_module
|
|
120
|
+
|
|
121
|
+
if min_precision not in edges_module.PRECISION_TIERS:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"min_precision must be one of {edges_module.PRECISION_TIERS!r}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
filtered = cross_repo_edges(rows, kind=kind, min_confidence=min_confidence)
|
|
127
|
+
|
|
128
|
+
require_precise = min_precision == "precise"
|
|
129
|
+
precise_pairs = None
|
|
130
|
+
if require_precise:
|
|
131
|
+
precise_pairs = edges_module.precise_pairs(repo_symbol_rows or [])
|
|
132
|
+
|
|
133
|
+
groups: dict[tuple[str, str], dict] = defaultdict(
|
|
134
|
+
lambda: {"providers": set(), "consumers": {}}
|
|
135
|
+
)
|
|
136
|
+
# consumers maps repo -> confidence for the best (highest) confidence row
|
|
137
|
+
# for each (kind, key_norm, consumer_repo) triple.
|
|
138
|
+
consumer_conf: dict[tuple[str, str, str], float] = {}
|
|
139
|
+
|
|
140
|
+
for b in filtered:
|
|
141
|
+
b_kind = b["kind"]
|
|
142
|
+
key_norm = b["key_norm"]
|
|
143
|
+
b_repo = b["repo"]
|
|
144
|
+
if b["role"] == "provider":
|
|
145
|
+
groups[(b_kind, key_norm)]["providers"].add(b_repo)
|
|
146
|
+
else:
|
|
147
|
+
groups[(b_kind, key_norm)]["consumers"][b_repo] = None # placeholder
|
|
148
|
+
ck = (b_kind, key_norm, b_repo)
|
|
149
|
+
raw_conf = b.get("confidence")
|
|
150
|
+
conf = float(raw_conf if raw_conf is not None else 1.0)
|
|
151
|
+
if ck not in consumer_conf or conf > consumer_conf[ck]:
|
|
152
|
+
consumer_conf[ck] = conf
|
|
153
|
+
|
|
154
|
+
graph = DepGraph()
|
|
155
|
+
graph.contracts = {k: dict(v) for k, v in groups.items()}
|
|
156
|
+
for (b_kind, key_norm), g in groups.items():
|
|
157
|
+
for cons in g["consumers"]:
|
|
158
|
+
graph.repos.add(cons)
|
|
159
|
+
for prov in g["providers"]:
|
|
160
|
+
graph.repos.add(prov)
|
|
161
|
+
|
|
162
|
+
if b_kind == "service" and key_norm.startswith("repo:"):
|
|
163
|
+
# The deploying repo (provider) depends on the repo it deploys.
|
|
164
|
+
target = key_norm[len("repo:") :]
|
|
165
|
+
for src in g["providers"]:
|
|
166
|
+
if src != target:
|
|
167
|
+
graph.edge(src, target).add(b_kind, short_repo(target))
|
|
168
|
+
continue
|
|
169
|
+
if b_kind == "service":
|
|
170
|
+
continue # image:/name: anchors aren't repo-to-repo edges
|
|
171
|
+
# consumer depends on provider; skip when the consumer repo itself also
|
|
172
|
+
# provides this key_norm (the repo self-serves its own route, so the
|
|
173
|
+
# path-key collision with a foreign provider must not generate an edge).
|
|
174
|
+
self_providing = g["providers"]
|
|
175
|
+
for cons in g["consumers"]:
|
|
176
|
+
if cons in self_providing:
|
|
177
|
+
continue
|
|
178
|
+
for prov in g["providers"]:
|
|
179
|
+
if cons == prov:
|
|
180
|
+
continue
|
|
181
|
+
if (
|
|
182
|
+
require_precise
|
|
183
|
+
and (cons, prov, b_kind, key_norm) not in precise_pairs
|
|
184
|
+
):
|
|
185
|
+
continue
|
|
186
|
+
conf = consumer_conf.get((b_kind, key_norm, cons), 1.0)
|
|
187
|
+
graph.edge(cons, prov).add(b_kind, key_norm, conf)
|
|
188
|
+
|
|
189
|
+
if repo:
|
|
190
|
+
graph.edges = {
|
|
191
|
+
k: e for k, e in graph.edges.items() if repo in e.src or repo in e.dst
|
|
192
|
+
}
|
|
193
|
+
graph.repos = {r for e in graph.edges.values() for r in (e.src, e.dst)} or {
|
|
194
|
+
r for r in graph.repos if repo in r
|
|
195
|
+
}
|
|
196
|
+
return graph
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── Rich terminal summary ─────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def render_rich(graph: DepGraph, console=None) -> None:
|
|
203
|
+
from rich.console import Console
|
|
204
|
+
from rich.table import Table
|
|
205
|
+
|
|
206
|
+
console = console or Console()
|
|
207
|
+
|
|
208
|
+
if not graph.edges:
|
|
209
|
+
console.print(
|
|
210
|
+
"[yellow]No cross-repo dependencies found.[/] "
|
|
211
|
+
"Index at least two related repos first."
|
|
212
|
+
)
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
kind_totals: dict[str, int] = defaultdict(int)
|
|
216
|
+
for e in graph.edges.values():
|
|
217
|
+
for k, n in e.kinds.items():
|
|
218
|
+
kind_totals[k] += n
|
|
219
|
+
|
|
220
|
+
legend = " ".join(
|
|
221
|
+
f"[{_rich_color(k)}]●[/] {k} ({kind_totals[k]})"
|
|
222
|
+
for k in KIND_COLORS
|
|
223
|
+
if kind_totals.get(k)
|
|
224
|
+
)
|
|
225
|
+
console.print(
|
|
226
|
+
f"\n[bold]Cross-repo dependencies[/] "
|
|
227
|
+
f"{len(graph.repos)} repos · {len(graph.edges)} links\n{legend}\n"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
table = Table(show_lines=False, header_style="bold")
|
|
231
|
+
table.add_column("depends on →", style="cyan", overflow="fold", no_wrap=False)
|
|
232
|
+
table.add_column("provider", style="green", overflow="fold", no_wrap=False)
|
|
233
|
+
table.add_column("links", justify="right", no_wrap=True)
|
|
234
|
+
table.add_column("by kind", overflow="fold", no_wrap=False)
|
|
235
|
+
for e in sorted(graph.edges.values(), key=lambda e: e.weight, reverse=True):
|
|
236
|
+
kinds = " ".join(
|
|
237
|
+
f"[{_rich_color(k)}]{k}:{n}[/]" for k, n in sorted(e.kinds.items())
|
|
238
|
+
)
|
|
239
|
+
table.add_row(short_repo(e.src), short_repo(e.dst), str(e.weight), kinds)
|
|
240
|
+
console.print(table)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _rich_color(kind: str) -> str:
|
|
244
|
+
return {
|
|
245
|
+
"env_var": "yellow",
|
|
246
|
+
"endpoint": "blue",
|
|
247
|
+
"package": "green",
|
|
248
|
+
"service": "magenta",
|
|
249
|
+
}.get(kind, "white")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ── Self-contained interactive HTML ───────────────────────────────
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def render_html(graph: DepGraph, path: Path) -> Path:
|
|
256
|
+
"""Write an interactive force-directed graph (vis-network) to ``path``."""
|
|
257
|
+
nodes = [
|
|
258
|
+
{"id": r, "label": short_repo(r), "shape": "box"} for r in sorted(graph.repos)
|
|
259
|
+
]
|
|
260
|
+
edges = []
|
|
261
|
+
for i, e in enumerate(graph.edges.values()):
|
|
262
|
+
dominant = max(e.kinds, key=e.kinds.get)
|
|
263
|
+
title = ", ".join(f"{k}: {n}" for k, n in sorted(e.kinds.items()))
|
|
264
|
+
edges.append(
|
|
265
|
+
{
|
|
266
|
+
"id": i,
|
|
267
|
+
"from": e.src,
|
|
268
|
+
"to": e.dst,
|
|
269
|
+
"value": e.weight,
|
|
270
|
+
"label": f"{short_repo(e.src)} → {short_repo(e.dst)}",
|
|
271
|
+
"title": f"{short_repo(e.src)} → {short_repo(e.dst)} ({title}) — click for details",
|
|
272
|
+
"color": {"color": KIND_COLORS.get(dominant, "#888")},
|
|
273
|
+
"arrows": "to",
|
|
274
|
+
"contracts": sorted(e.contracts, key=lambda c: (c["kind"], c["key"])),
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
legend = "".join(
|
|
278
|
+
f'<span class="chip" style="background:{c}"></span>{k} '
|
|
279
|
+
for k, c in KIND_COLORS.items()
|
|
280
|
+
)
|
|
281
|
+
html = _HTML_TEMPLATE.format(
|
|
282
|
+
nodes=json.dumps(nodes),
|
|
283
|
+
edges=json.dumps(edges),
|
|
284
|
+
kind_colors=json.dumps(KIND_COLORS),
|
|
285
|
+
legend=legend,
|
|
286
|
+
n_repos=len(graph.repos),
|
|
287
|
+
n_edges=len(graph.edges),
|
|
288
|
+
)
|
|
289
|
+
path = path.expanduser()
|
|
290
|
+
path.write_text(html)
|
|
291
|
+
return path
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
_HTML_TEMPLATE = """<!doctype html>
|
|
295
|
+
<html><head><meta charset="utf-8"><title>Cross-repo dependencies</title>
|
|
296
|
+
<script src="https://unpkg.com/vis-network@9/standalone/umd/vis-network.min.js"></script>
|
|
297
|
+
<style>
|
|
298
|
+
body {{ margin: 0; font: 14px system-ui, sans-serif; background: #1b1d23; color: #ddd; }}
|
|
299
|
+
#bar {{ padding: 10px 16px; border-bottom: 1px solid #333; }}
|
|
300
|
+
#net {{ width: 100vw; height: calc(100vh - 50px); }}
|
|
301
|
+
.chip {{ display:inline-block; width:11px; height:11px; border-radius:2px; margin:0 4px 0 12px; vertical-align:middle; }}
|
|
302
|
+
#detail {{ position: fixed; top: 60px; right: 16px; width: 420px; max-height: calc(100vh - 80px);
|
|
303
|
+
overflow: auto; background: #23262f; border: 1px solid #3a3f4b; border-radius: 8px;
|
|
304
|
+
padding: 12px 14px; display: none; box-shadow: 0 6px 24px rgba(0,0,0,.4); }}
|
|
305
|
+
#detail h3 {{ margin: 0 0 8px; font-size: 14px; }}
|
|
306
|
+
#detail .close {{ float: right; cursor: pointer; color: #888; }}
|
|
307
|
+
#detail table {{ border-collapse: collapse; width: 100%; }}
|
|
308
|
+
#detail th, #detail td {{ text-align: left; padding: 4px 8px; border-bottom: 1px solid #333;
|
|
309
|
+
font-size: 13px; word-break: break-all; }}
|
|
310
|
+
#detail .kind {{ white-space: nowrap; font-weight: 600; }}
|
|
311
|
+
</style></head>
|
|
312
|
+
<body>
|
|
313
|
+
<div id="bar"><b>Cross-repo dependencies</b> — {n_repos} repos · {n_edges} links
|
|
314
|
+
"A → B" = A depends on B {legend}
|
|
315
|
+
<span style="color:#888">click an edge for the linkage list</span></div>
|
|
316
|
+
<div id="net"></div>
|
|
317
|
+
<div id="detail"></div>
|
|
318
|
+
<script>
|
|
319
|
+
const KIND_COLORS = {kind_colors};
|
|
320
|
+
const nodes = new vis.DataSet({nodes});
|
|
321
|
+
const edges = new vis.DataSet({edges});
|
|
322
|
+
const network = new vis.Network(document.getElementById("net"), {{nodes, edges}}, {{
|
|
323
|
+
nodes: {{ color: {{ background: "#2b2f3a", border: "#5a6", highlight: {{ background: "#39415a" }} }},
|
|
324
|
+
font: {{ color: "#eee" }}, borderWidth: 1 }},
|
|
325
|
+
edges: {{ scaling: {{ min: 1, max: 8 }}, smooth: {{ type: "dynamic" }},
|
|
326
|
+
font: {{ color: "#aaa", size: 11, strokeWidth: 0, align: "top" }} }},
|
|
327
|
+
physics: {{ stabilization: true, barnesHut: {{ springLength: 160 }} }},
|
|
328
|
+
interaction: {{ hover: true, tooltipDelay: 100 }}
|
|
329
|
+
}});
|
|
330
|
+
|
|
331
|
+
const panel = document.getElementById("detail");
|
|
332
|
+
function esc(s) {{ return String(s).replace(/[&<>]/g, c => ({{'&':'&','<':'<','>':'>'}}[c])); }}
|
|
333
|
+
function showEdge(id) {{
|
|
334
|
+
const e = edges.get(id);
|
|
335
|
+
if (!e) return;
|
|
336
|
+
const rows = e.contracts.map(c =>
|
|
337
|
+
`<tr><td class="kind" style="color:${{KIND_COLORS[c.kind] || '#ccc'}}">${{esc(c.kind)}}</td>`
|
|
338
|
+
+ `<td>${{esc(c.key)}}</td></tr>`).join("");
|
|
339
|
+
panel.innerHTML = `<span class="close" onclick="document.getElementById('detail').style.display='none'">✕</span>`
|
|
340
|
+
+ `<h3>${{esc(e.label)}}</h3>`
|
|
341
|
+
+ `<table><thead><tr><th>kind</th><th>contract</th></tr></thead><tbody>${{rows}}</tbody></table>`;
|
|
342
|
+
panel.style.display = "block";
|
|
343
|
+
}}
|
|
344
|
+
network.on("click", p => p.edges.length ? showEdge(p.edges[0]) : (panel.style.display = "none"));
|
|
345
|
+
</script>
|
|
346
|
+
</body></html>
|
|
347
|
+
"""
|