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.
Files changed (65) hide show
  1. leyline/__init__.py +3 -0
  2. leyline/adapters/__init__.py +19 -0
  3. leyline/adapters/csharp.py +1026 -0
  4. leyline/adapters/generic.py +844 -0
  5. leyline/adapters/python.py +929 -0
  6. leyline/adapters/typescript.py +1512 -0
  7. leyline/affected.py +509 -0
  8. leyline/agent_skills.py +311 -0
  9. leyline/change.py +500 -0
  10. leyline/channels.py +1782 -0
  11. leyline/cli.py +1029 -0
  12. leyline/cluster.py +149 -0
  13. leyline/context.py +531 -0
  14. leyline/coupling.py +485 -0
  15. leyline/coverage.py +376 -0
  16. leyline/diagrams.py +680 -0
  17. leyline/diff.py +1078 -0
  18. leyline/drift.py +747 -0
  19. leyline/exact.py +410 -0
  20. leyline/explain.py +973 -0
  21. leyline/export.py +673 -0
  22. leyline/fncoupling.py +328 -0
  23. leyline/formats.py +923 -0
  24. leyline/grade.py +207 -0
  25. leyline/incremental.py +931 -0
  26. leyline/indexer.py +3733 -0
  27. leyline/learnings.py +743 -0
  28. leyline/loop.py +584 -0
  29. leyline/model.py +142 -0
  30. leyline/outline.py +1066 -0
  31. leyline/packing.py +86 -0
  32. leyline/patterns.py +478 -0
  33. leyline/pr.py +1249 -0
  34. leyline/props.py +255 -0
  35. leyline/query.py +683 -0
  36. leyline/quick.py +853 -0
  37. leyline/related.py +266 -0
  38. leyline/removal.py +231 -0
  39. leyline/renames.py +231 -0
  40. leyline/rereview.py +375 -0
  41. leyline/roslyn_refs/Program.cs +179 -0
  42. leyline/roslyn_refs/RoslynRefs.csproj +16 -0
  43. leyline/rules.py +171 -0
  44. leyline/schema.sql +178 -0
  45. leyline/scip_pb2.py +97 -0
  46. leyline/server.py +1230 -0
  47. leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
  48. leyline/skills/leyline-ask/SKILL.md +57 -0
  49. leyline/skills/leyline-change-impact/SKILL.md +79 -0
  50. leyline/skills/leyline-explain-flow/SKILL.md +70 -0
  51. leyline/skills/leyline-explore-module/SKILL.md +55 -0
  52. leyline/skills/leyline-pr-review/SKILL.md +77 -0
  53. leyline/skills/leyline-quick-change/SKILL.md +63 -0
  54. leyline/skills/leyline-spec/SKILL.md +179 -0
  55. leyline/skills/leyline-tour/SKILL.md +46 -0
  56. leyline/spec.py +1819 -0
  57. leyline/store.py +560 -0
  58. leyline/tours.py +352 -0
  59. leyline/verdicts.py +390 -0
  60. leyline/viewer/viewer.html +1622 -0
  61. leyline_code-0.8.1.dist-info/METADATA +1325 -0
  62. leyline_code-0.8.1.dist-info/RECORD +65 -0
  63. leyline_code-0.8.1.dist-info/WHEEL +4 -0
  64. leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
  65. leyline_code-0.8.1.dist-info/licenses/LICENSE +674 -0
leyline/props.py ADDED
@@ -0,0 +1,255 @@
1
+ """Property-based tests: scenarios that state an invariant, and the counterexample a failing property test prints.
2
+
3
+ A scenario that says something holds for any input ("for any amount, the balance is never negative") is proven
4
+ better by a property test than by one example: the library generates inputs, and when one breaks the property it
5
+ shrinks it to the smallest that still does. Kiro writes specs this way; here it is two small things.
6
+
7
+ `plan`: a scenario whose words state an invariant is marked, with the property-test library for the language
8
+ of the code it is about (Hypothesis, fast-check, FsCheck).
9
+ `check`: a failing property test's output carries its counterexample. Read from the runner's text, it is kept
10
+ on the test's result, so the scenario's verdict says what input broke it: "fails for amount=-1".
11
+
12
+ Formats read:
13
+ Hypothesis Falsifying example: test_x(a=0, b=-1) (or "Failing test case:", Hypothesis 6.130 and later)
14
+ fast-check Property failed after 1 tests ... Counterexample: [0,-1] ... Shrunk 32 time(s)
15
+ FsCheck Falsifiable, after 3 tests (1 shrink) (...): Original: ... Shrunk: ...
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from collections import Counter
22
+ from typing import Optional
23
+
24
+ # -- plan: which scenarios state an invariant ---------------------------------------------------------
25
+ # Words that say a statement holds over all inputs, not for one example. Kept narrow: "each" and "any" alone are
26
+ # too common in ordinary scenarios ("each request is logged") to mean an invariant.
27
+ INVARIANT = re.compile(r"\b(for (any|every|all|each possible)\b|always\b|never\b|no matter\b|regardless of\b|"
28
+ r"invariant\b|whatever (the|its)\b|under any\b|in any order\b)", re.I)
29
+
30
+ LIBRARIES = {
31
+ "python": "Hypothesis",
32
+ "typescript": "fast-check",
33
+ "javascript": "fast-check",
34
+ "csharp": "FsCheck",
35
+ "fsharp": "FsCheck",
36
+ }
37
+ EXT = {"py": "python", "pyi": "python", "ts": "typescript", "tsx": "typescript", "mts": "typescript", "cts": "typescript",
38
+ "js": "javascript", "jsx": "javascript", "mjs": "javascript", "cjs": "javascript", "cs": "csharp", "fs": "fsharp"}
39
+
40
+
41
+ def invariant(s: dict) -> bool:
42
+ """Whether a scenario's name or its WHEN/THEN lines state an invariant: for any, for every, always, never..."""
43
+ text = " ".join([s.get("name") or ""] + list(s.get("when") or []) + list(s.get("then") or []))
44
+ return bool(INVARIANT.search(text))
45
+
46
+
47
+ def _language(paths: list) -> Optional[str]:
48
+ langs = Counter(EXT.get((p or "").rsplit(".", 1)[-1].lower()) for p in paths if p and "." in p)
49
+ langs.pop(None, None)
50
+ return langs.most_common(1)[0][0] if langs else None
51
+
52
+
53
+ def language(con, ids: list[str], test_id: Optional[str] = None) -> Optional[str]:
54
+ """The language a scenario's test is written in: its test's file, else the code the tasks change, else the
55
+ repository's most common one."""
56
+ def paths(where, args):
57
+ return [r[0] for r in con.execute(f"SELECT path FROM nodes WHERE {where}", args)]
58
+ if test_id:
59
+ lang = _language(paths("id = ?", (test_id,)))
60
+ if lang:
61
+ return lang
62
+ if ids:
63
+ lang = _language(paths(f"id IN ({','.join('?' * len(ids))})", ids))
64
+ if lang:
65
+ return lang
66
+ return _language(paths("kind = 'file' AND layer = 'fact'", ()))
67
+
68
+
69
+ def mark_scenarios(con, scenarios: list[dict], ids: list[str]) -> None:
70
+ """Mark each scenario that states an invariant, in place: `invariant` and `property_library`."""
71
+ for s in scenarios:
72
+ if invariant(s):
73
+ s["invariant"] = True
74
+ s["property_library"] = LIBRARIES.get(language(con, ids, s.get("test")) or "")
75
+
76
+
77
+ def cell(s: dict) -> str:
78
+ """What the plan's scenario table adds for an invariant scenario."""
79
+ if not s.get("invariant"):
80
+ return ""
81
+ lib = s.get("property_library")
82
+ return "; invariant: a property test fits" + (f" ({lib})" if lib else "")
83
+
84
+
85
+ # -- check: the counterexample a failing property test prints ------------------------------------------
86
+ _HYPOTHESIS = re.compile(r"(?:Falsifying(?: explicit)? example|Failing test case):\s*([A-Za-z_]\w*)\(")
87
+ _FASTCHECK = re.compile(r"Counterexample:\s*(.*\S)") # greedy: a lazy one before \s*$ is quadratic in a run of spaces
88
+ _FC_RUNS = re.compile(r"Property failed after (\d+) tests?")
89
+ _FSCHECK = re.compile(r"Falsifiable, after (\d+) tests? \((\d+) shrinks?\)")
90
+ _PREFIX = re.compile(r"^(?:E\s{1,8}|\s*[|>]\s?)") # pytest's `E ` before an assertion's lines
91
+ MARK = re.compile(r"^fails for (.+) \((Hypothesis|fast-check|FsCheck)\)\.?$")
92
+ LIMIT = 200
93
+
94
+
95
+ def _clip(text: str) -> str:
96
+ text = " ".join(text.split())
97
+ return text if len(text) <= LIMIT else text[:LIMIT - 1].rstrip() + "…"
98
+
99
+
100
+ def _split_top(text: str) -> list[str]:
101
+ """Split at commas that are not inside brackets or quotes."""
102
+ out, depth, quote, cur = [], 0, None, []
103
+ for k, ch in enumerate(text):
104
+ if quote:
105
+ if ch == quote and text[k - 1] != "\\":
106
+ quote = None
107
+ elif ch in "\"'":
108
+ quote = ch
109
+ elif ch in "([{<":
110
+ depth += 1
111
+ elif ch in ")]}>":
112
+ depth -= 1
113
+ elif ch == "," and depth == 0:
114
+ out.append("".join(cur))
115
+ cur = []
116
+ continue
117
+ cur.append(ch)
118
+ out.append("".join(cur))
119
+ return [x.strip() for x in out if x.strip()]
120
+
121
+
122
+ def _hypothesis_args(lines: list[str], k: int, start: int) -> Optional[str]:
123
+ """The arguments of `test_x(...)` from the line it opens on to its closing parenthesis."""
124
+ buf, depth, quote, escaped = [], 1, None, False
125
+ for j in range(k, min(len(lines), k + 60)):
126
+ line = lines[j] if j > k else lines[j][start:]
127
+ if j > k:
128
+ line = _PREFIX.sub("", line)
129
+ line = re.sub(r"\s#\s.*$", "", line) # `a=[], # or any other generated value`
130
+ for ch in line:
131
+ if quote:
132
+ if escaped:
133
+ escaped = False # 'a\'b"c': an escaped quote does not close it
134
+ elif ch == "\\":
135
+ escaped = True
136
+ else:
137
+ quote = None if ch == quote else quote
138
+ elif ch in "\"'":
139
+ quote = ch
140
+ elif ch in "([{":
141
+ depth += 1
142
+ elif ch in ")]}":
143
+ depth -= 1
144
+ if depth == 0:
145
+ args = [a for a in _split_top("".join(buf)) if not a.startswith("self=")]
146
+ return ", ".join(args)
147
+ buf.append(ch)
148
+ buf.append(" ")
149
+ return None
150
+
151
+
152
+ def counterexamples(text: str) -> list[dict]:
153
+ """Every counterexample in a test run's text: {line, library, example, func (Hypothesis: the test function),
154
+ tests, shrinks}. The example is the smallest failing input the library found, as it printed it."""
155
+ lines = (text or "").splitlines()
156
+ out = []
157
+ for k, line in enumerate(lines):
158
+ m = _HYPOTHESIS.search(line)
159
+ if m:
160
+ args = _hypothesis_args(lines, k, m.end())
161
+ if args is not None:
162
+ out.append({"line": k, "library": "Hypothesis", "func": m.group(1), "example": _clip(args) or "no arguments"})
163
+ continue
164
+ m = _FASTCHECK.search(line)
165
+ head = next((x for x in reversed(lines[max(0, k - 6):k]) if _FC_RUNS.search(x)), None)
166
+ if m and head is not None:
167
+ ex = m.group(1)
168
+ if re.search(r"message:\s*\"", head): # TAP's YAML: the message is a quoted string, its quotes escaped
169
+ ex = ex.replace('\\"', '"').replace("\\\\", "\\")
170
+ if ex.startswith("[") and ex.endswith("]"):
171
+ ex = ", ".join(_split_top(ex[1:-1])) # the property's arguments, as a tuple
172
+ out.append({"line": k, "library": "fast-check", "example": _clip(ex) or "no arguments",
173
+ "tests": int(_FC_RUNS.search(head).group(1))})
174
+ continue
175
+ m = _FSCHECK.search(line)
176
+ if m:
177
+ blocks: dict = {}
178
+ cur = None
179
+ for x in lines[k + 1:k + 80]:
180
+ x = x.strip()
181
+ if x in ("Original:", "Shrunk:"): # each argument on a line of its own
182
+ cur = blocks.setdefault(x[:-1], [])
183
+ elif not x or x.startswith(("with exception:", "Last step was invoked")) or _FSCHECK.search(x):
184
+ if "Shrunk" in blocks or x.startswith("with exception:") or _FSCHECK.search(x):
185
+ break
186
+ cur = None
187
+ elif cur is not None:
188
+ cur.append(x)
189
+ args = blocks.get("Shrunk") or blocks.get("Original")
190
+ if args:
191
+ out.append({"line": k, "library": "FsCheck", "example": _clip(", ".join(args)), "tests": int(m.group(1)),
192
+ "shrinks": int(m.group(2))})
193
+ return out
194
+
195
+
196
+ def from_message(message: Optional[str]) -> Optional[str]:
197
+ """The counterexample `annotate` put on a result's message: "amount=-1"."""
198
+ first = (message or "").split("\n", 1)[0]
199
+ m = MARK.match(first)
200
+ return m.group(1) if m else None
201
+
202
+
203
+ def _keys(name: str) -> set[str]:
204
+ """What a result's name can be found by in a runner's text: the test's own name, without a parameter."""
205
+ from .diff import result_parts
206
+ p = result_parts(name)
207
+ leaf = p["leaf"].strip()
208
+ out = {leaf, re.sub(r"\[.*\]$", "", leaf)}
209
+ if "." in leaf and " " not in leaf: # Namespace.Class.Method, as dotnet prints it
210
+ out.add(leaf.rsplit(".", 1)[-1])
211
+ return {k for k in out if len(k) >= 3}
212
+
213
+
214
+ def _put(r: dict, ce: dict) -> None:
215
+ rest = (r.get("message") or "").strip()
216
+ if rest.startswith('"') and rest.count('"') == 1: # a quoted YAML message that went on past its first line
217
+ rest = rest[1:]
218
+ r["message"] = f"fails for {ce['example']} ({ce['library']})." + (f"\n{rest}" if rest else "")
219
+
220
+
221
+ def annotate(results: list[dict], text: Optional[str] = None) -> list[dict]:
222
+ """Put each failing property test's counterexample at the head of its result's message, in place, and return
223
+ the results. A counterexample is found in the result's own message, or in the run's text: Hypothesis names its
224
+ test; for the others, the nearest line above that names a failing test is taken to be its header."""
225
+ failing = [r for r in results if r.get("status") == "fail" and not from_message(r.get("message"))]
226
+ if not failing:
227
+ return results
228
+ for r in failing:
229
+ found = counterexamples(r.get("message") or "")
230
+ if found:
231
+ _put(r, found[0])
232
+ left = [r for r in failing if not from_message(r.get("message"))]
233
+ if not left or not text:
234
+ return results
235
+ lines = text.splitlines()
236
+ keys = {id(r): _keys(r["name"]) for r in left}
237
+ def nearest(cands: list[dict], at: int) -> Optional[dict]:
238
+ """The failing result named on the closest line above, by its longest name; a tie looks further up."""
239
+ for j in range(at, max(-1, at - 400), -1):
240
+ score = {id(x): max((len(k) for k in keys[id(x)] if k in lines[j]), default=0) for x in cands}
241
+ best = max(score.values(), default=0)
242
+ top = [x for x in cands if best and score[id(x)] == best]
243
+ if len(top) == 1:
244
+ return top[0]
245
+ return None
246
+ for ce in counterexamples(text):
247
+ cands = [r for r in left if ce["func"] in keys[id(r)]] if ce.get("func") else left
248
+ r = cands[0] if ce.get("func") and len(cands) == 1 else nearest(cands, ce["line"] - (0 if ce.get("func") else 1))
249
+ if r is None:
250
+ continue
251
+ _put(r, ce)
252
+ left.remove(r)
253
+ if not left:
254
+ break
255
+ return results