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
@@ -0,0 +1,1325 @@
1
+ Metadata-Version: 2.5
2
+ Name: leyline-code
3
+ Version: 0.8.1
4
+ Summary: Map a codebase so a person can plan a change, have an agent write the code, and check that it was done as agreed.
5
+ Project-URL: Homepage, https://github.com/Orbitope/leyline
6
+ Project-URL: Issues, https://github.com/Orbitope/leyline/issues
7
+ License-Expression: GPL-3.0-only
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: mcp>=1.0
11
+ Requires-Dist: networkx>=3.0
12
+ Requires-Dist: tree-sitter-c-sharp>=0.23
13
+ Requires-Dist: tree-sitter-python>=0.23
14
+ Requires-Dist: tree-sitter-typescript>=0.23
15
+ Requires-Dist: tree-sitter>=0.23
16
+ Provides-Extra: languages
17
+ Requires-Dist: tree-sitter-bash; extra == 'languages'
18
+ Requires-Dist: tree-sitter-c; extra == 'languages'
19
+ Requires-Dist: tree-sitter-cpp; extra == 'languages'
20
+ Requires-Dist: tree-sitter-gdscript; extra == 'languages'
21
+ Requires-Dist: tree-sitter-go; extra == 'languages'
22
+ Requires-Dist: tree-sitter-java; extra == 'languages'
23
+ Requires-Dist: tree-sitter-kotlin; extra == 'languages'
24
+ Requires-Dist: tree-sitter-lua; extra == 'languages'
25
+ Requires-Dist: tree-sitter-php; extra == 'languages'
26
+ Requires-Dist: tree-sitter-ruby; extra == 'languages'
27
+ Requires-Dist: tree-sitter-rust; extra == 'languages'
28
+ Requires-Dist: tree-sitter-scala; extra == 'languages'
29
+ Requires-Dist: tree-sitter-swift; extra == 'languages'
30
+ Provides-Extra: scip
31
+ Requires-Dist: protobuf>=7.35; extra == 'scip'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Leyline
35
+
36
+ ## Working through an agent
37
+
38
+ Leyline is meant to be driven by a coding agent: you ask in plain words, the agent uses Leyline's map and follows
39
+ one of its skills, and you read the answer or the page it writes. Set it up once, in the repository you will work on:
40
+
41
+ ```bash
42
+ pip install leyline-code # or, from a checkout: pip install -e .
43
+ claude mcp add leyline -- leyline serve # connect Claude Code to Leyline's MCP server
44
+ leyline skills install # copy the skills into .claude/skills (and .agents/skills if you have one)
45
+ ```
46
+
47
+ Start a new agent session, then ask. Nothing needs mapping first: the agent maps the code when it needs to.
48
+
49
+ | Skill | What it does | Ask, for example |
50
+ | --- | --- | --- |
51
+ | `leyline-ask` | Answers a question about the code, citing it, and says how sure it is | "Who calls `saveOne`, and is that link certain?" |
52
+ | `leyline-explain-flow` | Explains how something runs, step by step, from the map | "What happens when a user saves a dialogue?" |
53
+ | `leyline-explore-module` | Says what one part of the code holds and where to start reading | "What is in `editor/host`, and where do I start?" |
54
+ | `leyline-change-impact` | Says what a described change would touch, break and need tested | "What would changing the validator's output format affect?" |
55
+ | `leyline-spec` | Plans a change as an OpenSpec folder, checked against the code, then checks it after | "Plan adding an export button, with tests." |
56
+ | `leyline-quick-change` | Makes a small fix with no spec and says whether it was done; says when it needs a spec | "Make the retry count 3." |
57
+ | `leyline-pr-review` | Reviews a pull request: what changed, what it breaks, findings, what to fix first | "Review pull request 123." |
58
+ | `leyline-adversarial-review` | Attacks a plan or a pull request as a logic or a performance reviewer | "Stress-test the plan for loud-engine." |
59
+ | `leyline-tour` | Writes a guided reading order through the code, saved on the map | "Write a tour of the payment code for a new hire." |
60
+
61
+ `leyline skills list` describes them, `leyline skills show <name>` prints one, and running `leyline skills install`
62
+ again updates them, leaving alone any you edited (`--force` replaces those). An agent connected to `leyline serve`
63
+ can also load each skill as an MCP prompt of the same name, or through the `skills` tool, without installing them.
64
+ The rest of this page is what the agent does underneath, and the commands for doing it yourself.
65
+
66
+ ## Start here
67
+
68
+ Leyline lets a person design a change to a codebase while a coding agent writes it. Before any code is
69
+ written it shows, on one page, what code will be written, what it will affect and how you will know it
70
+ was done; afterwards it says on the same page whether the change was done as agreed. Everything on that
71
+ page comes from the code and the test results, not from the agent's account of its own work.
72
+
73
+ ```bash
74
+ pip install -e . # Python 3.10 or later; installs the `leyline` command
75
+ ```
76
+
77
+ Three commands, in this order. Each one ends with a `Next:` line saying what to do after it.
78
+
79
+ | Command | When | What you read |
80
+ | --- | --- | --- |
81
+ | `leyline map [repo ...]` | Once, before the first change | Ten lines on what was found, and a map page to browse |
82
+ | `leyline plan <change>` | After the change is written as a spec, and after every edit to it | The page, `leyline.md`, and what is still needed before implementation |
83
+ | `leyline check <change> --tests FILE` | After the code is written | The verdict, added to the same page |
84
+
85
+ A change is an [OpenSpec](https://openspec.dev) folder, `openspec/changes/<id>/`: a proposal, the
86
+ requirements with scenarios, and a task list. You describe the change in words and your agent writes the
87
+ folder; `skills/leyline-spec/SKILL.md` tells it how. `<change>` is the folder or just its id.
88
+
89
+ A change someone else already wrote (a branch, a pull request) has no spec. Check it out and run
90
+ `leyline pr [base]` instead: see [Reviewing a pull request](#reviewing-a-pull-request).
91
+
92
+ A one-line fix needs no spec either: `leyline quick "make the retry count 3" --about RETRIES` before, and
93
+ `leyline quick --done quick-make-the-retry-count-3 --tests -` after. See [A quick change](#a-quick-change).
94
+
95
+ ### A worked example
96
+
97
+ The repository `tests/fixture2` has a small Python package with an `Engine` class and its tests.
98
+
99
+ ```bash
100
+ cp -r tests/fixture2 /tmp/engine-repo && cd /tmp/engine-repo
101
+ leyline map .
102
+ ```
103
+ ```
104
+ Mapped engine-repo in 0.8 s: 14 files, 354 lines, 4 modules.
105
+ Found 34 types, 73 functions, 5 tests and 1 entry point (where a program starts).
106
+ Modules: cs/Mod (8 files), py/src/pkg (2 files), py/tests (2 files), py/web (2 files)
107
+ Design patterns found: builder, composite, decorator, factory 2, singleton, strategy, template method
108
+ Store: .leyline/leyline.db
109
+ Map page: .leyline/map.html (open it in a browser)
110
+ Next: write the change you want as an OpenSpec folder, openspec/changes/<id>/ (ask your agent; ...
111
+ ```
112
+
113
+ You tell your agent: "engine names should come back in upper case, and add a `shout`". It writes
114
+ `openspec/changes/loud-engine/` with this `tasks.md`
115
+
116
+ ```
117
+ - [ ] 1.1 Change `Engine.start` to return the name in upper case
118
+ - [ ] 1.2 Add `Engine.shout`, the name with an exclamation mark
119
+ - [ ] 1.3 Add the test "Shout"
120
+ ```
121
+
122
+ and two scenarios, `Start` and `Shout`, in `specs/engine/spec.md`. Then you plan it, passing the tests'
123
+ output from before any code changes so that `check` can tell a test the change breaks from one that
124
+ already failed (TAP from vitest, node --test or tap, `pytest -rA`, `go test -v`, `jest --verbose`,
125
+ `dotnet test --logger "console;verbosity=detailed"`, or any runner
126
+ that prints one PASS or FAIL line per test):
127
+
128
+ ```bash
129
+ (cd py && PYTHONPATH=src pytest -rA tests) | leyline plan loud-engine --tests -
130
+ ```
131
+ ```
132
+ # Loud engine
133
+ ...
134
+ **State: ready to implement.** It has not been reviewed.
135
+
136
+ Names are hard to read in logs. The plan has 3 tasks: it changes Engine.start and adds Engine.shout.
137
+ Nothing else must be edited with it; 4 places in 1 module run into the changed code and may behave
138
+ differently; 3 existing tests already run through it. It is done when 2 scenarios pass: 1 already has a
139
+ test, 1 needs a test written.
140
+ ...
141
+ **Shares a caller or a field with the change, and no task names it.** Each line is either right to
142
+ leave alone or a missing task:
143
+ - Engine.name (used by Engine.start) is also used by Engine.child
144
+ ...
145
+ Next: have the plan reviewed (logic and performance) before code is written (ask your agent to run the
146
+ leyline-adversarial-review skill), then run `leyline plan loud-engine` again. Or, if you accept the plan
147
+ as it is, implement it.
148
+ ```
149
+
150
+ The agent implements the tasks. Then:
151
+
152
+ ```bash
153
+ (cd py && PYTHONPATH=src pytest -rA tests) | leyline check loud-engine --tests -
154
+ ```
155
+ ```
156
+ ## 4. Was it done as agreed
157
+ **Yes.** Every task is done, every scenario is proven, and nothing outside the spec changed.
158
+
159
+ | Task | Result | Verdict | Missing |
160
+ | 1.1 Change `Engine.start` to return the name in upper case | done | proven | |
161
+ ...
162
+ | Scenario | Result | Verdict | Evidence |
163
+ | Start | passes | proven | its test reaches the changed code on the map |
164
+ | Shout | passes | proven | its test reaches the changed code on the map |
165
+
166
+ Verdicts: 5 proven. Blocking: partial, contradicted, inconclusive. Not blocking: needs you. (The default.)
167
+
168
+ Tests: 5 of 5 passed before, 6 of 6 after.
169
+ Next: nothing left to check; the change was done as agreed. Review the diff and commit it.
170
+ ```
171
+
172
+ `check` exits 0 only when the change was done as agreed. Had the agent also edited `Engine.child`, the
173
+ verdict would read "Not yet: 1 edit is outside the spec", and `Next:` would ask you to add a task for it or
174
+ undo it.
175
+
176
+ ### What you read
177
+
178
+ `openspec/changes/<id>/leyline.md` is the one page for a change. It opens with its state (not ready,
179
+ ready, done as agreed or not) and a paragraph in plain words, then:
180
+
181
+ 1. **What code will be written**: each task and the code it touches.
182
+ 2. **What it will affect**: what must be edited with it, what runs into it, the risks, and code that
183
+ shares a caller or a field with the change that no task names.
184
+ 3. **How you will know it was done**: each scenario and the test that proves it.
185
+ 4. **Review findings** and **Before implementation**: what blocks implementation, and what is left to
186
+ decide. Only you resolve a finding.
187
+ 5. **Was it done as agreed**, added by `check`.
188
+
189
+ ### With an agent
190
+
191
+ The agent uses the same loop through Leyline's MCP server. To connect it to Claude Code, run this in
192
+ the root of the repository you will work on:
193
+
194
+ ```bash
195
+ claude mcp add leyline -- leyline serve # this repository only
196
+ claude mcp add -s user leyline -- leyline serve # or: every project you open
197
+ ```
198
+
199
+ and `leyline skills install` to give Claude Code the skills (see [Working through an agent](#working-through-an-agent)).
200
+
201
+ Claude Code starts the server in the directory it was opened in, and the server reads the store there,
202
+ `.leyline/leyline.db`, or, from a folder inside the repository, the nearest such store above it. Nothing needs to be mapped first: the agent's first call is `map`. If `leyline` is
203
+ installed in a virtual environment, give its full path (`-- /path/to/venv/bin/leyline serve`); for a
204
+ store elsewhere, such as a workspace mapped from another directory, add `-e LEYLINE_DB=/full/path/leyline.db`
205
+ before the `--`. `claude mcp list` should then show `leyline` as connected.
206
+
207
+ The agent's path is the same three tools, `map`, `plan` and `check`, each returning `next`, with the
208
+ review tools between plan and implementation; the server's instructions give the agent that order.
209
+ The skills in `skills/` say how to write the spec (`leyline-spec`), review it
210
+ (`leyline-adversarial-review`), assess any change (`leyline-change-impact`), explain the code
211
+ (`leyline-tour`) and answer how something works (`leyline-explain-flow`). Answers are kept short enough for an agent's context: long lists are cut, and the
212
+ answer says what was cut (`cut`) and how to see the rest (`more`).
213
+
214
+ Everything below is the detail: the other commands, how the map is built, and how far to trust it.
215
+ `leyline --help` lists the other commands under "advanced".
216
+
217
+ ## What Leyline is
218
+
219
+ Leyline maps a codebase into one SQLite graph that people and coding agents can both query.
220
+ Every record belongs to one of three layers:
221
+
222
+ - **fact**: extracted deterministically from the source, rebuilt on every index run
223
+ - **inferred**: written by an LLM, with evidence and a confidence
224
+ - **intent**: written by you, such as boundaries and rules
225
+
226
+ This version builds the fact layer for C# and Python, proposes systems by clustering, labels design
227
+ patterns by their shape, generates an orientation tour, assesses a described change before it is made,
228
+ reviews it after, and checks architecture rules.
229
+
230
+ ## Use
231
+
232
+ ```bash
233
+ pip install -e .
234
+ leyline map path/to/repo # writes path/to/repo/.leyline/leyline.db and .leyline/map.html
235
+ leyline index path/to/repo # the same index, printing the full statistics instead
236
+ leyline --db ... coverage .coverage # import measured test coverage
237
+ leyline --db path/to/repo/.leyline/leyline.db overview
238
+ leyline --db ... search "Simulation Step"
239
+ leyline --db ... expand "<node id>"
240
+ leyline --db ... tour # a guided walk through the repository
241
+ leyline --db ... patterns # design patterns found by their shape
242
+ leyline --db ... state # fields assigned from outside their own type
243
+ leyline --db ... serve # MCP server over stdio
244
+ leyline --db ... view # the map, served on http://127.0.0.1:8765
245
+ leyline --db ... export -o map.html # the map as one self-contained page
246
+ ```
247
+
248
+ To use it from an MCP client, register the command `leyline --db <path> serve`.
249
+
250
+ ### Several repositories
251
+
252
+ ```bash
253
+ leyline map flask/ werkzeug/ # one workspace; writes ./.leyline/leyline.db
254
+ leyline --db .leyline/leyline.db index werkzeug/ # re-indexes flask too
255
+ ```
256
+
257
+ Repositories named together are indexed in one run, so a name in one resolves to its declaration in
258
+ the other: `import werkzeug` in Flask reaches Werkzeug's source (a package under `src/` by its package
259
+ name), a Flask class extends Werkzeug's, and a Werkzeug method that calls `self.open()` dispatches
260
+ into Flask's override. Ids keep their own repo's prefix, so a store indexed one repository at a time
261
+ reads the same. The store remembers its members: indexing any one of them later indexes all of them
262
+ again, which keeps the links between them. A member whose directory has gone is left as stored, and `map` says so.
263
+ Name its new place and it is the same member, under its old id, not a second copy, when Leyline can tell it is the
264
+ same repository: both are git repositories with the same first commit, and the same `origin` where both have one.
265
+ Anything less sure (not under git, or a store mapped before Leyline recorded first commits) is mapped as a new
266
+ member; drop the old one with `leyline map --forget <id>`. That drops a member's facts, where it was and its place
267
+ in the workspace, then maps the others again in full so no link into it is left; notes and review decisions about
268
+ its code are kept, as when code is deleted.
269
+
270
+ `overview` lists the links between repositories; the `cross_repo` tool adds the functions most called
271
+ across and the flows that cross and come back. An edge between repositories carries `to_repo`;
272
+ `impact` and a change assessment count what they reach per repo and flag a change that reaches into
273
+ another one. Not yet: the C# compiler pass (SCIP indexes are matched to their repository by the
274
+ directory they were made in), and values handed across that the syntax pass cannot follow, such as
275
+ the WSGI app a test client calls.
276
+
277
+ ### Someone else's repository
278
+
279
+ Leyline is made to be run on code you did not write: a pull request under review, a project you just
280
+ cloned. What such a repository holds is treated as its author's, not as Leyline's or yours:
281
+
282
+ - **`.leyline/` is Leyline's own.** Its files are local and rebuilt by `leyline map`, so a repository
283
+ that tracks files there (a pull request can commit a store with annotations, resolved findings and
284
+ rules of its author's making, a snapshot or a review page, and checking it out writes over your
285
+ ignored copy) is refused, with the files named. Stop tracking them (`git rm -r --cached .leyline`)
286
+ or keep the store elsewhere (`--db /elsewhere/leyline.db`, or `LEYLINE_DB`). A `.leyline` that is
287
+ a link to a folder outside the repository is refused too, so nothing is written there.
288
+ `LEYLINE_TRUST_STORE=1` uses the folder anyway, for a store you committed on purpose.
289
+ - **Nothing is written outside the repository through a link.** A file Leyline writes (the map page,
290
+ a change's `leyline.md`, the anchors and learnings files) replaces a link at its name rather than
291
+ writing through it; a change folder or `openspec/` that is a link to a folder outside the
292
+ repository is read but not written to, and the plan or check says so ("Not written: ...").
293
+ - **A pull request does not vouch for itself.** A learnings file records what reviewers decided
294
+ before, so one a pull request commits ("f may return anything, do not flag it") would steer its own
295
+ review. Learnings the pull request's commits add or change (compared with its base) are not applied
296
+ to it: not in `learnings_that_apply`, not matched against its findings. Its page lists them under
297
+ "Learnings added by this pull request, not applied", for the person to judge.
298
+ - **An agent's paths stay inside the map.** The MCP tools that take a path (`map`'s `paths`, the change
299
+ folder of `plan`, `check` and the `spec_*` tools, `affected_tests`, `drift`'s `path`, `review_pr`'s
300
+ `path`, a coverage file for `check`, `quick` or `coverage`) accept only paths inside the
301
+ repositories the store maps, or, before anything is mapped, the directory the server was started
302
+ in. An agent can be told which path to pass by text it read; the error says the person can run the
303
+ `leyline` command themselves, which takes any path.
304
+
305
+ ## How the pieces fit
306
+
307
+ Everything lives in one file: `.leyline/leyline.db` inside the repository you indexed. Three things
308
+ read and write it, and none of them runs unless you start it. Beside it, `leyline.cache.db` holds what the
309
+ next index needs to do again only what an edit changed: each file's parse output under its content hash,
310
+ and fingerprints of the last run. Deleting it only makes the next index a full one, and so does damage to it: a
311
+ cache that is not a database is started again.
312
+
313
+ | Piece | Started by | Reads | Writes |
314
+ | --- | --- | --- | --- |
315
+ | `leyline map` or `index` | You, a git hook or CI; `plan` and `check` when the code changed | The working tree | Facts, flows and system proposals. Replaces the previous facts: after the first run only what changed is redone, and the store comes out as a full run would leave it (`--full` forces one). |
316
+ | `leyline serve` (MCP) | Your coding agent, when it starts | The store | Annotations, change proposals and saved views |
317
+ | `leyline view` or `export` | You | The store | Nothing |
318
+
319
+ One index runs at a time per store. A second one that starts while another is running (a `map` while a `plan`
320
+ re-maps) says so on stderr, naming the other process and how long it has been going, waits for it to finish,
321
+ and then maps what changed since. It waits up to an hour (`LEYLINE_WAIT`, in seconds), and past that stops with
322
+ the same message. A run that dies lets go of the store with its process.
323
+
324
+ Annotations, proposals and views are not facts, so re-indexing keeps them. An annotation is flagged
325
+ stale when the code behind its evidence changes.
326
+
327
+ ### The spec loop
328
+
329
+ The intended way to change a mapped codebase: the person drives the design, an agent writes the
330
+ code, and one page says what will be written, what it affects and how you will know it was done.
331
+
332
+ A change is an [OpenSpec](https://openspec.dev) change folder. Leyline reads it and writes
333
+ `leyline.md` back into it. `leyline plan` and `leyline check` (see Start here) run the loop; these are
334
+ its steps one at a time, for when you want one alone:
335
+
336
+ ```
337
+ leyline spec brief openspec/changes/<id> # before any code: the one-page brief, and the gaps in the spec
338
+ leyline spec facts openspec/changes/<id> --reviewer logic # what the graph says, as questions for reviewers
339
+ leyline spec verify openspec/changes/<id> --before before --after after # after: was it done as agreed
340
+ ```
341
+
342
+ `plan` is `brief` after re-indexing changed code, and records the test output you pass it under
343
+ `before:spec-<id>`. `check` re-indexes, records its test output under `after:spec-<id>`, and runs `verify`
344
+ with both. Before re-indexing, both compare each source file's hash with the store, so an unchanged
345
+ repository is not indexed again.
346
+
347
+ node --test's TAP writes a tab in a test's name and a backslash followed by `t` the same way (`\\t`, and so
348
+ for `\n`, `\r`, `\b`, `\f`, `\v`); Leyline reads it as the tab, so a name with a literal `\t` in it may not
349
+ match its test.
350
+
351
+ The brief ties each task to code and each scenario to a test, by three conventions and no markup:
352
+
353
+ - Code named in backticks in `tasks.md` is looked up on the map (`` `Vehicle.Speed` ``). A name not on
354
+ the map is new code: `` `Owner.NewName` `` says where a member goes, and `` `module.new_func` ``,
355
+ `` `path/to/file.py: new_func` `` or `` `new_func` in `file.py` `` where a top-level function goes. A
356
+ path that is on the map counts with or without backticks. Other words in backticks (an issue code, a
357
+ value, a doc file) are noted, not checked. A task that names no code is left for the person to check;
358
+ a change whose tasks all name no code (docs only) is planned and checked all the same, and `check` says
359
+ whether any code changed with it.
360
+ - A task that starts with add, remove, rename or "change the signature" is read that way. Anything
361
+ else is a change in behavior.
362
+ - A scenario is proven by a test with the same name: one on the map, or one made at run time (a name built
363
+ from a template, a pytest parameter) whose result in the test run carries that name. A scenario that states an
364
+ invariant ("for any", "for every", "always", "never") is marked "invariant: a property test fits", with the
365
+ library for its language (Hypothesis, fast-check, FsCheck). When such a test fails, `check` reads the shrunk
366
+ counterexample from the runner's output and gives it as the reason: "contradicted: fails for amount=-1".
367
+
368
+ It then lists what the change reaches that no task covers, the patterns it sits in, the gaps that
369
+ block implementation, and the code that uses the same things and that no task names: other callers of
370
+ a changed function, other users of a field it uses, and users of an existing member that a new one is
371
+ named like (a new `EmergencyQueues` beside `EntryQueues`). Each of those lines is either right to
372
+ leave alone or a missing task. So is the last such list, which comes from git history rather than the map:
373
+ files that usually change with the files the tasks touch and that no task names (see
374
+ [Change coupling](#change-coupling)). Reviewers (the `leyline-adversarial-review` skill, one run
375
+ for logic and one for performance) file findings with node ids as evidence; only the person
376
+ resolves them (`leyline spec resolve <finding> accepted|rejected|deferred "why"`). A reviewer that passes
377
+ its kind (`--reviewer`, or `reviewer` on the `spec_review_facts` tool) is recorded as having run, so the page
378
+ can say a review found nothing rather than that none ran.
379
+
380
+ After implementation and a re-index, `verify` (inside `check`) marks each task from the graph diff and each scenario
381
+ from its test's recorded result, lists edits outside the spec, new links between modules and rules
382
+ newly broken, and appends the result to `leyline.md`. It exits 0 only when the change was done as
383
+ agreed. A new function that only code named in the spec calls is listed as a helper, not as an edit
384
+ outside the spec.
385
+
386
+ Each task and scenario gets one verdict, shown in the tables, counted under them ("7 proven, 1 needs
387
+ you, 1 inconclusive"), and given as `verdict` and `verdict_why` in the MCP `check` answer:
388
+
389
+ | Verdict | Task | Scenario |
390
+ | --- | --- | --- |
391
+ | proven | the code it names changed | its test passes, and failed or did not exist before, or passed both times while running the changed code |
392
+ | partial | some of the code it names changed, some did not | some results that carry its name pass, some fail |
393
+ | contradicted | the code it names did not change (or what it adds is missing) while other code did | its test fails after the change |
394
+ | inconclusive | the map cannot place the code it names, or nothing changed at all | no pass or fail was recorded for it |
395
+ | needs a person | it names no code (docs, say) | its test passes, but does not reach the changed code, or Leyline cannot tell whether it does |
396
+
397
+ A task that removes code is judged by what is left, since edited is not removed: "Remove `X`" is contradicted while
398
+ `X` still exists, partial when `X` is gone but code that called it still does ("`X` is gone but `caller` still
399
+ calls it"), and proven only when it is gone and uncalled. "Rename `X` to `Y`" also needs `Y` there.
400
+
401
+ Partial, contradicted and inconclusive hold up "done as agreed"; an item that needs a person is listed
402
+ and does not. A project can change that in `openspec/leyline.toml`, committed beside the changes:
403
+
404
+ ```toml
405
+ [check]
406
+ blocking = ["contradicted", "inconclusive", "partial", "needs a person"] # every item proven by the code
407
+ ```
408
+
409
+ A tick in `tasks.md` never clears an item: the agent ticks tasks as it goes.
410
+
411
+ A spec can change part-way. Once the code has moved on, `brief` keeps the picture of the code from
412
+ the first brief, so `verify` still compares with the code as it was; `--new-baseline` starts over. The code has moved
413
+ on when any source file on the map has changed: a constant at the top of a module counts, and so, to be safe, does a
414
+ comment.
415
+ That picture is `.leyline/snapshots/<change id>.db`: one per change, taken once, holding only what the
416
+ comparison reads (nodes and their hashes, links, flows, a hash per source line) and what a diagram of the code
417
+ as it was needs (the line of each call, the order of each flow, each channel link's channel). It stays after the
418
+ change is checked done as agreed, so `check` can run again after a later edit, and is deleted with
419
+ `leyline spec forget <id>` or once the change folder is archived or removed (a folder that a local branch not checked
420
+ out still holds is not removed: it is back when that branch is). A baseline deleted any other way (by hand, or with
421
+ the snapshots folder) is lost, not forgotten: `plan` will not quietly take the code as it is now as the start, which
422
+ after the change is implemented would leave `check` nothing to find, and asks for `--new-baseline` to take a new one.
423
+ `leyline map` writes `.leyline/.gitignore`, so the store stays out of git without touching your own `.gitignore`.
424
+
425
+ `skills/leyline-spec/SKILL.md` tells an agent how to write the folder and run the loop.
426
+
427
+ ### Spec drift
428
+
429
+ Specs name code in backticks, and the code moves on without them: a function is renamed, a method gets
430
+ a parameter, a class moves to another file. `leyline drift` reads every backticked name in the living
431
+ specs (`openspec/specs/<capability>/spec.md`) and in finished changes (`openspec/changes/archive/<date>-<id>/`),
432
+ compares each with the map, and prints one page, spec by spec:
433
+
434
+ ```
435
+ ## openspec/specs/engine/spec.md
436
+
437
+ - `Engine.start` has changed signature since the spec was written: was `def start(self)`, now `def start(self, loud=True)`.
438
+ - `core.make_engine` is gone: py/src/pkg/core.py has no `make_engine` now.
439
+ - `Journal.note` has moved: it is now in py/src/pkg/journal.py (was in py/src/pkg/core.py).
440
+ ```
441
+
442
+ A name is *gone*, *renamed*, *moved* (another file or owner), *signature changed*, *ambiguous* (it now names
443
+ several things) or *changed inside* (same signature; worth a read, not drift by itself). It exits 1 when something
444
+ is gone, renamed or changed signature. *Renamed* is a gone name with one new node beside it (same owner, same file
445
+ or module) that has its body without the name, or its declaration without the name when git or the change's
446
+ baseline shows the node is new; or a file git records as renamed: "`Engine.shout` has been renamed to
447
+ `Engine.yell`". Names that are not code (`true`, `GET`) are counted and left alone, and so is
448
+ code a task removes or renames.
449
+
450
+ To see a changed signature, Leyline must know what a name meant when it was right. When `check` finds a change
451
+ done as agreed, it records an *anchor* for each code name in the change's tasks and spec deltas: the node,
452
+ its kind and file, and fingerprints of its declaration (a function's signature line, a type's declaration and
453
+ member names, a field's type) and of its body. Anchors are kept in the store (table `spec_anchors`) and in
454
+ `openspec/leyline-anchors.json`: canonical JSON, sorted keys, keyed by change id (or `specs/<capability>`),
455
+ with node ids written without the repository's id so another clone reads them. Commit it with the specs; it
456
+ survives a fresh map, and wins over the store. A living spec's name is read against its own anchors, or those
457
+ of the change that wrote it. A name with no anchor is checked by name only. When the specs and the code agree,
458
+ `leyline drift --accept` records the code as it is now; code that is gone stays reported until the spec stops
459
+ naming it.
460
+
461
+ `leyline plan` reads the same anchors: a new change whose tasks touch drifted code lists, under "Specs that no
462
+ longer match code this change touches", lines such as "The living spec engine/spec.md names `Engine.start`,
463
+ which has changed signature since it was written."
464
+
465
+ ### Assessing a change
466
+
467
+ 1. You describe a change to an agent that has the Leyline MCP server connected.
468
+ 2. The agent finds the nodes the description refers to and calls `propose_change`.
469
+ 3. Leyline walks callers, interface links, channels and flows from those nodes and stores the result
470
+ as a draft proposal with a saved view.
471
+ 4. You refresh the map and open the Views tab: the change, what must be edited with it, what it
472
+ reaches, the tests to run and the risks.
473
+
474
+ `skills/leyline-change-impact/SKILL.md` tells an agent how to do steps 2 and 3 well. The same
475
+ `save_view` tool lets an agent save any other slice of the code as a view.
476
+
477
+ ### Reviewing a pull request
478
+
479
+ A spec says what a change will do before it is written. A pull request comes the other way round: the code
480
+ exists, and what it was meant to do is a title and a paragraph. `leyline pr` reads the change from the code:
481
+
482
+ ```
483
+ gh pr checkout 123
484
+ leyline pr main --about "what the pull request says it does" # or: leyline pr --github 123
485
+ ```
486
+
487
+ It maps the commit the branch left `main` at (from `git archive` into a temporary folder, so the repository
488
+ and its worktrees are not touched; files that did not change keep their parse output, so this costs a few
489
+ seconds), compares it with the checkout, uncommitted edits included, and writes
490
+ `.leyline/reviews/pr-<id>.md`:
491
+
492
+ - **What changed**: functions edited, added and removed, a changed parameter list as it was and as it is,
493
+ and the files the map does not read (docs, styles, data), so you know where its view stops.
494
+ - **What it reaches and did not change**: callers of a changed signature that were not edited, removed code
495
+ that is still called, and each channel the edit touches (an edited line within a few lines of the route,
496
+ table, event or program it names) with every other end that must agree. A channel the changed function
497
+ sits on but the edit does not touch is left out.
498
+ - **Shares a caller, a field or data with the change**: weaker leads, for a reviewer to read.
499
+ - **Usually changes with what it changed, and it did not change**: from the history before the branch (see
500
+ [Change coupling](#change-coupling)).
501
+ - **Tests**: whether it edits any, which changed code no test reaches, and which tests run it.
502
+ - New dependencies between modules, rules that now fail, and the repository's own rules for reviewers
503
+ (`AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md` ...) to review against.
504
+
505
+ With no `--about`, the commits' messages stand for the description. The review is stored as `pr-<id>` (the
506
+ pull request's number, else the branch name), and everything that takes a change id takes it: `leyline spec
507
+ facts pr-123 --reviewer logic` gives the adversarial reviewers their facts, `leyline spec finding pr-123 ...`
508
+ files one, and the page lists them. A finding whose evidence is nowhere near the change (not on its blast
509
+ radius, nor one call from it) is kept but marked, so the person questions it first. `leyline spec forget
510
+ pr-123` deletes the base's map.
511
+
512
+ `leyline pr` exits 0 whatever it finds. With `--gate` it exits 1 while something that blocks is left, so it
513
+ can hold up a merge in CI. The page opens with a **Gate** section: whether it passes, what blocks under which
514
+ config, one line for each thing that blocks, and a `Next:` line saying what to do about the first. The MCP
515
+ `review_pr` answer carries the same lines as `blocking`, and `gate_passed`. Each run judges the gate again from
516
+ the code at the checkout and the findings as they stand, so a commit that fixes a caller, or a finding the person
517
+ resolves, clears it on the next run.
518
+
519
+ By default four things block, the ones the map shows are broken: a caller of a changed signature that was not
520
+ edited, removed code that is still called, a confirmed error-level rule (see [Rules](#rules)) that now fails and
521
+ did not at the base, and an open high finding. A line that rests only on a link the map guessed by name never
522
+ blocks; the Gate section counts those so you can read them. A project picks its own set in
523
+ `openspec/leyline.toml`, as it does for `check`:
524
+
525
+ ```toml
526
+ [pr]
527
+ blocking = ["unedited-callers", "still-called", "failing-rules", "open-high-findings"] # the default
528
+ ```
529
+
530
+ The kinds are `unedited-callers`, `still-called`, `failing-rules`, `open-high-findings`, `open-medium-findings`
531
+ (medium or high), `open-findings` (any severity), `other-ends` (the other end of a channel the edit changed, not
532
+ edited) and `untested` (changed code no test on the map reaches). `blocking = []` lets everything through.
533
+
534
+ In CI, check the pull request's head out with enough history to find where it left the base:
535
+
536
+ ```yaml
537
+ - uses: actions/checkout@v4
538
+ with:
539
+ fetch-depth: 0
540
+ - run: pip install leyline-code
541
+ - run: leyline pr origin/${{ github.base_ref }} --gate
542
+ ```
543
+
544
+ ### A quick change
545
+
546
+ A spec folder is too much for "make the retry count 3". `leyline quick` gives the same three answers with no folder:
547
+
548
+ ```
549
+ pytest -rA | leyline quick "make the retry count 3" --about RETRIES fetch --tests -
550
+ ... edit ...
551
+ pytest -rA | leyline quick --done quick-make-the-retry-count-3 --tests -
552
+ ```
553
+
554
+ Before the edit it looks up the named code as a task's names are looked up (`--about`, or names in backticks in the
555
+ sentence; a constant the map has no node for is found where it is set, with the functions that read it), assesses what
556
+ it reaches, records the test run, keeps a baseline, and prints a short page: what it will touch, what must be edited
557
+ with it, the channels it touches or is reached across, the tests that run it, and the command to run when done.
558
+
559
+ After the edit, `--done` maps the code again and compares it with the baseline as `leyline pr` compares a branch with its
560
+ base. One line gives the verdict, then four items, each proven, partial, contradicted, inconclusive or needs a person:
561
+ the edits stayed in the named code (a new helper only it calls, and a caller updated to match a changed signature,
562
+ count as part of it), no caller was left broken, no test broke against the run from before, and a test that passed ran
563
+ the changed code. It exits 0 when nothing that blocks is left (by default partial, contradicted and inconclusive; the
564
+ project's `openspec/leyline.toml` applies). An edit that belongs but was not named is named afterwards, keeping the
565
+ baseline: `leyline quick --done <id> --about <name>`.
566
+
567
+ When the change has grown past quick (more than three functions edited, a channel crossed, or a caller that must
568
+ change left as it was), the page says so and gives the way into a spec: write `openspec/changes/<id>/`, run
569
+ `leyline quick --to-spec <id> quick-<slug>` to hand it this baseline and the test run from before, then `leyline plan
570
+ <id>` and `leyline check <id>`.
571
+
572
+ The change is stored as `quick-<slug>`, and the review steps take it as they take `pr-<id>`: `leyline spec facts
573
+ quick-<slug> --reviewer logic`, `leyline spec finding`, `leyline spec findings`, `leyline affected-tests` and
574
+ `leyline spec forget quick-<slug>` (which deletes its baseline). The MCP tool is `quick`.
575
+
576
+ Run it again after new commits and the page opens with **Since the last review (`<sha>`, `<when>`)**: the functions
577
+ edited, added and removed between the two heads (not since the base), the facts that are new (a caller newly broken)
578
+ and those that are gone, and each open finding marked "may be fixed" (the code its evidence names changed since it
579
+ was filed: re-check it) or "still applies" (do not file it again). Each run is kept in the store with its head, its
580
+ time and its facts, and a slim map of the code at that head (`.leyline/snapshots/pr-<id>.head-<n>.db`, the last
581
+ three and any an open finding was filed against); running it again on the same code changes nothing. The reviewers'
582
+ facts carry the same as `since_last_review`.
583
+
584
+ Both `leyline pr` and `leyline plan` list **Earlier changes to this code**: up to five finished OpenSpec changes
585
+ (archived, or checked) and earlier pull request reviews that touched the same functions or types, newest first, with
586
+ the names they share; the facts carry them as `related_changes`. A repository with no such history gets the commits
587
+ before the change that changed the same files instead (of the last 500 that touched them), most overlap first.
588
+ "Checked" means its latest check found it done as agreed: a check that fails after one that passed makes it unfinished
589
+ again, and so does planning it again (it is then reopened; the store keeps each check's time and verdict).
590
+
591
+ ### Learning from rejected findings
592
+
593
+ Many review findings that people reject are correct about the code but miss a choice made on purpose. So
594
+ when you reject a finding and say why (`leyline spec resolve <finding> rejected "why"`), Leyline keeps a
595
+ learning: your reason in your words, the finding's claim, its kind of review, and the code it is about (the
596
+ evidence nodes, and the type, file and module each is in). A rejection with no reason keeps nothing.
597
+
598
+ Learnings go in a file in the repository, so you commit them, the team shares them, and a fresh map keeps
599
+ them: `openspec/leyline-learnings.json` when the repository has an `openspec` folder, else
600
+ `.leyline-learnings.json` at its root. A file stays where it was first made. It is JSON with sorted keys
601
+ and a two-space indent, one entry per learning: `id`, `status` (active or retired), `reviewer`, `claim`,
602
+ `reason`, `scope`, `fingerprint` (the code it is about, see below), `source` (the change and finding it
603
+ came from), `created`, `confirmed` (when a person last said it holds for the code as it is), `hits`,
604
+ `dismissals`, `accepted`, and `findings` (the later findings it matched, with what people decided). Node
605
+ ids in it leave out the repository's id, so a clone in a folder of another name reads them. In a workspace of
606
+ several repositories each keeps its own file, and a learning applies only to code in its own repository.
607
+
608
+ - **Reviewers read them first.** The facts for a spec or a pull request (`leyline spec facts`, or
609
+ `spec_review_facts`) start with `learnings_that_apply`: active learnings about code the change touches or
610
+ reaches, closest first.
611
+ - **A repeat is marked, not dropped.** A new finding of the same kind of review, on the same node, type or
612
+ file, whose claim shares at least 45% of its words with a learning's claim (60% when only the module is
613
+ shared), comes back with `learned`, naming the learning and its reason. The page shows it as "Matches a
614
+ past decision: ..." and the person still decides it. Words are compared loosely: lower case, code names
615
+ split (`parseArgs` and `parse_args` are one name, `app.use.count` is `count`), the names of the code both
616
+ claims are about left out (they say where, not what), common words dropped, endings cut, words a review
617
+ uses for the same thing made one (about forty groups, such as argument and parameter, remove and delete,
618
+ null, None and undefined, caller and call site; `SYNONYMS` in `learnings.py`), and words found in nearly
619
+ every claim (file, read, value) counted half.
620
+ - **A wrong learning retires itself.** Each later decision on a finding it matched is counted. Once people
621
+ have accepted at least two of them, and more than they rejected, the learning is retired and the file
622
+ says why. It is also retired if the finding it came from is later marked anything but rejected.
623
+ - **A learning about code that has changed says so.** A learning keeps a fingerprint: for each evidence
624
+ node, the hash the map keeps of that node's own lines (trimmed, so moving or re-indenting it does not
625
+ count; the file's hash for a node with none). Each time a learning is used, in `learnings_that_apply`,
626
+ in `learned` on a new finding, on the page and in `leyline learnings`, it is compared with the map. If a
627
+ node was edited or is gone, the learning is `stale` and names them in `edited` and `gone`. It still
628
+ applies and still marks findings, and is not retired. The page says "Matches a past decision, but the
629
+ code it was about has changed since: ..." and asks whether it still holds. That is the person's call:
630
+ `leyline learnings confirm <id>` takes the fingerprint again for the code as it is now, and
631
+ `leyline learnings retire <id>` ends it. A learning kept before Leyline recorded fingerprints has none.
632
+ Whether its code changed is unknown, not stale, and it says so; confirming it gives it one.
633
+
634
+ `leyline learnings` lists them, and exits 1, naming the file, when a learnings file cannot be read;
635
+ `leyline learnings retire <id> "why"` retires one by hand; `leyline learnings confirm <id>` says one still holds for
636
+ the code as it is now.
637
+
638
+ ### How it runs: sequence diagrams
639
+
640
+ `leyline.md` (under "What code will be written", and again after `check`) and the pull request page carry a
641
+ Mermaid sequence diagram of the changed code, which GitHub draws from the ```` ```mermaid ```` block: how execution
642
+ reaches it from the nearest entry point (along the stored flow, in source order), and what it calls. Participants
643
+ are types, or the file for a function at the top of a file. Every arrow is an edge on the map: a call, a call through
644
+ an interface into an implementation, or a channel link, drawn with an open arrowhead (`-)`) and named (`http GET
645
+ /api/x`, `writes table orders, which load() reads later`); a link the map guessed by name is dotted. The changed
646
+ code is shaded. A diagram keeps to about 8 participants and 25 arrows, and says how many calls it left out.
647
+
648
+ After a change, the check page and the pull request page show two diagrams, **Before** and **After**, then list what
649
+ changed in how it runs: calls and channel links into or out of the changed code, added and removed since the
650
+ baseline. The Before diagram is drawn from the baseline alone, never from the map as it is now, and is shown only
651
+ when every one of its arrows is a link the baseline holds (`diagrams.unbacked(baseline, d)`; given the map, it
652
+ checks a diagram of the code as it is). A baseline taken by an older Leyline kept which pairs were linked but not in
653
+ what order: it still compares, and the code as it was is listed, not drawn. Keeping the order made a baseline of
654
+ Parlance about 22% larger (8.2 MB to 10.0 MB).
655
+
656
+ ### Reviewing a change after it is made
657
+
658
+ The first `propose_change` of a change keeps a baseline in `.leyline/snapshots/<change id>.db` (what the
659
+ comparison reads, not a copy of the store). Once the change is implemented:
660
+
661
+ ```
662
+ <run the tests> > before.txt # before the change, on the old code
663
+ leyline record-tests before before.txt
664
+ ... implement ...
665
+ <run the tests> > after.txt
666
+ leyline index .
667
+ leyline record-tests after after.txt
668
+ leyline review <change id> --before before --after after
669
+ ```
670
+
671
+ `review` compares the snapshot with the graph as it is now and reports:
672
+
673
+ - which predicted edits were made, which edits were not in the proposal, and which predicted edits
674
+ did not happen. A method whose parameters changed gets a new id; it is paired with the old one
675
+ by owner and name, so it counts as one edited method.
676
+ - links between modules that are new or gone, and the flows whose path changed
677
+ - rules that fail now and held before
678
+ - tests that fail now and passed before
679
+
680
+ It saves the result as a view, which the map shows under Views as "Review: ...".
681
+ `record-tests` reads one `PASS name` or `FAIL name: message` line per test; an agent can pass
682
+ results in any other format through the `record_test_run` tool.
683
+
684
+ ### Rules
685
+
686
+ A rule is a constraint on the graph, checked with no inference: `forbid` (nothing in A may use
687
+ anything in B), `no_cycle` (between modules or systems), `must_be_tested` (every function in A is on
688
+ some test's path). Selectors are `module:Name`, `system:Name`, `external:Name`, `path:prefix`,
689
+ `id:prefix` and `*`. A rule an agent adds is stored as suggested until you confirm it with
690
+ `leyline rules --confirm ID`. `leyline rules` checks all of them and exits 1 when a confirmed
691
+ error-level rule fails, so it can run in CI. A `forbid` rule on an external package sees imports
692
+ (`using`, `import`), not fully qualified names used inline.
693
+
694
+ ### Exact references
695
+
696
+ The syntax resolvers below work without a compiler and are sometimes wrong. Where a compiler's view
697
+ is available, `leyline index` lets it overrule them (`--exact auto`, the default; `--exact off` to
698
+ skip):
699
+
700
+ - **C#**: if `dotnet` is on the PATH, a small program built against the Roslyn that ships inside
701
+ the .NET SDK binds every module and reports each call and field access it resolved. It needs no
702
+ package restore. A module whose packages are missing still binds whatever refers to source in the
703
+ repository; the rest stays as the syntax pass left it. The first run builds the program into
704
+ `~/.cache/leyline` (about ten seconds).
705
+ - **Other languages**: a SCIP index, from `--scip FILE` or `index.scip` in the repository root.
706
+ Tested with scip-python. Run the indexer in an environment where the package resolves to the
707
+ source tree (an editable install), or references through the installed copy are lost.
708
+
709
+ A link the compiler confirms is stored as `exact`. A link it contradicts is removed, but only where
710
+ it bound every call of that name in the function (C#) or bound that name to something else on the
711
+ same line (SCIP, which does not say what it failed to bind). Links it found and the syntax pass
712
+ missed are added. `overview` reports the counts under `exact:roslyn` and `exact:scip`.
713
+
714
+ ### Measured coverage
715
+
716
+ Flows are static: what a test can reach. `leyline coverage FILE` imports what did run:
717
+
718
+ - coverage.py's data file. With `pytest --cov=<package> --cov-context=test`, each function is tied
719
+ to the tests that ran it.
720
+ - Cobertura XML (coverlet, `coverage xml`). A function ran or did not; no per-test detail.
721
+ - Istanbul's `coverage-final.json` (vitest `--coverage.reporter=json`, jest `--coverageReporters=json`). It covers a
722
+ whole run, so run one test file at a time and import each with `--test <that file>` (`test` on the `coverage`
723
+ tool): each function is then tied to the test file that ran it, not to the one test inside it.
724
+ - lcov (`lcov.info`, from c8, vitest or jest with the `lcov` reporter, and most other tools). Lines (`DA:`) and
725
+ functions (`FN:`, `FNDA:`) that ran; records under a `TN:` test name are tied to that test, and a file with no test
726
+ names is a whole run, imported with `--test` as Istanbul's is. Paths may be absolute, relative or Windows-style.
727
+
728
+ #### Scenarios proven by what ran
729
+
730
+ A scenario's test passing says the behavior holds; it does not say the test ran the code the change edited. Pass the
731
+ coverage of the same run to `check`, and each scenario also says whether its test ran the changed code:
732
+
733
+ ```
734
+ pytest -rA --cov=src --cov-context=test > after.txt
735
+ leyline check <change> --tests after.txt --coverage .coverage
736
+ ```
737
+
738
+ Each scenario result gets `ran_changed_code` (true, false, or null when it cannot be told) and a one-line
739
+ `ran_changed_code_note`. A test that passed without running the changed code is listed under **Weaker proof**: it
740
+ would pass whatever the change did. With no per-test coverage, `check` reads as it always has.
741
+
742
+ #### The tests a change needs
743
+
744
+ `leyline affected-tests <change>` (the `affected_tests` tool) lists the tests to run for a planned change or a
745
+ `pr-<id>` review, each with why, and prints a command that runs them (`pytest path::test ...`, `npx vitest run
746
+ <files>`, `npx jest --runTestsByPath <files>`, `go test -run`). With per-test coverage it takes the tests measured running the changed
747
+ or must-edit code, the change's own new tests, and, from the map, tests that reach changed code no measured test ran
748
+ or that the measured run left out; without it, the tests whose path on the map passes through the change. Feed that
749
+ smaller run to `check`. `leyline pr` lists the tests measured running the changed code the same way.
750
+
751
+ With coverage imported, a test's flow dims the steps that did not run and lists what ran without
752
+ being on its path (reached through a link the map does not have, such as a framework calling back
753
+ into the code). A change assessment adds the tests that were measured running the changed code, and
754
+ `must_be_tested` counts measured functions as tested. Only a function's body counts: the `def` line
755
+ runs when the file loads. An import outlives a re-index; `leyline coverage` flags one taken at
756
+ another commit.
757
+
758
+ ### Field reads and writes
759
+
760
+ Each function is linked to the fields it reads and the fields it assigns (`reads` and `writes` edges,
761
+ with a count and the first line). `expand` on a field lists its readers and writers; on a function,
762
+ the fields it touches; on a type, each field with how many functions set and read it and which
763
+ other types set it. `leyline state` (the `shared_state` tool) ranks the fields assigned from outside
764
+ the type that declares them: the mutable state with no single owner.
765
+
766
+ - An assignment is `x.f = v`, `x.f += v`, `x.f++`, `out`/`ref x.f` and `x.f[i] = v`. A value set
767
+ while creating an object (`new Foo { f = 1 }`) is recorded as construction and left out of the
768
+ shared-state ranking, as are constructors, subclasses and test code.
769
+ - A call on the field to a method that changes a collection in place (`list.Add(x)`, `items.append(x)`,
770
+ `queue.push(x)`) is a read and a write. The methods are named in a short list per language (`MUTATORS` in
771
+ each adapter); any other method call on a field is a read, since the map cannot tell what it does.
772
+ - Properties count as fields. Enum members and events do not. In Python, reading an attribute that a
773
+ getter computes (`@property`, or a decorator that is a descriptor class, such as Werkzeug's
774
+ `cached_property`) is also a call to the getter; on `self`, so is a subclass's getter of that name.
775
+ - In C#, a field on a receiver of unknown type is linked by name only when one field has that name,
776
+ and marked as a guess. In Python there is no such guess: attribute names repeat too often.
777
+ - A change assessment whose target is a field marks every reader and writer, and `review` reports
778
+ field access that is new.
779
+
780
+ ### Design patterns
781
+
782
+ Every index run looks for nine shapes in the graph and labels what it finds, with the nodes that
783
+ play each role, a sentence saying why, and a confidence:
784
+
785
+ | Pattern | The shape that is matched |
786
+ |---|---|
787
+ | strategy | an abstraction with two or more implementations, held in a field by another type that calls it |
788
+ | decorator | a type that implements an abstraction and holds one more of it |
789
+ | composite | a type that implements an abstraction and holds a collection of it |
790
+ | template method | a concrete method on a base type that calls methods its subclasses override |
791
+ | observer | an event with a raiser and subscribers in other code |
792
+ | factory | a function that creates two or more types sharing a supertype (higher confidence when it returns that supertype) |
793
+ | builder | a type with chaining methods that return itself and a `Build` or `Create` that returns something else |
794
+ | singleton | a static field of the type's own type and no public constructor |
795
+ | process boundary | one program launching another and talking over its pipes |
796
+
797
+ A label says the code has the shape. It does not say the author meant the pattern, and a pattern
798
+ built another way is not found. Labels in tests, samples and benchmarks are kept apart. An agent that
799
+ has read the code can add a label the matchers missed with `label_pattern`; it goes stale when the
800
+ code behind it changes.
801
+
802
+ ### Tours
803
+
804
+ A tour is an ordered list of stops, each pointing at something on the map (a module, a function, a
805
+ flow, a pattern, a saved view) with a few sentences on what to notice. `leyline index` writes an
806
+ orientation tour from the graph alone: what the repository is, where it starts, the modules from the
807
+ most depended-on outward, the main abstractions and boundaries, one test path worth tracing, how it
808
+ is tested, and what the map cannot see. Every sentence in it is a count or a name the store can
809
+ back. An agent can write further tours with `save_tour`; `skills/leyline-tour/SKILL.md` says how.
810
+
811
+ ### Change coupling
812
+
813
+ Some files have to change together with nothing in the code to say so: a doc and the rule it describes, a JSON
814
+ schema and its reader, a test fixture, the other side of a protocol. Git history shows them. `leyline coupling
815
+ <file>` lists what usually changed in the same commits as a file, and `leyline coupling` alone the most coupled
816
+ pairs:
817
+
818
+ ```
819
+ $ leyline coupling tooling/validate.py
820
+ `tooling/validate.py` changed in 12 commits (from the last 206 commits, leaving out 17 that changed more than 50
821
+ files). What usually changed with it:
822
+
823
+ - files in `editor/core/src/validation/`: 7 of 12 (58%)
824
+ - `tooling/conformance/validator/build_cases.py`: 7 of 12 (58%)
825
+ - files in `tooling/conformance/validator/cases/`: 7 of 12 (58%)
826
+ - `tooling/scripts/mutation_probe.py`: 6 of 12 (50%)
827
+ ```
828
+
829
+ That is the rule a contributor's guide states in words (a validator rule changes both validators, adds a
830
+ conformance case and a mutant), found from history alone.
831
+
832
+ A file is listed when it changed together with the first in at least 3 commits and in at least half of the first
833
+ file's commits (`--min-together`, `--min-confidence`). A folder is listed when its files did, though no one file
834
+ did (a rule that comes with a new test case each time), unless it is at the top of the repository. Read: the last
835
+ 1,000 commits within two years, merges and commits that changed more than 50 files left out, renames followed.
836
+ It is worked out once per commit and kept in the store. `plan` lists the files that usually change with the files
837
+ the tasks touch and that no task names, and `pr` those that usually changed with what the branch changed and that
838
+ it did not; `leyline spec facts` has the whole list.
839
+
840
+ The same is worked out by function, for the functions the tasks name (in `plan`) or the branch edited (in `pr`), up
841
+ to 6 of them: "`spec.py.verify` changed in 8 of the 9 commits that changed `spec.py.brief`". A past commit's line
842
+ numbers say which function they were in only against the file as it was then, so this reads, for each function, the
843
+ last 20 commits that changed its lines (`git log -L`), parses its file as it was at each with the same parser the map
844
+ uses, and puts each changed line in the innermost function around it; then reads the same way the 4 other files that
845
+ changed in most of those commits. The same thresholds apply (3 commits, half of them). Which functions a commit
846
+ changed in a file is kept in the store, so only the first plan pays for it (about a second on this repository; a
847
+ function renamed or moved counts only from then).
848
+
849
+ ### An outline for an agent
850
+
851
+ `leyline context <focus...> [--tokens N]` (the `context` tool) gives an agent about to edit some code a short outline of
852
+ the code around it, cut to a token budget (2,000 by default, 200 to 8,000; a token is counted as four characters):
853
+
854
+ ```
855
+ $ leyline context registerLoreRoutes --tokens 1000
856
+ Code around registerLoreRoutes: declarations, the most related files first; > marks the focus.
857
+
858
+ editor/host/src/lore-routes.ts
859
+ const relOf = (params: unknown): string
860
+ -- called by registerLoreRoutes
861
+ > function registerLoreRoutes(server: FastifyInstance, opts: { index: () => ReferenceIndex }): void
862
+ -- answers GET /api/lore-files, GET /api/usages/:type/:id; called from 1 place, calls 12
863
+
864
+ editor/host/src/server.ts
865
+ async function buildServer()
866
+ -- answers GET /api/types, ... and 29 more; calls registerLoreRoutes
867
+ ...
868
+ editor/client/src/lib/loreApi.ts
869
+ const loreApi = { ... }
870
+ listFiles: ()
871
+ -- requests GET /api/lore-files
872
+ Shown: 30 symbols in 17 files.
873
+ Left out: 1 more symbol linked directly to the focus and 422 two links away, in 120 files; the nearest: ...
874
+ ```
875
+
876
+ The focus is node ids, names (`Owner.method`), file paths, a change (`spec-<id>`, `pr-<id>`: the code its plan or review
877
+ marked) or words to search for. Every function, type, field and test is ranked by personalized PageRank from the focus
878
+ over calls, channel links, type use and containment, each way, with a small boost for code a test reaches and code an
879
+ entry point runs first. The outline shows declaration lines, not bodies, file by file, most related file first: the focus
880
+ marked `>`, each channel end in words ("answers GET /api/x", "writes table t"), and what calls the focus or what it calls
881
+ said so. The last lines say how much was left out and name the nearest of it. The ranking visits only code near the focus,
882
+ and the graph is read once per map run: on Parlance (160,000 lines) the first call takes 0.3 s and later ones 0.05 s.
883
+
884
+ ### Asking how something works
885
+
886
+ Leyline holds no language model; an agent asks it in words and reasons over what comes back. Two commands (and the
887
+ tools of the same names) make "how does a vehicle get spawned" or "what happens when a writer saves a dialogue" a
888
+ question with an answer on the map, and the `leyline-explain-flow` skill drives them:
889
+
890
+ ```
891
+ $ leyline find-flows "what happens when a writer saves a dialogue in the editor"
892
+ 1. DialogueScriptEditor.save [UI event handler] editor/client/src/surfaces/DialogueEditor/DialogueScriptEditor.tsx:338
893
+ matched: saves in its name; dialogue in the name of what it is in; editor in the name of what it is in
894
+ 2. DialogueCanvas.onInspectorSave [UI event handler] editor/client/src/surfaces/DialogueEditor/DialogueCanvas.tsx:764
895
+ ...
896
+ $ leyline explain-path DialogueCanvas.save
897
+ Walk from DialogueCanvas.save: 40 of 154 steps shown.
898
+ 1. DialogueCanvas.save (editor/client/src/surfaces/DialogueEditor/DialogueCanvas.tsx:230)
899
+ 2. call -> useAppStore.saveEntity (editor/client/src/store.ts:754)
900
+ at editor/client/src/surfaces/DialogueEditor/DialogueCanvas.tsx:233: return saveEntity(updated, label, ...
901
+ ...
902
+ 17. http PUT /api/entities/:type/:id -> the PUT /api/entities/:type/:id handler (editor/host/src/server.ts:361)
903
+ crosses to another process or service
904
+ ...
905
+ 31. call -> scheduleValidation (editor/host/src/validation.ts:161)
906
+ ```
907
+
908
+ `find-flows` ranks every function and test by the words it shares with the description: in its name, its route, the
909
+ test's name, the name of the type or component it sits in, its file's name and folders, its language, and the comment
910
+ just above it (or a Python docstring). Code names are split (`saveDialogue` is save and dialogue), endings are cut
911
+ (saves, saved, saving), a short table makes words that mean the same in code one (save, write, persist, store, put;
912
+ create, add, spawn; run, execute, play, start; about twenty groups, `SYNONYMS` in `explain.py`), a word rare on the
913
+ map counts for more, and a word for who does it (a user, a writer) counts little. Route handlers, UI event handlers,
914
+ program entries, message handlers and commands are ranked up, then tests. Each candidate says why it matched and which
915
+ flows start there; one where none starts names the entry points' flows that reach it (`reached_from`) and the tests'
916
+ (`tests_reaching`). `ambiguous` is set when the first ones score close in different files, so the agent
917
+ asks the person which they mean. The words index is built once per map run (about a second on Parlance) and kept: a
918
+ later question takes a few milliseconds.
919
+
920
+ `explain-path <start>` walks from a function, a route (`"PUT /api/x"`), a test or a flow id. Alone, it follows the
921
+ stored flow that starts there, or walks the same way now (depth first, in the order the code makes its calls, 8 calls
922
+ deep at most), and shows the shallow steps first and every hop across a channel, up to `--steps` (40); leaves called
923
+ from 8 or more places are counted as helpers, not shown. `--to` gives the shortest path (without file, table and key
924
+ hops when there is one), `--through` the path through a node and on from it. Each step says how it was reached (a
925
+ call, `http PUT /x`, `starts <program>`, a message, an interface's implementation), the calling line and the callee's
926
+ declaration line, and `later, elsewhere` for a table, file or key it writes that other code reads later. The walk ends
927
+ with what the map could not see: guessed links, interfaces, the depth limit, and a program a type in the walk started
928
+ and talks to over its pipes (the messages are not calls the map can follow, so it names the program to walk next).
929
+ Its diagram has one arrow per step, each a link on the map. `leyline diagram <ids>` draws the usual sequence diagram
930
+ for any functions or types.
931
+
932
+ ### Finding your way around a large module
933
+
934
+ On a large repository a module is still huge: Parlance's `editor/client` holds 249 files and the map draws it as one
935
+ box. `leyline outline <module>` (the `module_outline` tool) splits it into at most twelve parts, largest first, and
936
+ says for each what it holds and how it connects. With no module it lists the repository's modules. Here is
937
+ `editor/core/src` after an agent named its parts:
938
+
939
+ ```
940
+ $ leyline outline parlance:dir:editor/core/src
941
+ Core library (folder, editor/core/src/): 67 files, 669 functions, 20,380 lines.
942
+ The project model and every engine on it: storage, validation, explore, play, prose, review, export.
943
+ Split by folders and groups of files. 11 of 12 parts have a name.
944
+
945
+ 1. Storage and rename (projectStorage.ts group) [system]: 7 files, 104 functions, 3,098 lines
946
+ id: parlance:dir:editor/core/src#system:projectStorage.ts
947
+ Canonical JSON in and out, the one atomic write path, loading a project, and entity-id renames.
948
+ entry points: none; 103 of 104 functions on a test's path
949
+ uses: editor/runtime (25), Validation rules (8), Validator (8), Review threads and localization (6), ...
950
+ used by: Core tests (496), editor/host (175), editor/mcp (64), editor (16), editor/client (10), projectDiff.ts (6)
951
+ channel: file from editor/host, 5 links (e.g. threads/*.json)
952
+ busiest: stringify (887 flows), parse (753 flows), ownValue (738 flows)
953
+ key types: StorageContext, RenamePlan, EntityType, RenameInput
954
+ risky: busy: its functions are on the most flows
955
+ 2. Validation rules (validation/): 9 files, 78 functions, 2,850 lines
956
+ ...
957
+ 12. 12 more parts [more]: 18 files, 85 functions, 2,565 lines
958
+ ```
959
+
960
+ A part is a folder when the folder tree means something; a folder that holds nearly all of a module is shown instead
961
+ of it (`editor/client` is shown from `editor/client/src`), with the few files beside it as parts of their own. Where a
962
+ folder is flat, its files are grouped by Louvain community detection over calls, type use and inheritance between
963
+ them, each file going to the group that holds most of its code; when few of them link to each other (a folder of
964
+ tests), they are grouped by the code they use instead. A module that is one flat folder reuses the systems the map
965
+ proposed. A level of more than twelve parts shows the eleven largest and a `@more` part holding the rest. A file or a
966
+ group of one file is a leaf: its key functions, most flows through them first.
967
+
968
+ For each part: its size; its entry points (routes, UI handlers and components, programs, commands, message handlers)
969
+ and tests, and how many of its functions are on a test's path; what it uses and what uses it (its siblings, the
970
+ folder next to it, other modules), as counted calls and type links; the channels that cross its edge; its busiest
971
+ functions (most flows through them) and the types other code leans on most; and `risky` beside its siblings: the
972
+ busiest, the most depended on, and those few tests reach. `--depth 2` (the default) also lists the parts inside each
973
+ part; drill in with a part's id. Ids hold across maps: `<repo>:dir:<path>` for a folder, `<path>#files` for the loose
974
+ files beside its folders, `<path>#system:<anchor>` for a group (named after its most linked type or file), a file's or
975
+ a module's node id for those.
976
+
977
+ An agent names a part after reading its code: `name_part(part_id, name, summary, evidence)`, or `leyline name-part
978
+ <part id> "Validation rules" --summary "..." --evidence <node ids>` (`--intent` when the person said it). Names are
979
+ kept in the store (table `part_names`) with the part's files at the time, so they survive a re-map: a group whose
980
+ anchor changed takes the name of the old group of its folder it shares most files with, and a name whose part kept
981
+ less than half its files, or most of whose evidence is gone, is shown as "may be stale". Outlines, `overview` (module
982
+ titles and `named_parts`), `explain_path` (each step says the named part it enters) and the map page show the names.
983
+ Naming a system the map proposed also sets its `name` and `responsibility`.
984
+
985
+ The `leyline-explore-module` skill walks an agent through it: outline the module, read each part's key code, name
986
+ it, and give the person a one-screen guide (the parts, how they connect, where to start reading for common tasks,
987
+ which parts are risky), offered as a tour.
988
+
989
+ ## The map
990
+
991
+ `view` and `export` open the same page. It has up to four zoom levels:
992
+
993
+ 1. **Modules.** One box per module, with an arrow for each dependency and a count of the links behind it.
994
+ 2. **Inside a module.** A module of 30 files or more is drawn as its parts, as `leyline outline` splits it (see
995
+ [Finding your way around a large module](#finding-your-way-around-a-large-module)), with their names and sizes;
996
+ open a part for the parts inside it, then its types and files. A smaller module shows one box per system where
997
+ it was split, otherwise one per type or file.
998
+ 3. **Inside a system.** One box per type, or per file where functions sit outside any type.
999
+ 4. **Inside a type or file.** One box per function, with the outside callers and callees around it.
1000
+
1001
+ Select a box or an arrow to see detail in the side panel: members, callers, callees, the links that
1002
+ make up an arrow, and source text. Double-click a box to open it. Solid arrows are exact, dashed
1003
+ arrows were worked out from syntax, dotted arrows are guesses, and blue dash-dot arrows are events and
1004
+ process launches.
1005
+
1006
+ The Matrix tab shows the same level as a dependency matrix, ordered so that cycles appear above the
1007
+ diagonal. The Flows tab steps through one flow at a time, with the source of the selected step beside it.
1008
+ The Views tab shows saved views: for a proposed change, the marked code colored by role beside the
1009
+ impact report.
1010
+
1011
+ The exported page embeds the graph and the source text of every indexed file, so share it with the
1012
+ same care as the repository. Pass `--no-sources` to leave source text out.
1013
+
1014
+ ## MCP tools
1015
+
1016
+ | Tool | Returns |
1017
+ | --- | --- |
1018
+ | `map(paths?)` | Step 1: index the code (or map again what the store holds); counts, the map page, `next` |
1019
+ | `plan(change, test_output?, test_results?, new_baseline?)` | Step 2: the one page for a change, `status.blocking`, `next`; records the tests from before; `new_baseline` compares from the code as it is now |
1020
+ | `check(change, test_output?, test_results?, coverage_path?)` | Step 3: re-index, record the tests from after, the verdict and `next`; with the run's coverage, whether each scenario's test ran the changed code |
1021
+ | `affected_tests(change)` | The tests to run for a change or a `pr-<id>` review, and the command that runs them |
1022
+ | `overview(scope?, limit?)` | Repos, modules with sizes, module-to-module dependencies by edge kind, systems, external packages, extractors that ran |
1023
+ | `cross_repo(limit?)` | In a workspace of several repositories: links between them, functions most called across, flows that cross and come back |
1024
+ | `search(text, kind?, limit?)` | Node ids matching a name, qualified name or path |
1025
+ | `expand(node_id, limit?)` | One node in detail: contents, dependencies, dependents, callers and callees |
1026
+ | `neighbors(node_id, direction?, kinds?, limit?)` | Raw edges around a node, by kind |
1027
+ | `source(node_id, max_lines?)` | The node's source text |
1028
+ | `context(focus, budget_tokens?)` | A short outline of the code around a focus (ids, names, paths, a change, or words), most related first, cut to a token budget |
1029
+ | `module_outline(module?, depth?)` | A large module split into at most 12 parts (folders, or groups of files where a folder is flat): each part's size, entry points, links in and out, channels, busiest functions, risks and name; drill in with a part's id |
1030
+ | `name_part(part_id, name, summary?, evidence?, layer?)` | Name a part from the outline with a one-line summary; kept across maps, shown as "may be stale" when the part changes a lot |
1031
+ | `flows(kind?, through?, limit?, offset?)` | Flows walked from each entry point and test; `through` keeps flows that pass a node |
1032
+ | `flow(flow_id, max_steps?, offset?)` | One flow step by step, in source order, with call depth |
1033
+ | `trace(from_id, to_id)` | The shortest chain of calls and channels between two functions |
1034
+ | `find_flows(description, limit?)` | Where a behavior described in words could start: entry points, route, UI and message handlers, tests named for it, functions; each with why it matched, its kind, file:line and the flows that start there, or else the entry points' flows (`reached_from`) and the tests' (`tests_reaching`) that reach it |
1035
+ | `explain_path(start, to?, through?, max_steps?)` | An ordered walk across calls and channels from `start` (its main flow, the shortest path `to` a node, or the path `through` one), each step with how it was reached, the calling line and the declaration; data written for later marked; a Mermaid diagram of the walk |
1036
+ | `diagram(ids)` | A Mermaid sequence diagram of how execution reaches some functions or types and what they call |
1037
+ | `impact(node_id, max_depth?, limit?)` | What can reach a node: callers by module and the flows through it |
1038
+ | `annotate(node_id, key, value, evidence?, confidence?, layer?)` | Write an inferred or intent statement about a node |
1039
+ | `propose_change(intent, targets, title?, depth?)` | Assess a change without an OpenSpec folder and save its blast-radius view |
1040
+ | `save_view(title, narrative, marks, legend?)` | Save any set of marked nodes as a view |
1041
+ | `review_change(change_id, before_run?, after_run?)` | Compare a change assessed with `propose_change` with what was done, and save a review view |
1042
+ | `record_test_run(run, results)` | Store one test run under a label, for `review_change` |
1043
+ | `add_rule(kind, selector_from, selector_to?, edge_kinds?, severity?, reason?, confirmed?)` | Add an architecture rule, suggested unless the user stated it |
1044
+ | `check_rules()` | Evaluate every rule against the graph |
1045
+ | `review_pr(base?, about?, github?, review_id?, path?)` | Review a checked-out branch or pull request with no spec: what changed, what it reaches and did not change, tests; returns `change_id` (`pr-<id>`), `blocking` (one line for each thing that holds up the merge) and `gate_passed` |
1046
+ | `quick(what?, names?, done?, test_output?, test_results?, coverage_path?, change_id?)` | A small change with no spec folder: before (`what`, `names`), what it touches and the tests that run it; after (`done`), one verdict, and `grown` when it needs a spec |
1047
+ | `spec_review_facts(change, reviewer?)`, `spec_finding(change, reviewer, severity, claim, evidence, proposal?)`, `spec_findings(change)`, `spec_resolve(finding_id, status, resolution?)` | Adversarial review of a planned change, or of a pull request by its `pr-<id>` |
1048
+ | `learnings(retire?, why?, confirm?)` | Past decisions on review findings, kept from rejections with a reason, each marked `stale` when its code changed since; `retire` one the person says no longer holds, `confirm` one they say still holds for the code as it is now |
1049
+ | `spec_brief(change, new_baseline?)`, `spec_verify(change, before_run?, after_run?)` | The steps inside `plan` and `check`, one at a time; rarely needed |
1050
+ | `drift(path?, accept?)` | Code the living specs and finished changes name that is gone, moved, changed signature or ambiguous; `fails`, the page, `next` |
1051
+ | `shared_state(scope?, limit?)` | Fields assigned from outside the type that declares them |
1052
+ | `coupling(path?, min_together?, min_confidence?, limit?)` | Files (and folders) that usually change in the same commits as a file, from git history; with no path, the most coupled pairs |
1053
+ | `coverage(node_id?, flow_id?, import_path?, test?)` | Measured coverage: what ran, set against the static paths; `import_path` reads a report in first, and `test` names the one test file that ran for a report with no per-test detail |
1054
+ | `patterns(pattern?, node_id?, include_tests?, limit?)` | Design patterns found by shape, with roles, rationale and confidence; `include_tests` adds those inside test code |
1055
+ | `label_pattern(pattern, roles, rationale, confidence?)` | Record a pattern the matchers missed |
1056
+ | `tours()`, `tour(tour_id)` | List tours, or read one stop by stop |
1057
+ | `save_tour(title, stops, audience?)` | Save a tour written for the user |
1058
+ | `views()`, `view(view_id, limit?)` | List saved views, or read one |
1059
+ | `skills(skill?)` | The skills that ship with Leyline, each with when to use it, or one skill's text. Each is also an MCP prompt of the same name |
1060
+
1061
+ `change` is a change folder or its id. A tool that cannot answer returns an error saying what to do
1062
+ instead (no store yet: call `map`; an unknown id: find it with `search`). Each answer is at most about
1063
+ 24,000 characters (30,000 for `module_outline` and `explain_path`, 36,000 for `context`): lists show their
1064
+ first items and their total, `cut` names any list that was shortened, and `more` says how to see the rest
1065
+ (`limit`, `offset`, `scope`, or a narrower id).
1066
+
1067
+ ## What is indexed
1068
+
1069
+ ### Which files
1070
+
1071
+ The files git lists: tracked ones, and untracked ones `.gitignore` does not exclude. A directory that is
1072
+ not a git repository (or one git refuses to read, such as a checkout owned by another user) is walked
1073
+ instead, leaving out what git would: the patterns of your global excludes file (`core.excludesFile`, else
1074
+ `~/.config/git/ignore`), of `.git/info/exclude` and of each `.gitignore`, escapes and trailing spaces read as
1075
+ git reads them, so a file kept out of git such as `.env` stays off the map and its page. Some files are left out, and `map` names them with
1076
+ the reason: other people's code (`node_modules`, a Go or Composer `vendor`), submodules and nested
1077
+ repositories, symlinks to directories or out of the repository, files git lists that are gone, and source
1078
+ files that are binary, minified, unreadable or larger than 5 MB (`LEYLINE_MAX_FILE_MB` changes that). A
1079
+ file that does not parse is kept on the map as a file, without its contents, and named too.
1080
+
1081
+ Large repositories are parsed in worker processes (`LEYLINE_JOBS` sets how many). A worker that crashes
1082
+ or is still on one batch of files after ten minutes (`LEYLINE_PARSE_TIMEOUT`, in seconds) costs only
1083
+ the file it was on. Workers start by spawn on macOS and Windows and by fork on Linux;
1084
+ `LEYLINE_START_METHOD` picks one. The compiler step (`--exact`) gives up after half an hour
1085
+ (`LEYLINE_EXACT_TIMEOUT`) and keeps the syntax-based links.
1086
+
1087
+ ### Languages
1088
+
1089
+ Any language with a tree-sitter grammar is indexed by one **generic adapter** that knows no
1090
+ language. It reads what grammars have in common: node names (`function_declaration`,
1091
+ `class_definition`, `call_expression`), the grammar's own tags query where it ships one, and how
1092
+ typed languages write a variable's type (`Foo x`, `x: Foo`, `x = new Foo(`). From that it gets
1093
+ declarations, nesting, calls with what the text shows of the receiver, imports matched to files by
1094
+ path, tests by naming convention, and `main`. Adding a language is one line in
1095
+ `adapters/generic.py` and a `pip install tree-sitter-<language>`.
1096
+
1097
+ Installed by default: C#, Python, TypeScript and JavaScript. `pip install 'leyline-code[languages]'`
1098
+ adds Go, Rust, Java, Kotlin, Swift, C, C++, Ruby, PHP, Scala, Lua, Bash and GDScript.
1099
+
1100
+ C#, Python and TypeScript also have **hand-written adapters** that see more: receiver types,
1101
+ overloads, field reads and writes, events, routes. They are used for those languages unless
1102
+ `LEYLINE_GENERIC=1`. For other languages, a **SCIP index** (from the language's own indexer,
1103
+ `--scip FILE`) replaces the generic adapter's guesses with the compiler's links.
1104
+
1105
+ `leyline grade <repo> <index.scip | roslyn>` measures either adapter against a compiler. Precision
1106
+ counts only call sites the compiler resolved; recall counts the compiler's links between functions
1107
+ on the map.
1108
+
1109
+ | Language (repository) | Compiler | Hand-written: right / found | Generic: right / found |
1110
+ | --- | --- | --- | --- |
1111
+ | TypeScript (Parlance, 160k lines) | scip-typescript | 100% / 98% | 99% / 97% |
1112
+ | Python (Flask) | scip-python | 99% / 68% | 96% / 87% |
1113
+ | C# (a private project) | Roslyn | 99.7% / 97% | 98% / 83% |
1114
+
1115
+ Where the generic adapter loses, it is on calls made on a variable whose type it cannot read from
1116
+ the text. The hand-written Python adapter misses calls inside decorators (`@app.route(...)`).
1117
+
1118
+ | Record | Precision | Notes |
1119
+ | --- | --- | --- |
1120
+ | Modules, files, types, callables, fields, entry points | exact | A module is a directory with a project file, or a source file's own directory |
1121
+ | `contains`, `has_field`, `exposes` | exact | |
1122
+ | `imports`, `depends_on` | exact | From `using` and `import` statements and from `.csproj` references |
1123
+ | `extends`, `implements`, `uses_type`, `instantiates` | heuristic | Type names resolved by name, limited to what the project can reference |
1124
+ | `calls` | heuristic or guess | See below |
1125
+ | `overrides` | heuristic | A method to the interface or base method it implements, by name and arity |
1126
+ | `communicates` | heuristic or guess | Events and process launches; see Channels |
1127
+ | Tests | exact | xUnit, NUnit and MSTest attributes, pytest names, and inline runners of the form `Run("name", () => { ... })` |
1128
+
1129
+ ### How calls are resolved
1130
+
1131
+ Without a compiler, a call is linked only when its receiver's type can be worked out from what is
1132
+ written: fields, parameters, typed locals, `new` expressions, collection element types, static type
1133
+ names, and the declared return type of the call a value came from (`a.Make().Run()`, `var x =
1134
+ a.Make(); x.Run()`). In C#, extension methods are matched on the type of their `this` parameter,
1135
+ and overloads of equal length are narrowed by the arguments: a lambda's parameter count, the type
1136
+ of a literal, a `new` expression or a typed local, and explicit type arguments. In Python, packages
1137
+ under a source root are imported by their own name, with or without an `__init__.py` (a namespace
1138
+ package), names re-exported through `__init__.py` are followed, and a test parameter filled by a pytest
1139
+ fixture takes the fixture's return type. In TypeScript and JavaScript, an import through the `paths` of
1140
+ the nearest `tsconfig.json` or `jsconfig.json` (`@ui/*`) is the local file, not a package; `extends` is
1141
+ not followed. Three outcomes are counted per run:
1142
+
1143
+ - **resolved**: linked to a callable in the workspace
1144
+ - **external**: the receiver's type is outside the workspace, or nothing in the workspace has that name
1145
+ - **unresolved**: the name exists in the workspace but the receiver's type is unknown
1146
+
1147
+ A call on a receiver of unknown type is linked by name only when exactly one declaration of that
1148
+ name is visible and the name was never seen on an outside type. In Python it is linked only to a method, and
1149
+ never by a name the built-in types have (`add`, `get`, `close`, `split`). Those links are stored with
1150
+ precision `guess`, not `heuristic`, and the map draws them differently.
1151
+
1152
+ ### Channels
1153
+
1154
+ A `communicates` edge records code talking to code without a direct call. It points the way data
1155
+ moves, and carries a `channel` and an `address`.
1156
+
1157
+ - **event** (C#): from the function that raises an event to the function that handles it. The
1158
+ address is the event. Subscriptions to events declared outside the workspace are counted, not linked.
1159
+ - **process** (Python): from a `subprocess` call to the entry point of the program it launches, when
1160
+ the command names a project, a build output or a script in the workspace. If only the surrounding
1161
+ file names it, the edge is a `guess`.
1162
+
1163
+ - **http**: from a request to the route that serves it. Routes are read from decorators
1164
+ (`@app.route("/x")`, `@app.get`), ASP.NET attributes (`[HttpGet("x")]`) and `MapGet`-style calls;
1165
+ requests from `.get("/x")`-style calls and `HttpClient` methods with a literal path. A request is
1166
+ linked to a route declared inside the same test first, then to the only route in the repository
1167
+ that matches. When several match, in every language, the route that names more of the path outright
1168
+ wins (`/items/special` over `/items/{id}`, `/api/review/health` over `/api/review/:id`); if that leaves
1169
+ more than one, it is counted as ambiguous and left unlinked.
1170
+ In TypeScript (Fastify, Express, Hono, Koa routers), a route's inline handler
1171
+ (`server.get("/api/x", async (req) => ...)`, `server.route({ method, url, handler })`, or
1172
+ `server.get(path, { schema }, handler)`) is a function of its own, named for its route (`GET /api/x`)
1173
+ and nested in the function that registers it: the request lands on it, what it calls is its own, and
1174
+ the registrar registers it (kept as a call, so startup still reaches it). A handler given by name
1175
+ (`server.get("/x", listThings)`) is that function. When several handlers' routes still fit a request
1176
+ after that, a request with no method written is taken as a GET. In a pull request, an edit inside a handler touches
1177
+ its route and no other. An ASP.NET
1178
+ controller's `[Route("api/[controller]")]`, and a minimal API's `MapGroup("api/x")`, start the
1179
+ routes declared under them. A path handed to
1180
+ something the name does not give away is linked only when exactly one route serves it: a wrapper
1181
+ (`apiFetch("/api/x")`, `api_get("/api/x/1")`, `Get<T>("/api/x")`), `server.inject({ url })`, a
1182
+ session's `request("GET", "/x")`, a test client's `open("/x", method=...)`, a base address in a
1183
+ variable (`f"{BASE}/api/x"`, `BASE + "/api/x"`), `HttpClient` with a relative path
1184
+ (`GetAsync("api/x")`), RestSharp's `new RestRequest("api/x")` and `new HttpRequestMessage(...)`.
1185
+ - **file**: from a function that writes a file to one that reads it, when the path fragments written
1186
+ in the two agree: the same file name, or the same directory and the same extension
1187
+ (`policies/*.bin`). These are always a `guess`, since paths are usually built at run time. Flows
1188
+ do not follow them: writing a file does not run its reader.
1189
+
1190
+ - **di**: from a method of an interface to the same method of the implementation a container
1191
+ registers for it (`services.AddScoped<IFoo, Foo>()` and `typeof` forms, Autofac's
1192
+ `RegisterType<Foo>().As<IFoo>()`, Python `container.register(IFoo, Foo)` and `bind(IFoo).to(Foo)`,
1193
+ Angular and NestJS `{ provide: X, useClass: Y }`, Inversify `bind<X>(…).to(Y)`), with
1194
+ `registered_in` and `registered_at` naming the registration. An implementation read off a factory
1195
+ (`sp => new Foo()`, `sp.GetRequiredService<Foo>()`) is a `guess`. Also from the registration to a
1196
+ hosted service's `ExecuteAsync`, and from a FastAPI endpoint to the function its `Depends()` names.
1197
+ Flows try the registered implementation before the other implementations.
1198
+ - **queue**: from code that publishes to the handlers of the same topic, queue or message type:
1199
+ Kafka, RabbitMQ, Redis, NATS-style `publish`/`subscribe` with a literal or constant name, Node
1200
+ `emit`/`on`, NestJS `@EventPattern`/`@OnEvent`, graphql-subscriptions, Celery and RQ tasks
1201
+ (`send_receipt.delay()` reaches `send_receipt`), Django signals, and C# messages by type (MediatR
1202
+ `Send(new X())` and `AddDomainEvent` to `IRequestHandler<X>`/`INotificationHandler<X>`, and the
1203
+ like). A message type that is created with a handler in sight but not visibly sent (handed to an
1204
+ outbox or a wrapper) is linked as a `guess` with `created_only`. Event names that streams and
1205
+ sockets raise themselves (`error`, `data`, `exit`) are not linked.
1206
+ - **db**: from a function that writes a table to one that reads it: SQL in string literals and
1207
+ constants, SQLAlchemy and SQLModel, Django managers, EF `DbSet`s and generic repositories
1208
+ (`IRepository<Order>`), Prisma, Knex, TypeORM, Mongoose, Sequelize and document stores
1209
+ (`db.users.find`). Table names come from `__tablename__`, `[Table]`, `ToTable`, `@Entity` and the
1210
+ like, or the class name. A match only after folding singular and plural (`user ~ users`) is a
1211
+ `guess`. Sites in tests and migrations are counted, not linked. Flows do not follow these.
1212
+ - **rpc**: from a call on a gRPC or Thrift stub (`GreeterStub`, `Greeter.GreeterClient`,
1213
+ `getService('X')`) to the method of the same name on a class that implements the service
1214
+ (`GreeterServicer`, `Greeter.GreeterBase`, `@GrpcMethod`). When several servers implement it, the
1215
+ ones nearest the caller by directory are linked.
1216
+ - **format** (TypeScript, Python, C#): from a function that builds keys of one shape to a function
1217
+ that takes them apart, where no call joins the two. The address is the shape
1218
+ (`dialogue/*/nodes/*/text`). A builder is a template with holes and two or more fixed parts
1219
+ between `/` or `:` separators, starting with a fixed part: a template literal, an f-string, an
1220
+ interpolated string, `"%s/x/%s" %`, `String.Format` or a chain of `+`. A hole that calls another
1221
+ builder (`${nodeKey(d, n)}/text`) takes that builder's shape. A reader is a regular expression over
1222
+ the shape (where a constant holding it is used), a `startsWith` test of a literal with two fixed
1223
+ parts, or code that compares a split key's pieces by position (`parts[2] === "nodes"`, through
1224
+ `slice`, aliases and destructuring). They are linked when two or more of the reader's fixed parts
1225
+ sit at the same places in the builder's shape and none disagrees; when the pieces come from a
1226
+ parameter and two shapes share a tail, the reader is taken to read the shape its own file builds,
1227
+ else the one most files build. Routes, file paths, git's `refs/...` and keys built in tests are
1228
+ left out. A `guess` unless three fixed parts agree. Like files and tables, flows do not follow
1229
+ these, and a pull request that edits the builder's template lists the readers as "must agree".
1230
+
1231
+ All five meet by a name or a shape written in code, so each link is `heuristic` at best. Each change brief
1232
+ lists the channels a change crosses, with their address.
1233
+
1234
+ ### Systems
1235
+
1236
+ A module is split into systems by Louvain community detection over calls, type use and inheritance
1237
+ between its units: each top-level type, and each file for the functions that sit outside any type. A
1238
+ module with fewer than 12 units holding code, or one that does not split cleanly (modularity under
1239
+ 0.3), is left whole. Each proposed system is named after its most connected unit (a type, or a file)
1240
+ until something better is written with `annotate`, using the keys `name` and `responsibility`.
1241
+
1242
+ The grouping is deterministic. The names are not: they belong to the inferred layer.
1243
+
1244
+ ### Annotations
1245
+
1246
+ `annotate` is the only write. An inferred annotation must list the node ids it is based on; the
1247
+ store hashes the files behind them and marks the annotation stale when any of those files changes.
1248
+ An intent annotation is the user's own statement and needs no evidence. Facts cannot be written.
1249
+
1250
+ ### Flows
1251
+
1252
+ A flow is a walk of the call graph from one entry point or one test, depth-first, in source order,
1253
+ listing each function once. Flows follow calls, calls through an interface into its implementations
1254
+ (the registered one first), process launches, HTTP requests, messages to their handlers, remote calls,
1255
+ and events whose handler was subscribed earlier in the same flow. They do not follow files or tables:
1256
+ writing one does not run its reader. They stop at
1257
+ 8 calls deep or 300 steps.
1258
+
1259
+ A flow's id is `flow:<start>`. A start that is two kinds of start at once (an entry point that is also a test)
1260
+ has a flow for each: the entry point's keeps that id, and the other's is `flow:<start>#test`.
1261
+
1262
+ Flows are static: they show what can run, not what did run. Per-test coverage will replace them with
1263
+ observed paths where it is available.
1264
+
1265
+ SCIP indexers will replace these heuristics with exact references. That extractor and test coverage
1266
+ are recorded as `not_analyzed` in `extractor_coverage`, so missing analysis is never mistaken for an
1267
+ empty result.
1268
+
1269
+ ### How well this holds up
1270
+
1271
+ Measured on three repositories, counting call sites whose target is inside the repository:
1272
+
1273
+ | Repository | Files | Index time | Call sites linked | Of those, by guess | Left open |
1274
+ |---|---|---|---|---|---|
1275
+ | A private project (C# and Python) | 59 | 1 s | 99% | 4% | 18 |
1276
+ | Flask (Python) | 83 | 1 s | 91% | 9% | 101 |
1277
+ | Polly (C#) | 801 | 7 s | 94% | 2% | 915 |
1278
+
1279
+ With the compiler pass on, the syntax links can be scored. On the private project the compiler confirmed 2,081
1280
+ of them, removed 16 and added 38 it had missed. On Polly, where packages could not be restored and
1281
+ only part of the code binds, it confirmed 4,844 and removed 1,286, nearly all extra overloads of
1282
+ the right method.
1283
+
1284
+ Without that pass, "linked" is not "correct". In a random sample of 42 links across the
1285
+ three, read against the source by hand, all 42 pointed at the right method; 4 of the 22 from Polly
1286
+ pointed at the wrong overload of it. Polly's fluent API has up to twenty overloads per name, which
1287
+ is the hard case for this approach. A call the indexer classes
1288
+ as leaving the repository is not in these counts, and some of those are misses: a call on a value
1289
+ whose type comes from outside (Flask's test client) cannot be followed back in.
1290
+
1291
+ ## Identity
1292
+
1293
+ Node ids are stable across file moves within a project:
1294
+
1295
+ - C#: `repo:csharp:Project::Namespace.Type.Member(ParamTypes)`. The project is part of the id
1296
+ because two projects may declare the same type name.
1297
+ - Python: `repo:python:package.module.Class.method`
1298
+ - TypeScript, JavaScript and the generic languages: `repo:language:dir.file.Name`, the file named without its
1299
+ extension. Two files of one language in one directory that differ only in their extension (`a.js` beside
1300
+ `a.ts`, `m.c` beside `m.h`) would name their contents alike: the first by path keeps that id, and the others
1301
+ keep their extension in it (`repo:typescript:dir.a.ts.Name`). Adding or removing such a neighbour can change
1302
+ the ids of the files after it.
1303
+ - Files and modules: `repo:file:path` and `repo:module:path`
1304
+
1305
+ A name defined twice in one file (`def load()` in both branches of an `if` or a `try`, a property's getter and
1306
+ setter) is one node: which definition runs is not known from the text. Its span runs from the first definition
1307
+ to the last, its `definitions` attribute lists each one's lines, its text hash reads every body, and its calls
1308
+ come from all of them.
1309
+
1310
+ ## Tests
1311
+
1312
+ ```bash
1313
+ pytest
1314
+ ```
1315
+
1316
+ A new skill is a folder under `skills/` with a `SKILL.md`. That folder is the only copy: the wheel carries it as
1317
+ `leyline/skills/`, and `leyline skills`, the MCP prompts and the `skills` tool pick it up with no other edit.
1318
+ `tests/test_skills.py` checks each skill's frontmatter and that the wheel's copy matches.
1319
+
1320
+ ## License
1321
+
1322
+ Copyright (C) 2026 Matthew Burke.
1323
+
1324
+ Leyline is free software, licensed under the GNU General Public License, version 3.
1325
+ See [LICENSE](LICENSE) for the full text. It comes with no warranty.