Skip to main content
aSPARK-graph logo

🕸️ aspark-graph

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.

aspark-graph reads one repository — its source code and its aSPARK .spark/ delivery trail (specs, plans, reviews, QA reports) — and builds a single queryable graph, served over a CLI and an MCP server. It is deterministic (tree-sitter + declared artifact links; no LLM, no network) and disposable (the graph is a rebuildable read model, never a source of truth).


The two questions it exists to answer

Everything else is in service of these:

Question Tool Plain meaning
"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.
"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.

Why this exists

When an AI agent (or the developer supervising it) works on an aSPARK-managed repo too large to hold in your head, those two questions are exactly the ones aSPARK's own review and QA gates depend on — and today the only way to answer them is Grep/Glob plus reading .spark/ files by hand.

The spec → plan → review → QA trail is machine-parseable, and it is linked to code by intent. But nothing joins the two, so an agent re-derives the link every time it greps: slowly, incompletely, and non-reproducibly. aspark-graph computes that join once, deterministically, and lets you query it.

It does this by deliberately doing less on the code side than a general code graph, and adding the one thing general graphs don't have: the delivery artifacts. That artifact layer is what makes story tracing, gate-aware impact, and orphan detection possible at all.

When to use it — and when not

  • Use it on an aSPARK repo big enough that "read every relevant file" isn't viable, when you need a fast, reproducible answer before opening files.
  • Skip it on a repo small enough to hold in your head (just read the files), or a repo with no .spark/ artifacts (the artifact layer is the whole point).
  • 🤝 Want a broad semantic code graph too? Run Graphify alongside it — different scope, no conflict. aspark-graph is an accelerant for aSPARK, not a replacement for a code-search tool.

Using aspark-graph in aSPARK gates? See docs/aspark-integration.md for drop-in CLAUDE.md blocks that wire the /peer-review and /demo-day gates to the query tools.

Trust boundary, non-guarantees, and how to report a vulnerability: see SECURITY.md — read it before treating this server's output as anything other than data.


The graph model (read this to interpret any result)

The graph is a typed, directed multigraph. Every node id is stable and deterministic, derived only from content and location, so two builds of an unchanged repo produce byte-identical ids and a byte-identical graph.json.

Node types

Layer Types Source
Code File, Class, Function tree-sitter extraction
Artifact Feature, Story, AcceptanceCriterion, Task, Finding, QACheck .spark/ templates

Edge types

Edge Direction Meaning
contains File → Class/Function code structure
imports File → File resolved import
calls Function → Function best-effort, may be absent
has_story Feature → Story artifact structure
has_ac Story → AcceptanceCriterion "
has_task Feature → Task "
maps_to Task → Story plan links a task to the story it serves
implements Task → File/Function the code↔story bridge (best-effort; see below)
verifies QACheck → AcceptanceCriterion QA result for an AC
found_in Finding → File a review finding's location

Confidence tiers — every artifact/code link carries a tier, and impact reports the weakest link on the strongest path so you can trust a result appropriately:

Tier Rank Where it comes from
declared strongest an explicit files: note in a plan task
extracted middle tree-sitter (contains/imports) — deterministic structure
inferred weakest self-derived from git history — treat as a hint, confirm before acting

Reading a result: an impact hit tagged inferred reached the story only through a git-history guess; a declared hit rests on an author-written link. The tier never raises confidence — it reports the weakest step, so an inferred edge can only ever lower a path's trust, never mask a real one.

Node id schemes (useful when constructing get_node/shortest_path queries):

file:<relpath>                     e.g. file:src/aspark_graph/queries.py
def:<relpath>::<qualname>          e.g. def:src/foo.py::Widget.render
feature:<name>                     e.g. feature:aspark-graph
story:<feature>:<id>               e.g. story:aspark-graph:US-1
ac:<feature>:<id>                  e.g. ac:aspark-graph:AC-1.2
task:<feature>:<id>                e.g. task:aspark-graph:T3
finding:<feature>:<id>             e.g. finding:aspark-graph:F1
qa:<feature>:<ac>#<index>          e.g. qa:aspark-graph:AC-1.1#0

Install

Requires Python ≥ 3.11. aspark-graph is published on PyPI:

pip install aspark-graph
# or, with uv:
uvx aspark-graph build .   # build the graph for the current repo, no install step

Add it to Claude Code as an MCP server:

claude mcp add aspark-graph -- uvx aspark-graph serve

Building from source (for contributors) is documented under Development below.

Update

pip install --upgrade aspark-graph
# or, with uvx, the latest published version always runs — no separate update step

The graph is not forwards-compatible across versions: always rebuild after updating (aspark-graph build .). Incremental builds (v0.4.0+) make this fast — only changed files are re-parsed, so a routine update rebuild takes seconds on most repos.

Build the graph

aspark-graph build [path]     # scans code + .spark/, writes .aspark-graph/graph.json

The graph is written to .aspark-graph/graph.json at the repo root (gitignore it — it's rebuildable). Re-running build on an unchanged repo produces a byte-identical graph. Parsing fails loudly on .spark/ template drift (it names the file and the mismatch) rather than silently guessing.

Query

Every query is available on both the CLI and MCP, and they return identical answers by construction (all query logic lives in one shared module; the CLI and server are thin adapters over it, and a parity test enforces it). Output is JSON.

CLI

# The two headline queries
aspark-graph query story_trace US-2 --feature my-feature
aspark-graph query impact src/foo.py src/bar.py
aspark-graph query impact --diff HEAD~1..HEAD      # blast radius of a change range

# Gate & freshness
aspark-graph query gate_health my-feature          # are this feature's ACs covered / passing?
aspark-graph query staleness                        # does the graph still match the repo on disk?

# Graph navigation
aspark-graph query get_node "file:src/foo.py"
aspark-graph query find_nodes Widget --type Class
aspark-graph query get_neighbors "story:my-feature:US-1" --edge-type has_ac
aspark-graph query shortest_path "task:my-feature:T1" "ac:my-feature:AC-1.1"

MCP

The same operations are exposed as MCP tools: story_trace, impact, gate_health, staleness, get_node, find_nodes, get_neighbors, shortest_path — all eight read-only, plus build_graph, the one tool that writes (<target>/.aspark-graph/graph.json and parse-cache.json). Querying before a build (or any domain error) returns a clean {"found": false, ...}-shaped result — never a raw traceback. See SECURITY.md for the full trust boundary.

Linking code to stories

impact and story_trace are only as useful as the implements (task→code) links they can find. aspark-graph establishes those links three ways, strongest to weakest confidence:

Confidence Source How to opt in
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.
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.
extracted tree-sitter (contains/imports) Automatic — no action needed.

Recommendation for aSPARK repos: make one commit per task whose message names the task and story ids (the convention aSPARK's own workflow already encourages). That alone lets impact answer on a repo that was never hand-annotated.

Inference is deterministic (it reads only committed state — file paths and message ids, never timestamps) and offline; if git is unavailable it is simply skipped. When multiple .spark/ features reuse the same T<n>/US-<n> numbering, a commit is resolved to a single feature before linking (by the .spark/<feature>/ tree it touched, or by a unique task→story pairing in its message); a genuinely ambiguous commit contributes no edge — an honest absence over a wrong cross-feature link.

Supported languages

Code extraction covers TypeScript/JavaScript, Python and Java (tree-sitter). Files in other languages are recorded as unparsed File nodes — the build never fails on an unknown language.

Design guarantees (why you can trust the output)

  • Deterministic. Byte-identical rebuild on an unchanged repo; parse-affecting dependencies are pinned exactly; a double-build test enforces it. The five tree-sitter grammars (Python, TypeScript, Java, Go, Rust) and tree-sitter core are pinned with ==, and the committed uv.lock is part of the determinism contract for the rest — a grammar version can change the node types it extracts, so an unpinned grammar would silently break byte-identity. The guarantee's boundary: byte-identical rebuild holds for an unchanged repo on a fixed grammar set. A grammar bump is a deliberate, changelog-documented, version-bumped event — never a silent change to what a past build promised.
  • Offline & LLM-free. No network, no model calls — just AST parsing and artifact-template parsing.
  • Fails loudly, never silently. Template drift raises a named error; it never skips or guesses.
  • Clean errors. Domain errors (drift, graph-not-built) surface as one-line messages with a non-zero exit / a structured dict — never a stack trace.
  • Disposable. The graph is a read model. Delete .aspark-graph/ and rebuild; the source of truth is always the code and the .spark/ files.

Out of scope

Languages beyond the six currently supported, an LLM/natural-language layer, precise call-graph resolution, a visualization UI, exports (Neo4j/GraphML/Obsidian), HTTP/team mode, and authenticated or remote MCP transport are out of scope. The current language support is Python, TypeScript/JavaScript, Java, Go, and Rust.

Development

Requires uv. Build and run from a checkout:

git clone https://github.com/a-lottes/aSPARK-graph.git aspark-graph
cd aspark-graph
uv sync --extra dev
uv run aspark-graph build .   # build the graph for the current repo
uv run pytest

Add a checkout to Claude Code as an MCP server:

claude mcp add aspark-graph -- uv run --directory /path/to/aspark-graph aspark-graph serve

To pick up upstream changes: git pull && uv sync --extra dev.

The project dogfoods itself: its own .spark/aspark-graph/ trail is the primary test fixture, so touching the parser or a query is checked against a real aSPARK trail.

License

MIT © Andreas Lottes. Part of the aSPARK product family. Code-graph prior art: Graphify — different scope.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aspark_graph-0.7.0.tar.gz (79.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aspark_graph-0.7.0-py3-none-any.whl (49.8 kB view details)

Uploaded Python 3

File details

Details for the file aspark_graph-0.7.0.tar.gz.

File metadata

  • Download URL: aspark_graph-0.7.0.tar.gz
  • Upload date:
  • Size: 79.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for aspark_graph-0.7.0.tar.gz
Algorithm Hash digest
SHA256 1f1ad0615e5f1dc6f65e4b8b78888b3f33cd54e1934d6860ca1a63f4e9df4b57
MD5 957fe00ac1d2fa5743d7a1f283e84e18
BLAKE2b-256 3c2dab1ba093d501ff7699364d17f836d6f6ff5fe2046de98720b1b92a13c109

See more details on using hashes here.

File details

Details for the file aspark_graph-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: aspark_graph-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 49.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for aspark_graph-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 28783ede5a221fac7a63fbfaf6ea30f9beb1eec53ec3bc360a541d64793da373
MD5 056fe812804058cb0c2f46dab4b83ddd
BLAKE2b-256 556d12137e6cd7e9b77b443a03a558bb91c41f1a25c8a910852c3e80157648e5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page