leyline-code 0.8.1__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.
- leyline/__init__.py +3 -0
- leyline/adapters/__init__.py +19 -0
- leyline/adapters/csharp.py +1026 -0
- leyline/adapters/generic.py +844 -0
- leyline/adapters/python.py +929 -0
- leyline/adapters/typescript.py +1512 -0
- leyline/affected.py +509 -0
- leyline/agent_skills.py +311 -0
- leyline/change.py +500 -0
- leyline/channels.py +1782 -0
- leyline/cli.py +1029 -0
- leyline/cluster.py +149 -0
- leyline/context.py +531 -0
- leyline/coupling.py +485 -0
- leyline/coverage.py +376 -0
- leyline/diagrams.py +680 -0
- leyline/diff.py +1078 -0
- leyline/drift.py +747 -0
- leyline/exact.py +410 -0
- leyline/explain.py +973 -0
- leyline/export.py +673 -0
- leyline/fncoupling.py +328 -0
- leyline/formats.py +923 -0
- leyline/grade.py +207 -0
- leyline/incremental.py +931 -0
- leyline/indexer.py +3733 -0
- leyline/learnings.py +743 -0
- leyline/loop.py +584 -0
- leyline/model.py +142 -0
- leyline/outline.py +1066 -0
- leyline/packing.py +86 -0
- leyline/patterns.py +478 -0
- leyline/pr.py +1249 -0
- leyline/props.py +255 -0
- leyline/query.py +683 -0
- leyline/quick.py +853 -0
- leyline/related.py +266 -0
- leyline/removal.py +231 -0
- leyline/renames.py +231 -0
- leyline/rereview.py +375 -0
- leyline/roslyn_refs/Program.cs +179 -0
- leyline/roslyn_refs/RoslynRefs.csproj +16 -0
- leyline/rules.py +171 -0
- leyline/schema.sql +178 -0
- leyline/scip_pb2.py +97 -0
- leyline/server.py +1230 -0
- leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
- leyline/skills/leyline-ask/SKILL.md +57 -0
- leyline/skills/leyline-change-impact/SKILL.md +79 -0
- leyline/skills/leyline-explain-flow/SKILL.md +70 -0
- leyline/skills/leyline-explore-module/SKILL.md +55 -0
- leyline/skills/leyline-pr-review/SKILL.md +77 -0
- leyline/skills/leyline-quick-change/SKILL.md +63 -0
- leyline/skills/leyline-spec/SKILL.md +179 -0
- leyline/skills/leyline-tour/SKILL.md +46 -0
- leyline/spec.py +1819 -0
- leyline/store.py +560 -0
- leyline/tours.py +352 -0
- leyline/verdicts.py +390 -0
- leyline/viewer/viewer.html +1622 -0
- leyline_code-0.8.1.dist-info/METADATA +1325 -0
- leyline_code-0.8.1.dist-info/RECORD +65 -0
- leyline_code-0.8.1.dist-info/WHEEL +4 -0
- leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
- leyline_code-0.8.1.dist-info/licenses/LICENSE +674 -0
leyline/tours.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Tours: an ordered walk through a codebase, each stop pointing at something on the map.
|
|
2
|
+
|
|
3
|
+
The orientation tour is generated from the graph alone, so every sentence in it is a count or a name
|
|
4
|
+
the store can back. An agent or the user can write further tours for a purpose (a feature end to end,
|
|
5
|
+
what a new contributor to one module needs) through `save`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from collections import Counter, defaultdict
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from . import store
|
|
19
|
+
|
|
20
|
+
SOURCE = "leyline-tour/0.1"
|
|
21
|
+
REF_KINDS = ("repo", "node", "flow", "pattern", "view")
|
|
22
|
+
SIDE_DIRS = ("sample", "samples", "example", "examples", "bench", "benchmarks", "docs", "doc", "demo", "demos", "snippets")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _plural(n: int, word: str) -> str:
|
|
26
|
+
return f"{n:,} {word}{'' if n == 1 else 's'}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _names(items, limit=4) -> str:
|
|
30
|
+
items = list(items)
|
|
31
|
+
return ", ".join(items[:limit]) + (f" and {len(items) - limit} more" if len(items) > limit else "")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _readme(con, repo_id: str) -> Optional[str]:
|
|
35
|
+
here = store.roots(con).get(repo_id)
|
|
36
|
+
row = (str(here),) if here is not None else None
|
|
37
|
+
if not row:
|
|
38
|
+
return None
|
|
39
|
+
for name in ("README.md", "README.rst", "README.txt", "README"):
|
|
40
|
+
p = Path(row[0]) / name
|
|
41
|
+
if p.is_file() and store.inside(p, row[0]):
|
|
42
|
+
para: list[str] = []
|
|
43
|
+
try:
|
|
44
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
45
|
+
except OSError:
|
|
46
|
+
continue
|
|
47
|
+
for line in text.splitlines():
|
|
48
|
+
s = line.strip()
|
|
49
|
+
skip = (not s or s.startswith(("#", "=", "-", "!", "[", "<", "|", "..", "```", ">")) or set(s) <= set("=-~"))
|
|
50
|
+
if skip and para:
|
|
51
|
+
break
|
|
52
|
+
if not skip:
|
|
53
|
+
para.append(s)
|
|
54
|
+
text = re.sub(r"\[([^\]]+)\](\([^)]*\)|\[[^\]]*\])?", r"\1", " ".join(para)).replace("**", "").replace("`", "")
|
|
55
|
+
if len(text) > 40:
|
|
56
|
+
return text[:420].rsplit(" ", 1)[0] + ("..." if len(text) > 420 else "")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# How each language is written in a sentence: one name per language, as its own community writes it.
|
|
61
|
+
LANGUAGE_NAMES = {"csharp": "C#", "python": "Python", "typescript": "TypeScript", "javascript": "JavaScript",
|
|
62
|
+
"go": "Go", "rust": "Rust", "java": "Java", "kotlin": "Kotlin", "swift": "Swift", "c": "C",
|
|
63
|
+
"cpp": "C++", "ruby": "Ruby", "php": "PHP", "scala": "Scala", "lua": "Lua", "elixir": "Elixir",
|
|
64
|
+
"dart": "Dart", "zig": "Zig", "haskell": "Haskell", "ocaml": "OCaml", "julia": "Julia",
|
|
65
|
+
"bash": "Bash", "gdscript": "GDScript"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def generate(con, repo_id: str) -> dict:
|
|
69
|
+
"""Write the orientation tour for a repo, replacing the previous one."""
|
|
70
|
+
q = lambda sql, *a: con.execute(sql, a).fetchall()
|
|
71
|
+
repo = q("SELECT id, name FROM nodes WHERE id = ? AND kind = 'repo'", repo_id)
|
|
72
|
+
if not repo:
|
|
73
|
+
return {"error": f"no repo {repo_id!r}"}
|
|
74
|
+
# In id order, not the order the rows were written in: a module an incremental run adds is written last, and a
|
|
75
|
+
# tie between modules must be settled as a full run would settle it.
|
|
76
|
+
mods = {r["id"]: dict(r) for r in q("SELECT id, name, path FROM nodes WHERE kind = 'module' AND repo_id = ? ORDER BY id", repo_id)}
|
|
77
|
+
named = Counter(m["name"] for m in mods.values())
|
|
78
|
+
for m in mods.values(): # two modules called lib (src/lib, scripts/lib) are told apart by their paths
|
|
79
|
+
if named[m["name"]] > 1 and m["path"]:
|
|
80
|
+
m["name"] = m["path"]
|
|
81
|
+
if not mods:
|
|
82
|
+
with con: # nothing to tour: the tour of an earlier map, which names modules that are gone, goes too
|
|
83
|
+
con.execute("DELETE FROM tour_stops WHERE tour_id = ?", (f"tour:orientation:{repo_id}",))
|
|
84
|
+
con.execute("DELETE FROM tours WHERE id = ?", (f"tour:orientation:{repo_id}",))
|
|
85
|
+
return {"stops": 0}
|
|
86
|
+
# The few module ids and kinds are one string each: every row read gives its own copy, and on a large
|
|
87
|
+
# repository the copies were hundreds of MB.
|
|
88
|
+
share = {}
|
|
89
|
+
share = share.setdefault
|
|
90
|
+
module = {r[0]: share(r[1], r[1]) for r in con.execute("SELECT node_id, module_id FROM ancestry")}
|
|
91
|
+
names, kind_of, parent = {}, {}, {}
|
|
92
|
+
for r in con.execute("SELECT id, name, kind, parent_id FROM nodes WHERE repo_id = ?", (repo_id,)): # one pass, not three
|
|
93
|
+
names[r[0]], kind_of[r[0]], parent[r[0]] = r[1], share(r[2], r[2]), r[3]
|
|
94
|
+
|
|
95
|
+
def qual(i):
|
|
96
|
+
p = parent.get(i)
|
|
97
|
+
return f"{names[p]}.{names[i]}" if kind_of.get(p) == "type" and kind_of.get(i) in ("callable", "field") else names.get(i, i)
|
|
98
|
+
# Only each flow's kind is read from its attrs, which list every module the flow passes (hundreds of MB in all).
|
|
99
|
+
flows = q("SELECT id, name, entry_id, json_extract(attrs, '$.kind') AS kind FROM flows"
|
|
100
|
+
" WHERE entry_id IN (SELECT id FROM nodes WHERE repo_id = ?)", repo_id)
|
|
101
|
+
flow_kind = {f["id"]: f["kind"] for f in flows}
|
|
102
|
+
test_mods = {module.get(f["entry_id"]) for f in flows if flow_kind[f["id"]] == "test"} - {None}
|
|
103
|
+
|
|
104
|
+
# Tests kept beside the code they test (grocery.test.ts next to grocery.ts) do not make that code a test module.
|
|
105
|
+
test_files, mostly_tests = store.test_places(con)
|
|
106
|
+
|
|
107
|
+
def side(m): # samples, docs, benchmarks and test helpers: real code, but not the product
|
|
108
|
+
return any(part.lower() in SIDE_DIRS or part.lower().startswith("test") for part in (mods[m]["path"] or "").split("/"))
|
|
109
|
+
core = [m for m in mods if m not in mostly_tests and not side(m)] or list(mods)
|
|
110
|
+
size = {r["m"]: (r["files"], r["loc"]) for r in q(
|
|
111
|
+
"SELECT a.module_id AS m, COUNT(*) AS files, SUM(COALESCE(n.span_end, 0)) AS loc FROM nodes n"
|
|
112
|
+
" JOIN ancestry a ON a.node_id = n.id WHERE n.kind = 'file' GROUP BY a.module_id")}
|
|
113
|
+
deps, users = defaultdict(set), defaultdict(set)
|
|
114
|
+
for sql in ("SELECT src_id, dst_id FROM calls",
|
|
115
|
+
"SELECT src_id, dst_id FROM edges WHERE kind IN ('imports','uses_type','instantiates','extends','implements')"):
|
|
116
|
+
for r in con.execute(sql): # read as they come: held all at once, millions of rows were GBs
|
|
117
|
+
a, b = module.get(r[0], r[0] if r[0] in mods else None), module.get(r[1], r[1] if r[1] in mods else None)
|
|
118
|
+
if a and b and a != b and a in mods and b in mods:
|
|
119
|
+
deps[a].add(b)
|
|
120
|
+
users[b].add(a)
|
|
121
|
+
langs = [r[0] for r in q("SELECT language FROM nodes WHERE kind = 'file' AND repo_id = ? AND language IS NOT NULL"
|
|
122
|
+
" GROUP BY language ORDER BY COUNT(*) DESC", repo_id)]
|
|
123
|
+
n_files = q("SELECT COUNT(*) FROM nodes WHERE kind = 'file' AND repo_id = ?", repo_id)[0][0]
|
|
124
|
+
stops: list[dict] = []
|
|
125
|
+
|
|
126
|
+
def stop(title, kind, ref, text):
|
|
127
|
+
stops.append({"title": title, "ref_kind": kind, "ref_id": ref, "narrative": " ".join(text.split())})
|
|
128
|
+
|
|
129
|
+
# 1. What it is.
|
|
130
|
+
readme = _readme(con, repo_id)
|
|
131
|
+
lang_names = LANGUAGE_NAMES
|
|
132
|
+
stop("What this repository is", "repo", repo_id,
|
|
133
|
+
f"{repo[0]['name']} has {_plural(len(mods), 'module')} and {_plural(n_files, 'source file')}, written in "
|
|
134
|
+
f"{_names([lang_names.get(x, x) for x in langs])}. "
|
|
135
|
+
+ (f"{len(test_mods)} of the modules hold tests. " if test_mods else "No tests were found. ")
|
|
136
|
+
+ (f"Its README begins: \"{readme}\"" if readme else ""))
|
|
137
|
+
|
|
138
|
+
# 2. Where it starts.
|
|
139
|
+
entries = q("SELECT e.dst_id AS target, n.attrs FROM edges e JOIN nodes n ON n.id = e.src_id"
|
|
140
|
+
" WHERE e.kind = 'exposes' AND n.kind = 'entry_point' AND n.repo_id = ?", repo_id)
|
|
141
|
+
reach = {}
|
|
142
|
+
for f in flows:
|
|
143
|
+
if flow_kind[f["id"]] != "test":
|
|
144
|
+
steps = [r[0] for r in q("SELECT callable_id FROM flow_steps WHERE flow_id = ?", f["id"])]
|
|
145
|
+
reach[f["entry_id"]] = (f["id"], len(steps), {module.get(s) for s in steps} - {None})
|
|
146
|
+
path_of = {r["id"]: r["path"] for r in q("SELECT id, path FROM nodes WHERE repo_id = ? AND kind IN ('callable', 'test')", repo_id)}
|
|
147
|
+
all_starts = [e["target"] for e in entries if e["target"] in reach]
|
|
148
|
+
# An entry that reaches almost nothing (a build script, a one-line sample) teaches little.
|
|
149
|
+
starts = sorted((t for t in all_starts if reach[t][1] > 8),
|
|
150
|
+
key=lambda t: (module.get(t) not in core, -len(reach[t][2]), -reach[t][1], t))
|
|
151
|
+
if not starts and core:
|
|
152
|
+
lib = max(core, key=lambda m: len(users[m]))
|
|
153
|
+
stop("No single place to start", "node", lib,
|
|
154
|
+
"No entry point in the product code leads far, which is what a library looks like: it is entered through "
|
|
155
|
+
"whatever its callers use. "
|
|
156
|
+
+ (f"The {_plural(len(all_starts), 'entry point')} found {'is a sample, benchmark or build script' if len(all_starts) == 1 else 'are samples, benchmarks or build scripts'}. " if all_starts else "")
|
|
157
|
+
+ "The tests are the best record of how it is meant to be called.")
|
|
158
|
+
picked = []
|
|
159
|
+
for t in starts: # at most two, from different modules
|
|
160
|
+
if len(picked) < 2 and module.get(t) not in {module.get(x) for x in picked}:
|
|
161
|
+
picked.append(t)
|
|
162
|
+
for t in picked:
|
|
163
|
+
fid, n, ms = reach[t]
|
|
164
|
+
m = module.get(t)
|
|
165
|
+
script = names.get(t, "").startswith("<") or "top-level" in names.get(t, "")
|
|
166
|
+
body = f"The top-level code of {path_of.get(t) or 'this file'} in" if script else f"{qual(t)} in"
|
|
167
|
+
stop(f"Where {(path_of.get(t) or '').rsplit('/', 1)[-1] if script else qual(t)} starts", "node", t,
|
|
168
|
+
f"{body} {mods[m]['name'] if m in mods else 'this module'} is a program entry point. From here the code can reach "
|
|
169
|
+
f"{_plural(n - 1, 'function')} in {_plural(len(ms), 'module')}"
|
|
170
|
+
+ (f": {_names(sorted(mods[x]['name'] for x in ms if x in mods))}." if ms else ".")
|
|
171
|
+
+ " Open Flows to walk that path in order.")
|
|
172
|
+
if len(all_starts) > len(picked) and picked:
|
|
173
|
+
stops[-1]["narrative"] += f" There are {len(all_starts) - len(picked)} more entry points; the repository panel lists them."
|
|
174
|
+
|
|
175
|
+
# 3. The modules, most depended-on first.
|
|
176
|
+
order = sorted(core, key=lambda m: (-len(users[m] & set(core)), -len(users[m]), -(size.get(m, (0, 0))[1] or 0), m))
|
|
177
|
+
top_types = lambda m: list(dict.fromkeys(r["name"] for r in q(
|
|
178
|
+
"SELECT n.name, COUNT(*) AS c FROM edges e JOIN nodes n ON n.id = e.dst_id JOIN ancestry a ON a.node_id = n.id"
|
|
179
|
+
" WHERE a.module_id = ? AND n.kind = 'type' AND e.kind IN ('uses_type','instantiates','extends','implements')"
|
|
180
|
+
" GROUP BY n.id ORDER BY c DESC, n.name LIMIT 8", m)))[:4]
|
|
181
|
+
systems = defaultdict(list)
|
|
182
|
+
for r in q("SELECT n.id, n.parent_id, n.name, (SELECT value FROM annotations a WHERE a.node_id = n.id AND a.key = 'name'"
|
|
183
|
+
" ORDER BY layer = 'intent' DESC LIMIT 1) AS given FROM nodes n WHERE n.kind = 'system'"):
|
|
184
|
+
systems[r["parent_id"]].append(r["given"] or r["name"])
|
|
185
|
+
for k, m in enumerate(order[:5]):
|
|
186
|
+
files, loc = size.get(m, (0, 0))
|
|
187
|
+
d, u = sorted(mods[x]["name"] for x in deps[m]), sorted(mods[x]["name"] for x in users[m])
|
|
188
|
+
role = ("Nothing else in the repository is needed to read it, so it is the place to start." if not d and u else
|
|
189
|
+
"Nothing depends on it, so it is an end point: an application, a tool or a host for the code below it." if d and not u else
|
|
190
|
+
"It stands alone: no other module uses it and it uses none." if not d and not u else "")
|
|
191
|
+
tt = top_types(m)
|
|
192
|
+
stop(("The foundation: " if k == 0 and u else "Module: ") + mods[m]["name"], "node", m,
|
|
193
|
+
f"{mods[m]['name']} has {_plural(files, 'file')} and about {loc:,} lines. "
|
|
194
|
+
+ (f"It is used by {_names(u)}. " if u else "")
|
|
195
|
+
+ (f"It depends on {_names(d)}. " if d else "")
|
|
196
|
+
+ role + " "
|
|
197
|
+
+ (f"Its most used types are {_names(tt)}. " if tt else "")
|
|
198
|
+
+ (f"Clustering splits it into {len(systems[m])} groups: {_names(sorted(systems[m]), 5)}." if systems.get(m) else ""))
|
|
199
|
+
if len(order) > 5:
|
|
200
|
+
stops[-1]["narrative"] += f" {len(order) - 5} smaller modules are left out of this tour."
|
|
201
|
+
|
|
202
|
+
# 4. The main abstractions and boundaries, from the pattern labels.
|
|
203
|
+
try:
|
|
204
|
+
from . import patterns
|
|
205
|
+
labels = patterns.listing(con, limit=500)["patterns"]
|
|
206
|
+
except Exception:
|
|
207
|
+
labels = []
|
|
208
|
+
weight = lambda p: (p["confidence"], sum(len(v) for v in p["roles"].values()))
|
|
209
|
+
shapes = sorted((p for p in labels if p["pattern"] in ("strategy", "composite", "decorator", "template method", "builder")
|
|
210
|
+
and p["confidence"] >= 0.7), key=weight, reverse=True)
|
|
211
|
+
seen_kinds = set()
|
|
212
|
+
for p in shapes:
|
|
213
|
+
if p["pattern"] in seen_kinds or len(seen_kinds) >= 3:
|
|
214
|
+
continue
|
|
215
|
+
seen_kinds.add(p["pattern"])
|
|
216
|
+
stop(f"A {p['pattern']} to know", "pattern", p["id"], f"{p['rationale']} {p['about']}")
|
|
217
|
+
edges_ = sorted((x for x in labels if x["pattern"] in ("process boundary", "observer")),
|
|
218
|
+
key=lambda x: (x["pattern"] != "process boundary", "standard input" not in x["rationale"]))
|
|
219
|
+
shown = set()
|
|
220
|
+
for p in edges_:
|
|
221
|
+
if p["pattern"] in shown:
|
|
222
|
+
continue # one of each kind; the rest are in the pattern list
|
|
223
|
+
shown.add(p["pattern"])
|
|
224
|
+
stop("A boundary" if p["pattern"] == "process boundary" else "An event", "pattern", p["id"],
|
|
225
|
+
f"{p['rationale']} A change on one side of this needs a matching change on the other, and the compiler will not say so.")
|
|
226
|
+
|
|
227
|
+
# State with more than one writer.
|
|
228
|
+
try:
|
|
229
|
+
from . import query
|
|
230
|
+
shared = [f for f in query.shared_state(con, limit=100000)["fields"] if len(f["written_from"]) >= 2]
|
|
231
|
+
except Exception:
|
|
232
|
+
shared = []
|
|
233
|
+
if shared:
|
|
234
|
+
f = shared[0]
|
|
235
|
+
stop("State changed from several places", "node", f["id"],
|
|
236
|
+
f"{f['name']} is assigned by {_plural(f['writers'], 'function')} in {_names(f['written_from'])}, none of them the type "
|
|
237
|
+
f"that declares it, and read by {_plural(f['readers'], 'function')}. No single piece of code keeps it valid, so a "
|
|
238
|
+
f"change to what it means has to be checked at every one of those places. "
|
|
239
|
+
+ (f"{len(shared) - 1} more fields are assigned from two or more other types; `leyline state` lists them." if len(shared) > 1 else ""))
|
|
240
|
+
|
|
241
|
+
# 5. A path worth tracing: a test that crosses the most modules without being huge.
|
|
242
|
+
best = None
|
|
243
|
+
sized = store.flow_callables(con, 6, 80)
|
|
244
|
+
core_set = set(core)
|
|
245
|
+
for f in flows:
|
|
246
|
+
if flow_kind[f["id"]] != "test" or f["id"] not in sized:
|
|
247
|
+
continue
|
|
248
|
+
steps = sized[f["id"]]
|
|
249
|
+
if 6 <= len(steps) <= 80:
|
|
250
|
+
ms = {module.get(s) for s in steps} - {None}
|
|
251
|
+
score = (len(ms & core_set), -abs(len(steps) - 25))
|
|
252
|
+
if best is None or score > best[0]:
|
|
253
|
+
best = (score, f, len(steps), ms)
|
|
254
|
+
if best:
|
|
255
|
+
_, f, n, ms = best
|
|
256
|
+
stop("A path worth tracing", "flow", f["id"],
|
|
257
|
+
f"The test \"{f['name']}\" passes through {_plural(n, 'function')} in {_names(sorted(mods[x]['name'] for x in ms if x in mods))}. "
|
|
258
|
+
f"Reading it top to bottom shows how those modules work together on one concrete case.")
|
|
259
|
+
|
|
260
|
+
# 6. Tests, and what the map does not show.
|
|
261
|
+
n_tests = sum(1 for f in flows if flow_kind[f["id"]] == "test")
|
|
262
|
+
if n_tests:
|
|
263
|
+
tested = {r[0] for r in q(store.TESTED)}
|
|
264
|
+
fns = [r[0] for r in q("SELECT id, path FROM nodes WHERE kind = 'callable' AND repo_id = ?", repo_id)
|
|
265
|
+
if module.get(r[0]) in core and r[1] not in test_files]
|
|
266
|
+
on = sum(1 for i in fns if i in tested)
|
|
267
|
+
home = sorted(test_mods, key=lambda m: -(size.get(m, (0, 0))[1] or 0))
|
|
268
|
+
from . import coverage as measured
|
|
269
|
+
m = measured.summary(con)
|
|
270
|
+
watched = sum(x["functions"] for x in m.get("modules", []))
|
|
271
|
+
if m.get("imported") and watched:
|
|
272
|
+
did = sum(x["ran"] for x in m["modules"])
|
|
273
|
+
off = sum(x["ran_off_every_path"] for x in m["modules"])
|
|
274
|
+
never = sum(x["path_but_never_ran"] for x in m["modules"])
|
|
275
|
+
tail = (f"Coverage was measured: {did:,} of the {watched:,} functions in the watched files ran ({round(100 * did / watched)}%). "
|
|
276
|
+
f"{off:,} of those ran without being on any test's path on the map, so they were reached through links the map "
|
|
277
|
+
f"does not have; {never:,} are on a path and never ran.")
|
|
278
|
+
else:
|
|
279
|
+
tail = ("That is reachability read from the source, not measured coverage: a function on a path may sit behind a "
|
|
280
|
+
"branch the test never takes. Import a coverage file to see what ran.")
|
|
281
|
+
stop("How it is tested", "node", home[0],
|
|
282
|
+
f"There are {_plural(n_tests, 'test')} in {_names([mods[m_]['name'] for m_ in home if m_ in mods])}. "
|
|
283
|
+
f"{on:,} of the {len(fns):,} functions outside test code are on some test's path ({round(100 * on / max(1, len(fns)))}%). " + tail)
|
|
284
|
+
cov = q("SELECT extractor, status, stats FROM extractor_coverage WHERE repo_id = ?", repo_id)
|
|
285
|
+
blind = sorted(c["extractor"].split(":", 1)[-1] for c in cov if c["status"] == "not_analyzed" and c["extractor"].startswith("communicates"))
|
|
286
|
+
guess = q("SELECT COUNT(*) FROM calls WHERE precision = 'guess'")[0][0]
|
|
287
|
+
total = q("SELECT COUNT(*) FROM calls")[0][0]
|
|
288
|
+
open_calls = sum((json.loads(c["stats"] or "{}").get("calls_unresolved", 0) + json.loads(c["stats"] or "{}").get("calls_guess_declined", 0))
|
|
289
|
+
for c in cov if c["extractor"].startswith(("tree-sitter", "generic")))
|
|
290
|
+
stop("What this map cannot see", "repo", repo_id,
|
|
291
|
+
f"Links come from reading syntax, not from a compiler. Of {total:,} call links, {guess:,} are guesses by name, and "
|
|
292
|
+
f"{open_calls:,} call sites could not be tied to a function at all. "
|
|
293
|
+
+ (f"Connections through {_names(blind, 6)} are not analyzed, so two parts that talk that way look unconnected. " if blind else "")
|
|
294
|
+
+ "Flows show what can run, not what did run.")
|
|
295
|
+
|
|
296
|
+
tid = f"tour:orientation:{repo_id}"
|
|
297
|
+
_write(con, tid, f"Orientation: {repo[0]['name']}", "someone new to the repository", "fact", SOURCE, repo_id, stops)
|
|
298
|
+
return {"id": tid, "stops": len(stops)}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _write(con, tid, title, audience, layer, source, repo_id, stops) -> None:
|
|
302
|
+
now = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
|
|
303
|
+
with con:
|
|
304
|
+
con.execute("DELETE FROM tour_stops WHERE tour_id = ?", (tid,))
|
|
305
|
+
con.execute("INSERT OR REPLACE INTO tours (id, title, audience, layer, source, repo_id, created) VALUES (?,?,?,?,?,?,?)",
|
|
306
|
+
(tid, title, audience, layer, source, repo_id, now))
|
|
307
|
+
con.executemany("INSERT INTO tour_stops (tour_id, seq, ref_kind, ref_id, narrative, title) VALUES (?,?,?,?,?,?)",
|
|
308
|
+
[(tid, k, s["ref_kind"], s["ref_id"], s["narrative"], s.get("title") or "") for k, s in enumerate(stops)])
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _exists(con, kind: str, ref: str) -> bool:
|
|
312
|
+
table = {"node": "nodes", "repo": "nodes", "flow": "flows", "pattern": "pattern_instances", "view": "views"}[kind]
|
|
313
|
+
return con.execute(f"SELECT 1 FROM {table} WHERE id = ?", (ref,)).fetchone() is not None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def save(con, title: str, stops: list[dict], audience: str = "", source: str = "mcp", layer: str = "inferred") -> dict:
|
|
317
|
+
"""Save an authored tour. Each stop is {"title", "ref": id, "kind": node|flow|pattern|view|repo, "narrative"}."""
|
|
318
|
+
clean, missing = [], []
|
|
319
|
+
for s in stops:
|
|
320
|
+
kind = s.get("kind") or s.get("ref_kind") or "node"
|
|
321
|
+
ref = s.get("ref") or s.get("ref_id") or s.get("id")
|
|
322
|
+
if kind not in REF_KINDS:
|
|
323
|
+
return {"error": f"stop kind must be one of {REF_KINDS}"}
|
|
324
|
+
if not ref or not _exists(con, kind, ref):
|
|
325
|
+
missing.append(ref)
|
|
326
|
+
continue
|
|
327
|
+
if not (s.get("narrative") or "").strip():
|
|
328
|
+
return {"error": "every stop needs a narrative: what to notice here and why it matters"}
|
|
329
|
+
clean.append({"title": s.get("title") or "", "ref_kind": kind, "ref_id": ref, "narrative": s["narrative"].strip()})
|
|
330
|
+
if not clean:
|
|
331
|
+
return {"error": "no stop refers to something in the store", "missing": missing[:5]}
|
|
332
|
+
tid = "tour:" + hashlib.sha1(title.encode()).hexdigest()[:8]
|
|
333
|
+
repo = con.execute("SELECT id FROM nodes WHERE kind = 'repo' LIMIT 1").fetchone()
|
|
334
|
+
_write(con, tid, title, audience, layer, source, repo[0] if repo else None, clean)
|
|
335
|
+
return {"id": tid, "title": title, "stops": len(clean), "missing": missing[:10]}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def listing(con) -> dict:
|
|
339
|
+
out = [{"id": t["id"], "title": t["title"], "audience": t["audience"], "source": t["source"], "layer": t["layer"],
|
|
340
|
+
"stops": con.execute("SELECT COUNT(*) FROM tour_stops WHERE tour_id = ?", (t["id"],)).fetchone()[0]}
|
|
341
|
+
for t in con.execute("SELECT * FROM tours ORDER BY source = ? DESC, created DESC", (SOURCE,))]
|
|
342
|
+
return {"total": len(out), "tours": out}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get(con, tour_id: str) -> dict:
|
|
346
|
+
t = con.execute("SELECT * FROM tours WHERE id = ?", (tour_id,)).fetchone()
|
|
347
|
+
if t is None:
|
|
348
|
+
return {"error": f"No tour {tour_id!r}. Use `tours` to list them."}
|
|
349
|
+
stops = [{"seq": s["seq"] + 1, "title": s["title"], "kind": s["ref_kind"], "ref": s["ref_id"], "narrative": s["narrative"],
|
|
350
|
+
"exists": _exists(con, s["ref_kind"], s["ref_id"])}
|
|
351
|
+
for s in con.execute("SELECT * FROM tour_stops WHERE tour_id = ? ORDER BY seq", (tour_id,))]
|
|
352
|
+
return {"id": t["id"], "title": t["title"], "audience": t["audience"], "source": t["source"], "layer": t["layer"], "stops": stops}
|