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.
- aspark_graph-0.7.0/.gitignore +21 -0
- aspark_graph-0.7.0/LICENSE +21 -0
- aspark_graph-0.7.0/PKG-INFO +314 -0
- aspark_graph-0.7.0/README.md +293 -0
- aspark_graph-0.7.0/pyproject.toml +70 -0
- aspark_graph-0.7.0/src/aspark_graph/__init__.py +3 -0
- aspark_graph-0.7.0/src/aspark_graph/artifacts.py +365 -0
- aspark_graph-0.7.0/src/aspark_graph/build.py +417 -0
- aspark_graph-0.7.0/src/aspark_graph/cli.py +204 -0
- aspark_graph-0.7.0/src/aspark_graph/confinement.py +116 -0
- aspark_graph-0.7.0/src/aspark_graph/extractors/__init__.py +36 -0
- aspark_graph-0.7.0/src/aspark_graph/extractors/base.py +62 -0
- aspark_graph-0.7.0/src/aspark_graph/extractors/code_go.py +153 -0
- aspark_graph-0.7.0/src/aspark_graph/extractors/code_java.py +74 -0
- aspark_graph-0.7.0/src/aspark_graph/extractors/code_py.py +98 -0
- aspark_graph-0.7.0/src/aspark_graph/extractors/code_rust.py +162 -0
- aspark_graph-0.7.0/src/aspark_graph/extractors/code_ts.py +118 -0
- aspark_graph-0.7.0/src/aspark_graph/git.py +103 -0
- aspark_graph-0.7.0/src/aspark_graph/graph.py +156 -0
- aspark_graph-0.7.0/src/aspark_graph/inference.py +124 -0
- aspark_graph-0.7.0/src/aspark_graph/model.py +89 -0
- aspark_graph-0.7.0/src/aspark_graph/parse_cache.py +161 -0
- aspark_graph-0.7.0/src/aspark_graph/queries.py +417 -0
- aspark_graph-0.7.0/src/aspark_graph/server.py +123 -0
- aspark_graph-0.7.0/tests/conftest.py +100 -0
- aspark_graph-0.7.0/tests/fixtures/code/sample.py +20 -0
- aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/plan.md +15 -0
- aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/qa-report.md +19 -0
- aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/release-notes.md +13 -0
- aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/review-report.md +14 -0
- aspark_graph-0.7.0/tests/fixtures/sample_repo/.spark/demo/spec.md +34 -0
- aspark_graph-0.7.0/tests/fixtures/sample_repo/src/demo/app.py +10 -0
- aspark_graph-0.7.0/tests/fixtures/sample_repo/src/demo/util.py +2 -0
- aspark_graph-0.7.0/tests/test_artifacts.py +129 -0
- aspark_graph-0.7.0/tests/test_build.py +73 -0
- aspark_graph-0.7.0/tests/test_build_bounds.py +177 -0
- aspark_graph-0.7.0/tests/test_cli_mcp_parity.py +156 -0
- aspark_graph-0.7.0/tests/test_confinement.py +179 -0
- aspark_graph-0.7.0/tests/test_confinement_cli_mcp.py +197 -0
- aspark_graph-0.7.0/tests/test_confinement_guards.py +127 -0
- aspark_graph-0.7.0/tests/test_extractor_go.py +129 -0
- aspark_graph-0.7.0/tests/test_extractor_java.py +82 -0
- aspark_graph-0.7.0/tests/test_extractor_rust.py +183 -0
- aspark_graph-0.7.0/tests/test_extractor_ts.py +58 -0
- aspark_graph-0.7.0/tests/test_gate_health.py +43 -0
- aspark_graph-0.7.0/tests/test_git.py +100 -0
- aspark_graph-0.7.0/tests/test_graph.py +74 -0
- aspark_graph-0.7.0/tests/test_headline.py +62 -0
- aspark_graph-0.7.0/tests/test_impact.py +59 -0
- aspark_graph-0.7.0/tests/test_impact_diff.py +56 -0
- aspark_graph-0.7.0/tests/test_incremental.py +492 -0
- aspark_graph-0.7.0/tests/test_incremental_bench.py +101 -0
- aspark_graph-0.7.0/tests/test_inference.py +127 -0
- aspark_graph-0.7.0/tests/test_integration_docs.py +427 -0
- aspark_graph-0.7.0/tests/test_link_conventions.py +49 -0
- aspark_graph-0.7.0/tests/test_mcp_errors.py +27 -0
- aspark_graph-0.7.0/tests/test_mcp_transport.py +109 -0
- aspark_graph-0.7.0/tests/test_navigation.py +81 -0
- aspark_graph-0.7.0/tests/test_packaging.py +60 -0
- aspark_graph-0.7.0/tests/test_readme.py +57 -0
- aspark_graph-0.7.0/tests/test_security_doc.py +216 -0
- aspark_graph-0.7.0/tests/test_smoke.py +7 -0
- aspark_graph-0.7.0/tests/test_staleness.py +55 -0
- aspark_graph-0.7.0/tests/test_story_trace.py +60 -0
- aspark_graph-0.7.0/tests/test_story_trace_inference.py +40 -0
- 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.
|