aspark-graph 0.7.0__tar.gz

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 (66) hide show
  1. aspark_graph-0.7.0/.gitignore +21 -0
  2. aspark_graph-0.7.0/LICENSE +21 -0
  3. aspark_graph-0.7.0/PKG-INFO +314 -0
  4. aspark_graph-0.7.0/README.md +293 -0
  5. aspark_graph-0.7.0/pyproject.toml +70 -0
  6. aspark_graph-0.7.0/src/aspark_graph/__init__.py +3 -0
  7. aspark_graph-0.7.0/src/aspark_graph/artifacts.py +365 -0
  8. aspark_graph-0.7.0/src/aspark_graph/build.py +417 -0
  9. aspark_graph-0.7.0/src/aspark_graph/cli.py +204 -0
  10. aspark_graph-0.7.0/src/aspark_graph/confinement.py +116 -0
  11. aspark_graph-0.7.0/src/aspark_graph/extractors/__init__.py +36 -0
  12. aspark_graph-0.7.0/src/aspark_graph/extractors/base.py +62 -0
  13. aspark_graph-0.7.0/src/aspark_graph/extractors/code_go.py +153 -0
  14. aspark_graph-0.7.0/src/aspark_graph/extractors/code_java.py +74 -0
  15. aspark_graph-0.7.0/src/aspark_graph/extractors/code_py.py +98 -0
  16. aspark_graph-0.7.0/src/aspark_graph/extractors/code_rust.py +162 -0
  17. aspark_graph-0.7.0/src/aspark_graph/extractors/code_ts.py +118 -0
  18. aspark_graph-0.7.0/src/aspark_graph/git.py +103 -0
  19. aspark_graph-0.7.0/src/aspark_graph/graph.py +156 -0
  20. aspark_graph-0.7.0/src/aspark_graph/inference.py +124 -0
  21. aspark_graph-0.7.0/src/aspark_graph/model.py +89 -0
  22. aspark_graph-0.7.0/src/aspark_graph/parse_cache.py +161 -0
  23. aspark_graph-0.7.0/src/aspark_graph/queries.py +417 -0
  24. aspark_graph-0.7.0/src/aspark_graph/server.py +123 -0
  25. aspark_graph-0.7.0/tests/conftest.py +100 -0
  26. aspark_graph-0.7.0/tests/fixtures/code/sample.py +20 -0
  27. aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/plan.md +15 -0
  28. aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/qa-report.md +19 -0
  29. aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/release-notes.md +13 -0
  30. aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/review-report.md +14 -0
  31. aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/spec.md +34 -0
  32. aspark_graph-0.7.0/tests/fixtures/sample_repo/src/demo/app.py +10 -0
  33. aspark_graph-0.7.0/tests/fixtures/sample_repo/src/demo/util.py +2 -0
  34. aspark_graph-0.7.0/tests/test_artifacts.py +129 -0
  35. aspark_graph-0.7.0/tests/test_build.py +73 -0
  36. aspark_graph-0.7.0/tests/test_build_bounds.py +177 -0
  37. aspark_graph-0.7.0/tests/test_cli_mcp_parity.py +156 -0
  38. aspark_graph-0.7.0/tests/test_confinement.py +179 -0
  39. aspark_graph-0.7.0/tests/test_confinement_cli_mcp.py +197 -0
  40. aspark_graph-0.7.0/tests/test_confinement_guards.py +127 -0
  41. aspark_graph-0.7.0/tests/test_extractor_go.py +129 -0
  42. aspark_graph-0.7.0/tests/test_extractor_java.py +82 -0
  43. aspark_graph-0.7.0/tests/test_extractor_rust.py +183 -0
  44. aspark_graph-0.7.0/tests/test_extractor_ts.py +58 -0
  45. aspark_graph-0.7.0/tests/test_gate_health.py +43 -0
  46. aspark_graph-0.7.0/tests/test_git.py +100 -0
  47. aspark_graph-0.7.0/tests/test_graph.py +74 -0
  48. aspark_graph-0.7.0/tests/test_headline.py +62 -0
  49. aspark_graph-0.7.0/tests/test_impact.py +59 -0
  50. aspark_graph-0.7.0/tests/test_impact_diff.py +56 -0
  51. aspark_graph-0.7.0/tests/test_incremental.py +492 -0
  52. aspark_graph-0.7.0/tests/test_incremental_bench.py +101 -0
  53. aspark_graph-0.7.0/tests/test_inference.py +127 -0
  54. aspark_graph-0.7.0/tests/test_integration_docs.py +427 -0
  55. aspark_graph-0.7.0/tests/test_link_conventions.py +49 -0
  56. aspark_graph-0.7.0/tests/test_mcp_errors.py +27 -0
  57. aspark_graph-0.7.0/tests/test_mcp_transport.py +109 -0
  58. aspark_graph-0.7.0/tests/test_navigation.py +81 -0
  59. aspark_graph-0.7.0/tests/test_packaging.py +60 -0
  60. aspark_graph-0.7.0/tests/test_readme.py +57 -0
  61. aspark_graph-0.7.0/tests/test_security_doc.py +216 -0
  62. aspark_graph-0.7.0/tests/test_smoke.py +7 -0
  63. aspark_graph-0.7.0/tests/test_staleness.py +55 -0
  64. aspark_graph-0.7.0/tests/test_story_trace.py +60 -0
  65. aspark_graph-0.7.0/tests/test_story_trace_inference.py +40 -0
  66. aspark_graph-0.7.0/tests/test_walking_skeleton.py +94 -0
@@ -0,0 +1,21 @@
1
+ # aspark-graph read model (disposable, rebuildable)
2
+ .aspark-graph/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .venv/
10
+ .venv-test/
11
+ build/
12
+ dist/
13
+
14
+ # graphify knowledge-graph output (disposable, rebuildable)
15
+ graphify-out/
16
+
17
+ # uv
18
+ uv.lock merge=binary
19
+
20
+ # macOS
21
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andreas Lottes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.4
2
+ Name: aspark-graph
3
+ Version: 0.7.0
4
+ Summary: A lean, local code-and-artifact knowledge graph that speaks SPARK.
5
+ Author: Andreas Lottes
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: aspark,code-graph,knowledge-graph,mcp,tree-sitter
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: mcp<1.20,>=1.12
11
+ Requires-Dist: networkx>=3.2
12
+ Requires-Dist: tree-sitter-go==0.25.0
13
+ Requires-Dist: tree-sitter-java==0.23.5
14
+ Requires-Dist: tree-sitter-python==0.25.0
15
+ Requires-Dist: tree-sitter-rust==0.24.2
16
+ Requires-Dist: tree-sitter-typescript==0.23.2
17
+ Requires-Dist: tree-sitter==0.26.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ <picture>
23
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/a-lottes/aSPARK-graph/main/docs/aSPARK-graph-logo-dark.png">
24
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/a-lottes/aSPARK-graph/main/docs/aSPARK-graph-logo-light.png">
25
+ <img alt="aSPARK-graph logo" src="https://raw.githubusercontent.com/a-lottes/aSPARK-graph/main/docs/aSPARK-graph-logo-light.png" width="480">
26
+ </picture>
27
+
28
+ # 🕸️ aspark-graph
29
+
30
+ > **A lean, local knowledge graph that joins a repo's _code_ to its _delivery artifacts_ — so agents and humans can trace a user story to the code that implements it, and see the story-level blast radius of a change.**
31
+
32
+ aspark-graph reads one repository — its source code **and** its
33
+ [aSPARK](https://github.com/a-lottes/aSPARK) `.spark/` delivery trail (specs,
34
+ plans, reviews, QA reports) — and builds a single queryable graph, served over a
35
+ CLI **and** an MCP server. It is **deterministic** (tree-sitter + declared
36
+ artifact links; **no LLM, no network**) and **disposable** (the graph is a
37
+ rebuildable read model, never a source of truth).
38
+
39
+ ---
40
+
41
+ ## The two questions it exists to answer
42
+
43
+ Everything else is in service of these:
44
+
45
+ | Question | Tool | Plain meaning |
46
+ |---|---|---|
47
+ | *"Which code implements this user story, and did its acceptance criteria pass QA?"* | `story_trace US-2` | Follow story → ACs → plan tasks → code → QA results, with zero grepping. |
48
+ | *"If I change these files, which stories and acceptance criteria are in the blast radius — what must QA re-verify?"* | `impact src/foo.py` | Walk code → tasks → stories/ACs, tagging each hit with how trustworthy the link is. |
49
+
50
+ ## Why this exists
51
+
52
+ When an AI agent (or the developer supervising it) works on an aSPARK-managed
53
+ repo **too large to hold in your head**, those two questions are exactly the ones
54
+ aSPARK's own review and QA gates depend on — and today the only way to answer
55
+ them is `Grep`/`Glob` plus reading `.spark/` files by hand.
56
+
57
+ The spec → plan → review → QA trail *is* machine-parseable, and it *is* linked to
58
+ code by intent. But nothing joins the two, so an agent re-derives the link every
59
+ time it greps: **slowly, incompletely, and non-reproducibly.** aspark-graph
60
+ computes that join once, deterministically, and lets you query it.
61
+
62
+ It does this by deliberately doing **less** on the code side than a general code
63
+ graph, and adding the one thing general graphs don't have: the **delivery
64
+ artifacts**. That artifact layer is what makes story tracing, gate-aware impact,
65
+ and orphan detection possible at all.
66
+
67
+ ## When to use it — and when not
68
+
69
+ - ✅ **Use it** on an aSPARK repo big enough that "read every relevant file"
70
+ isn't viable, when you need a fast, reproducible answer *before* opening files.
71
+ - ❌ **Skip it** on a repo small enough to hold in your head (just read the
72
+ files), or a repo with no `.spark/` artifacts (the artifact layer is the whole
73
+ point).
74
+ - 🤝 **Want a broad semantic code graph too?** Run
75
+ [Graphify](https://github.com/safishamsi/graphify) alongside it — different
76
+ scope, no conflict. aspark-graph is an accelerant for aSPARK, not a replacement
77
+ for a code-search tool.
78
+
79
+ **Using aspark-graph in aSPARK gates?** See
80
+ [`docs/aspark-integration.md`](https://github.com/a-lottes/aSPARK-graph/blob/main/docs/aspark-integration.md)
81
+ for drop-in `CLAUDE.md` blocks that wire the `/peer-review` and `/demo-day`
82
+ gates to the query tools.
83
+
84
+ **Trust boundary, non-guarantees, and how to report a vulnerability:** see
85
+ [`SECURITY.md`](https://github.com/a-lottes/aSPARK-graph/blob/main/SECURITY.md)
86
+ — read it before treating this server's output as anything other than data.
87
+
88
+ ---
89
+
90
+ ## The graph model (read this to interpret any result)
91
+
92
+ The graph is a typed, directed multigraph. Every node id is **stable and
93
+ deterministic**, derived only from content and location, so two builds of an
94
+ unchanged repo produce byte-identical ids and a byte-identical `graph.json`.
95
+
96
+ **Node types**
97
+
98
+ | Layer | Types | Source |
99
+ |---|---|---|
100
+ | Code | `File`, `Class`, `Function` | tree-sitter extraction |
101
+ | Artifact | `Feature`, `Story`, `AcceptanceCriterion`, `Task`, `Finding`, `QACheck` | `.spark/` templates |
102
+
103
+ **Edge types**
104
+
105
+ | Edge | Direction | Meaning |
106
+ |---|---|---|
107
+ | `contains` | File → Class/Function | code structure |
108
+ | `imports` | File → File | resolved import |
109
+ | `calls` | Function → Function | best-effort, may be absent |
110
+ | `has_story` | Feature → Story | artifact structure |
111
+ | `has_ac` | Story → AcceptanceCriterion | " |
112
+ | `has_task` | Feature → Task | " |
113
+ | `maps_to` | Task → Story | plan links a task to the story it serves |
114
+ | `implements` | Task → File/Function | **the code↔story bridge** (best-effort; see below) |
115
+ | `verifies` | QACheck → AcceptanceCriterion | QA result for an AC |
116
+ | `found_in` | Finding → File | a review finding's location |
117
+
118
+ **Confidence tiers** — every artifact/code link carries a tier, and `impact`
119
+ reports the **weakest link on the strongest path** so you can trust a result
120
+ appropriately:
121
+
122
+ | Tier | Rank | Where it comes from |
123
+ |---|---|---|
124
+ | `declared` | strongest | an explicit `files:` note in a plan task |
125
+ | `extracted` | middle | tree-sitter (`contains`/`imports`) — deterministic structure |
126
+ | `inferred` | weakest | self-derived from git history — treat as a hint, confirm before acting |
127
+
128
+ > **Reading a result:** an `impact` hit tagged `inferred` reached the story only
129
+ > through a git-history guess; a `declared` hit rests on an author-written link.
130
+ > The tier never *raises* confidence — it reports the weakest step, so an
131
+ > inferred edge can only ever *lower* a path's trust, never mask a real one.
132
+
133
+ **Node id schemes** (useful when constructing `get_node`/`shortest_path` queries):
134
+
135
+ ```
136
+ file:<relpath> e.g. file:src/aspark_graph/queries.py
137
+ def:<relpath>::<qualname> e.g. def:src/foo.py::Widget.render
138
+ feature:<name> e.g. feature:aspark-graph
139
+ story:<feature>:<id> e.g. story:aspark-graph:US-1
140
+ ac:<feature>:<id> e.g. ac:aspark-graph:AC-1.2
141
+ task:<feature>:<id> e.g. task:aspark-graph:T3
142
+ finding:<feature>:<id> e.g. finding:aspark-graph:F1
143
+ qa:<feature>:<ac>#<index> e.g. qa:aspark-graph:AC-1.1#0
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Install
149
+
150
+ Requires Python ≥ 3.11. aspark-graph is published on
151
+ [PyPI](https://pypi.org/project/aspark-graph/):
152
+
153
+ ```bash
154
+ pip install aspark-graph
155
+ # or, with uv:
156
+ uvx aspark-graph build . # build the graph for the current repo, no install step
157
+ ```
158
+
159
+ Add it to Claude Code as an MCP server:
160
+
161
+ ```bash
162
+ claude mcp add aspark-graph -- uvx aspark-graph serve
163
+ ```
164
+
165
+ Building from source (for contributors) is documented under
166
+ [Development](#development) below.
167
+
168
+ ## Update
169
+
170
+ ```bash
171
+ pip install --upgrade aspark-graph
172
+ # or, with uvx, the latest published version always runs — no separate update step
173
+ ```
174
+
175
+ The graph is not forwards-compatible across versions: always rebuild after
176
+ updating (`aspark-graph build .`). Incremental builds (v0.4.0+) make this fast —
177
+ only changed files are re-parsed, so a routine update rebuild takes seconds on
178
+ most repos.
179
+
180
+ ## Build the graph
181
+
182
+ ```bash
183
+ aspark-graph build [path] # scans code + .spark/, writes .aspark-graph/graph.json
184
+ ```
185
+
186
+ The graph is written to `.aspark-graph/graph.json` at the repo root (gitignore
187
+ it — it's rebuildable). Re-running `build` on an unchanged repo produces a
188
+ **byte-identical** graph. Parsing fails **loudly** on `.spark/` template drift
189
+ (it names the file and the mismatch) rather than silently guessing.
190
+
191
+ ## Query
192
+
193
+ Every query is available on **both** the CLI and MCP, and they return **identical
194
+ answers by construction** (all query logic lives in one shared module; the CLI
195
+ and server are thin adapters over it, and a parity test enforces it). Output is
196
+ JSON.
197
+
198
+ ### CLI
199
+
200
+ ```bash
201
+ # The two headline queries
202
+ aspark-graph query story_trace US-2 --feature my-feature
203
+ aspark-graph query impact src/foo.py src/bar.py
204
+ aspark-graph query impact --diff HEAD~1..HEAD # blast radius of a change range
205
+
206
+ # Gate & freshness
207
+ aspark-graph query gate_health my-feature # are this feature's ACs covered / passing?
208
+ aspark-graph query staleness # does the graph still match the repo on disk?
209
+
210
+ # Graph navigation
211
+ aspark-graph query get_node "file:src/foo.py"
212
+ aspark-graph query find_nodes Widget --type Class
213
+ aspark-graph query get_neighbors "story:my-feature:US-1" --edge-type has_ac
214
+ aspark-graph query shortest_path "task:my-feature:T1" "ac:my-feature:AC-1.1"
215
+ ```
216
+
217
+ ### MCP
218
+
219
+ The same operations are exposed as MCP tools: `story_trace`, `impact`,
220
+ `gate_health`, `staleness`, `get_node`, `find_nodes`, `get_neighbors`,
221
+ `shortest_path` — all eight **read-only**, plus `build_graph`, the **one tool
222
+ that writes** (`<target>/.aspark-graph/graph.json` and `parse-cache.json`).
223
+ Querying before a build (or any domain error) returns a clean
224
+ `{"found": false, ...}`-shaped result — never a raw traceback. See
225
+ [`SECURITY.md`](https://github.com/a-lottes/aSPARK-graph/blob/main/SECURITY.md)
226
+ for the full trust boundary.
227
+
228
+ ## Linking code to stories
229
+
230
+ `impact` and `story_trace` are only as useful as the `implements` (task→code)
231
+ links they can find. aspark-graph establishes those links three ways, strongest
232
+ to weakest confidence:
233
+
234
+ | Confidence | Source | How to opt in |
235
+ |---|---|---|
236
+ | `declared` | An explicit `files:` note on a plan task | In `plan.md`, add `files: <path>` to a task's *Definition of Done* cell, e.g. `… ; files: src/foo.py`. The link is created only if the file exists — a dangling path is ignored, never fabricated. |
237
+ | `inferred` | Git commit history | Reference the task id **and** its story id in the commit message — subject `T3: add parser (US-1)` or a `Refs: T3, US-1` trailer. Any file that commit touched is linked to the task at `inferred` confidence. |
238
+ | `extracted` | tree-sitter (`contains`/`imports`) | Automatic — no action needed. |
239
+
240
+ **Recommendation for aSPARK repos:** make one commit per task whose message names
241
+ the task and story ids (the convention aSPARK's own workflow already encourages).
242
+ That alone lets `impact` answer on a repo that was never hand-annotated.
243
+
244
+ Inference is **deterministic** (it reads only committed state — file paths and
245
+ message ids, never timestamps) and **offline**; if git is unavailable it is
246
+ simply skipped. When multiple `.spark/` features reuse the same `T<n>`/`US-<n>`
247
+ numbering, a commit is resolved to a **single** feature before linking (by the
248
+ `.spark/<feature>/` tree it touched, or by a unique task→story pairing in its
249
+ message); a genuinely ambiguous commit contributes **no** edge — an honest
250
+ absence over a wrong cross-feature link.
251
+
252
+ ## Supported languages
253
+
254
+ Code extraction covers **TypeScript/JavaScript, Python and Java** (tree-sitter).
255
+ Files in other languages are recorded as unparsed `File` nodes — the build never
256
+ fails on an unknown language.
257
+
258
+ ## Design guarantees (why you can trust the output)
259
+
260
+ - **Deterministic.** Byte-identical rebuild on an unchanged repo; parse-affecting
261
+ dependencies are pinned exactly; a double-build test enforces it. The five
262
+ tree-sitter grammars (Python, TypeScript, Java, Go, Rust) and tree-sitter core
263
+ are pinned with `==`, and the committed `uv.lock` is part of the determinism
264
+ contract for the rest — a grammar version can change the node types it
265
+ extracts, so an unpinned grammar would silently break byte-identity.
266
+ **The guarantee's boundary:** byte-identical
267
+ rebuild holds for an unchanged repo on a **fixed** grammar set. A grammar bump
268
+ is a deliberate, changelog-documented, version-bumped event — never a silent
269
+ change to what a past build promised.
270
+ - **Offline & LLM-free.** No network, no model calls — just AST parsing and
271
+ artifact-template parsing.
272
+ - **Fails loudly, never silently.** Template drift raises a named error; it never
273
+ skips or guesses.
274
+ - **Clean errors.** Domain errors (drift, graph-not-built) surface as one-line
275
+ messages with a non-zero exit / a structured dict — never a stack trace.
276
+ - **Disposable.** The graph is a read model. Delete `.aspark-graph/` and rebuild;
277
+ the source of truth is always the code and the `.spark/` files.
278
+
279
+ ## Out of scope
280
+
281
+ Languages beyond the six currently supported, an LLM/natural-language layer, precise
282
+ call-graph resolution, a visualization UI, exports (Neo4j/GraphML/Obsidian), HTTP/team
283
+ mode, and authenticated or remote MCP transport are out of scope. The current language
284
+ support is Python, TypeScript/JavaScript, Java, Go, and Rust.
285
+
286
+ ## Development
287
+
288
+ Requires [uv](https://docs.astral.sh/uv/). Build and run from a checkout:
289
+
290
+ ```bash
291
+ git clone https://github.com/a-lottes/aSPARK-graph.git aspark-graph
292
+ cd aspark-graph
293
+ uv sync --extra dev
294
+ uv run aspark-graph build . # build the graph for the current repo
295
+ uv run pytest
296
+ ```
297
+
298
+ Add a checkout to Claude Code as an MCP server:
299
+
300
+ ```bash
301
+ claude mcp add aspark-graph -- uv run --directory /path/to/aspark-graph aspark-graph serve
302
+ ```
303
+
304
+ To pick up upstream changes: `git pull && uv sync --extra dev`.
305
+
306
+ The project **dogfoods itself**: its own `.spark/aspark-graph/` trail is the
307
+ primary test fixture, so touching the parser or a query is checked against a real
308
+ aSPARK trail.
309
+
310
+ ## License
311
+
312
+ MIT © Andreas Lottes. Part of the aSPARK product family.
313
+ Code-graph prior art: [Graphify](https://github.com/safishamsi/graphify) —
314
+ different scope.
@@ -0,0 +1,293 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/a-lottes/aSPARK-graph/main/docs/aSPARK-graph-logo-dark.png">
3
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/a-lottes/aSPARK-graph/main/docs/aSPARK-graph-logo-light.png">
4
+ <img alt="aSPARK-graph logo" src="https://raw.githubusercontent.com/a-lottes/aSPARK-graph/main/docs/aSPARK-graph-logo-light.png" width="480">
5
+ </picture>
6
+
7
+ # 🕸️ aspark-graph
8
+
9
+ > **A lean, local knowledge graph that joins a repo's _code_ to its _delivery artifacts_ — so agents and humans can trace a user story to the code that implements it, and see the story-level blast radius of a change.**
10
+
11
+ aspark-graph reads one repository — its source code **and** its
12
+ [aSPARK](https://github.com/a-lottes/aSPARK) `.spark/` delivery trail (specs,
13
+ plans, reviews, QA reports) — and builds a single queryable graph, served over a
14
+ CLI **and** an MCP server. It is **deterministic** (tree-sitter + declared
15
+ artifact links; **no LLM, no network**) and **disposable** (the graph is a
16
+ rebuildable read model, never a source of truth).
17
+
18
+ ---
19
+
20
+ ## The two questions it exists to answer
21
+
22
+ Everything else is in service of these:
23
+
24
+ | Question | Tool | Plain meaning |
25
+ |---|---|---|
26
+ | *"Which code implements this user story, and did its acceptance criteria pass QA?"* | `story_trace US-2` | Follow story → ACs → plan tasks → code → QA results, with zero grepping. |
27
+ | *"If I change these files, which stories and acceptance criteria are in the blast radius — what must QA re-verify?"* | `impact src/foo.py` | Walk code → tasks → stories/ACs, tagging each hit with how trustworthy the link is. |
28
+
29
+ ## Why this exists
30
+
31
+ When an AI agent (or the developer supervising it) works on an aSPARK-managed
32
+ repo **too large to hold in your head**, those two questions are exactly the ones
33
+ aSPARK's own review and QA gates depend on — and today the only way to answer
34
+ them is `Grep`/`Glob` plus reading `.spark/` files by hand.
35
+
36
+ The spec → plan → review → QA trail *is* machine-parseable, and it *is* linked to
37
+ code by intent. But nothing joins the two, so an agent re-derives the link every
38
+ time it greps: **slowly, incompletely, and non-reproducibly.** aspark-graph
39
+ computes that join once, deterministically, and lets you query it.
40
+
41
+ It does this by deliberately doing **less** on the code side than a general code
42
+ graph, and adding the one thing general graphs don't have: the **delivery
43
+ artifacts**. That artifact layer is what makes story tracing, gate-aware impact,
44
+ and orphan detection possible at all.
45
+
46
+ ## When to use it — and when not
47
+
48
+ - ✅ **Use it** on an aSPARK repo big enough that "read every relevant file"
49
+ isn't viable, when you need a fast, reproducible answer *before* opening files.
50
+ - ❌ **Skip it** on a repo small enough to hold in your head (just read the
51
+ files), or a repo with no `.spark/` artifacts (the artifact layer is the whole
52
+ point).
53
+ - 🤝 **Want a broad semantic code graph too?** Run
54
+ [Graphify](https://github.com/safishamsi/graphify) alongside it — different
55
+ scope, no conflict. aspark-graph is an accelerant for aSPARK, not a replacement
56
+ for a code-search tool.
57
+
58
+ **Using aspark-graph in aSPARK gates?** See
59
+ [`docs/aspark-integration.md`](https://github.com/a-lottes/aSPARK-graph/blob/main/docs/aspark-integration.md)
60
+ for drop-in `CLAUDE.md` blocks that wire the `/peer-review` and `/demo-day`
61
+ gates to the query tools.
62
+
63
+ **Trust boundary, non-guarantees, and how to report a vulnerability:** see
64
+ [`SECURITY.md`](https://github.com/a-lottes/aSPARK-graph/blob/main/SECURITY.md)
65
+ — read it before treating this server's output as anything other than data.
66
+
67
+ ---
68
+
69
+ ## The graph model (read this to interpret any result)
70
+
71
+ The graph is a typed, directed multigraph. Every node id is **stable and
72
+ deterministic**, derived only from content and location, so two builds of an
73
+ unchanged repo produce byte-identical ids and a byte-identical `graph.json`.
74
+
75
+ **Node types**
76
+
77
+ | Layer | Types | Source |
78
+ |---|---|---|
79
+ | Code | `File`, `Class`, `Function` | tree-sitter extraction |
80
+ | Artifact | `Feature`, `Story`, `AcceptanceCriterion`, `Task`, `Finding`, `QACheck` | `.spark/` templates |
81
+
82
+ **Edge types**
83
+
84
+ | Edge | Direction | Meaning |
85
+ |---|---|---|
86
+ | `contains` | File → Class/Function | code structure |
87
+ | `imports` | File → File | resolved import |
88
+ | `calls` | Function → Function | best-effort, may be absent |
89
+ | `has_story` | Feature → Story | artifact structure |
90
+ | `has_ac` | Story → AcceptanceCriterion | " |
91
+ | `has_task` | Feature → Task | " |
92
+ | `maps_to` | Task → Story | plan links a task to the story it serves |
93
+ | `implements` | Task → File/Function | **the code↔story bridge** (best-effort; see below) |
94
+ | `verifies` | QACheck → AcceptanceCriterion | QA result for an AC |
95
+ | `found_in` | Finding → File | a review finding's location |
96
+
97
+ **Confidence tiers** — every artifact/code link carries a tier, and `impact`
98
+ reports the **weakest link on the strongest path** so you can trust a result
99
+ appropriately:
100
+
101
+ | Tier | Rank | Where it comes from |
102
+ |---|---|---|
103
+ | `declared` | strongest | an explicit `files:` note in a plan task |
104
+ | `extracted` | middle | tree-sitter (`contains`/`imports`) — deterministic structure |
105
+ | `inferred` | weakest | self-derived from git history — treat as a hint, confirm before acting |
106
+
107
+ > **Reading a result:** an `impact` hit tagged `inferred` reached the story only
108
+ > through a git-history guess; a `declared` hit rests on an author-written link.
109
+ > The tier never *raises* confidence — it reports the weakest step, so an
110
+ > inferred edge can only ever *lower* a path's trust, never mask a real one.
111
+
112
+ **Node id schemes** (useful when constructing `get_node`/`shortest_path` queries):
113
+
114
+ ```
115
+ file:<relpath> e.g. file:src/aspark_graph/queries.py
116
+ def:<relpath>::<qualname> e.g. def:src/foo.py::Widget.render
117
+ feature:<name> e.g. feature:aspark-graph
118
+ story:<feature>:<id> e.g. story:aspark-graph:US-1
119
+ ac:<feature>:<id> e.g. ac:aspark-graph:AC-1.2
120
+ task:<feature>:<id> e.g. task:aspark-graph:T3
121
+ finding:<feature>:<id> e.g. finding:aspark-graph:F1
122
+ qa:<feature>:<ac>#<index> e.g. qa:aspark-graph:AC-1.1#0
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Install
128
+
129
+ Requires Python ≥ 3.11. aspark-graph is published on
130
+ [PyPI](https://pypi.org/project/aspark-graph/):
131
+
132
+ ```bash
133
+ pip install aspark-graph
134
+ # or, with uv:
135
+ uvx aspark-graph build . # build the graph for the current repo, no install step
136
+ ```
137
+
138
+ Add it to Claude Code as an MCP server:
139
+
140
+ ```bash
141
+ claude mcp add aspark-graph -- uvx aspark-graph serve
142
+ ```
143
+
144
+ Building from source (for contributors) is documented under
145
+ [Development](#development) below.
146
+
147
+ ## Update
148
+
149
+ ```bash
150
+ pip install --upgrade aspark-graph
151
+ # or, with uvx, the latest published version always runs — no separate update step
152
+ ```
153
+
154
+ The graph is not forwards-compatible across versions: always rebuild after
155
+ updating (`aspark-graph build .`). Incremental builds (v0.4.0+) make this fast —
156
+ only changed files are re-parsed, so a routine update rebuild takes seconds on
157
+ most repos.
158
+
159
+ ## Build the graph
160
+
161
+ ```bash
162
+ aspark-graph build [path] # scans code + .spark/, writes .aspark-graph/graph.json
163
+ ```
164
+
165
+ The graph is written to `.aspark-graph/graph.json` at the repo root (gitignore
166
+ it — it's rebuildable). Re-running `build` on an unchanged repo produces a
167
+ **byte-identical** graph. Parsing fails **loudly** on `.spark/` template drift
168
+ (it names the file and the mismatch) rather than silently guessing.
169
+
170
+ ## Query
171
+
172
+ Every query is available on **both** the CLI and MCP, and they return **identical
173
+ answers by construction** (all query logic lives in one shared module; the CLI
174
+ and server are thin adapters over it, and a parity test enforces it). Output is
175
+ JSON.
176
+
177
+ ### CLI
178
+
179
+ ```bash
180
+ # The two headline queries
181
+ aspark-graph query story_trace US-2 --feature my-feature
182
+ aspark-graph query impact src/foo.py src/bar.py
183
+ aspark-graph query impact --diff HEAD~1..HEAD # blast radius of a change range
184
+
185
+ # Gate & freshness
186
+ aspark-graph query gate_health my-feature # are this feature's ACs covered / passing?
187
+ aspark-graph query staleness # does the graph still match the repo on disk?
188
+
189
+ # Graph navigation
190
+ aspark-graph query get_node "file:src/foo.py"
191
+ aspark-graph query find_nodes Widget --type Class
192
+ aspark-graph query get_neighbors "story:my-feature:US-1" --edge-type has_ac
193
+ aspark-graph query shortest_path "task:my-feature:T1" "ac:my-feature:AC-1.1"
194
+ ```
195
+
196
+ ### MCP
197
+
198
+ The same operations are exposed as MCP tools: `story_trace`, `impact`,
199
+ `gate_health`, `staleness`, `get_node`, `find_nodes`, `get_neighbors`,
200
+ `shortest_path` — all eight **read-only**, plus `build_graph`, the **one tool
201
+ that writes** (`<target>/.aspark-graph/graph.json` and `parse-cache.json`).
202
+ Querying before a build (or any domain error) returns a clean
203
+ `{"found": false, ...}`-shaped result — never a raw traceback. See
204
+ [`SECURITY.md`](https://github.com/a-lottes/aSPARK-graph/blob/main/SECURITY.md)
205
+ for the full trust boundary.
206
+
207
+ ## Linking code to stories
208
+
209
+ `impact` and `story_trace` are only as useful as the `implements` (task→code)
210
+ links they can find. aspark-graph establishes those links three ways, strongest
211
+ to weakest confidence:
212
+
213
+ | Confidence | Source | How to opt in |
214
+ |---|---|---|
215
+ | `declared` | An explicit `files:` note on a plan task | In `plan.md`, add `files: <path>` to a task's *Definition of Done* cell, e.g. `… ; files: src/foo.py`. The link is created only if the file exists — a dangling path is ignored, never fabricated. |
216
+ | `inferred` | Git commit history | Reference the task id **and** its story id in the commit message — subject `T3: add parser (US-1)` or a `Refs: T3, US-1` trailer. Any file that commit touched is linked to the task at `inferred` confidence. |
217
+ | `extracted` | tree-sitter (`contains`/`imports`) | Automatic — no action needed. |
218
+
219
+ **Recommendation for aSPARK repos:** make one commit per task whose message names
220
+ the task and story ids (the convention aSPARK's own workflow already encourages).
221
+ That alone lets `impact` answer on a repo that was never hand-annotated.
222
+
223
+ Inference is **deterministic** (it reads only committed state — file paths and
224
+ message ids, never timestamps) and **offline**; if git is unavailable it is
225
+ simply skipped. When multiple `.spark/` features reuse the same `T<n>`/`US-<n>`
226
+ numbering, a commit is resolved to a **single** feature before linking (by the
227
+ `.spark/<feature>/` tree it touched, or by a unique task→story pairing in its
228
+ message); a genuinely ambiguous commit contributes **no** edge — an honest
229
+ absence over a wrong cross-feature link.
230
+
231
+ ## Supported languages
232
+
233
+ Code extraction covers **TypeScript/JavaScript, Python and Java** (tree-sitter).
234
+ Files in other languages are recorded as unparsed `File` nodes — the build never
235
+ fails on an unknown language.
236
+
237
+ ## Design guarantees (why you can trust the output)
238
+
239
+ - **Deterministic.** Byte-identical rebuild on an unchanged repo; parse-affecting
240
+ dependencies are pinned exactly; a double-build test enforces it. The five
241
+ tree-sitter grammars (Python, TypeScript, Java, Go, Rust) and tree-sitter core
242
+ are pinned with `==`, and the committed `uv.lock` is part of the determinism
243
+ contract for the rest — a grammar version can change the node types it
244
+ extracts, so an unpinned grammar would silently break byte-identity.
245
+ **The guarantee's boundary:** byte-identical
246
+ rebuild holds for an unchanged repo on a **fixed** grammar set. A grammar bump
247
+ is a deliberate, changelog-documented, version-bumped event — never a silent
248
+ change to what a past build promised.
249
+ - **Offline & LLM-free.** No network, no model calls — just AST parsing and
250
+ artifact-template parsing.
251
+ - **Fails loudly, never silently.** Template drift raises a named error; it never
252
+ skips or guesses.
253
+ - **Clean errors.** Domain errors (drift, graph-not-built) surface as one-line
254
+ messages with a non-zero exit / a structured dict — never a stack trace.
255
+ - **Disposable.** The graph is a read model. Delete `.aspark-graph/` and rebuild;
256
+ the source of truth is always the code and the `.spark/` files.
257
+
258
+ ## Out of scope
259
+
260
+ Languages beyond the six currently supported, an LLM/natural-language layer, precise
261
+ call-graph resolution, a visualization UI, exports (Neo4j/GraphML/Obsidian), HTTP/team
262
+ mode, and authenticated or remote MCP transport are out of scope. The current language
263
+ support is Python, TypeScript/JavaScript, Java, Go, and Rust.
264
+
265
+ ## Development
266
+
267
+ Requires [uv](https://docs.astral.sh/uv/). Build and run from a checkout:
268
+
269
+ ```bash
270
+ git clone https://github.com/a-lottes/aSPARK-graph.git aspark-graph
271
+ cd aspark-graph
272
+ uv sync --extra dev
273
+ uv run aspark-graph build . # build the graph for the current repo
274
+ uv run pytest
275
+ ```
276
+
277
+ Add a checkout to Claude Code as an MCP server:
278
+
279
+ ```bash
280
+ claude mcp add aspark-graph -- uv run --directory /path/to/aspark-graph aspark-graph serve
281
+ ```
282
+
283
+ To pick up upstream changes: `git pull && uv sync --extra dev`.
284
+
285
+ The project **dogfoods itself**: its own `.spark/aspark-graph/` trail is the
286
+ primary test fixture, so touching the parser or a query is checked against a real
287
+ aSPARK trail.
288
+
289
+ ## License
290
+
291
+ MIT © Andreas Lottes. Part of the aSPARK product family.
292
+ Code-graph prior art: [Graphify](https://github.com/safishamsi/graphify) —
293
+ different scope.