Salak
Deterministic, provenance-tagged code knowledge graph generator.
Salak parses a source repository and emits a single JSON file — repo-graph.json —
describing its structure and the relations between its symbols, so an AI coding
assistant can answer "what depends on this module?" from a recorded fact instead of
a guess.
The graph is a build artifact, not a database. It is regenerated from source, committed to git, and read by tools. Salak owns the generation and nothing else.
Package name
Published on PyPI as salak. The name
salak is taken on npm; if a JavaScript distribution is ever needed it ships as
salak-graph (decision D5).
Status
0.1.0, shipped 2026-08-26 — ship verdict at the v1.0 exit gate.
Every phase gate (TASK-039, TASK-062, TASK-083, TASK-098) returned ship. salak scan, salak check, salak diff, salak validate, salak doctor and salak version all work today, verified against the published PyPI package on real
repositories, not just this checkout. 0.1.0 rather than 1.0.0 on purpose (decision
D55) — the WBS's internal "v1.0" phase label was never a semver stability promise.
Currently published on PyPI: 0.3.0 (2026-09-26) — the Go adapter (v1.1), salak init,
salak query, JavaScript and .vue/.svelte/.astro files, multi-module Go; release
notes on the GitHub v0.3.0 release. Rehearsed as 0.3.0rc1 on TestPyPI and verified
from a clean install before publishing.
0.2.0 (2026-08-26, TASK-106/D58 onward — adapters[].emits
coverage declaration, class-level attributes as variable nodes, schema 1.1, the
salak check exit-code/message fixes S20/S49). Not a WBS phase release — the phase
table below tracks the four gated deliverables above, and this bump carries none of
their own DoD; it exists because the fixes and features landed and were adopted, not
because a phase closed. Don't read "0.2.0" as "the WBS's v0.2 phase, re-shipped" — that
phase's own work shipped inside 0.1.0 already, per D55.
| Phase | Deliverable | State |
|---|---|---|
| Spike 0 | Feasibility and readiness | done |
| v0.1 | Structure pass: files/nodes/defines/imports/depends_on, published JSON Schema, salak validate |
shipped |
| v0.2 | Resolution pass (§4.1 Levels 1–2), extends/implements/instantiates/references, full provenance |
shipped |
| v0.3 | Incremental scan (the run_incremental library entry point), salak check, salak diff |
shipped |
| v1.0 | Packaging, salak doctor, --stdout, docs, clean-machine install proof |
shipped |
| v1.1 | Go adapter: structure, go.mod-driven imports, gopls-backed calls |
shipped in 0.3.0 (exit gate TASK-117, 2026-09-25) |
Also in 0.3.0, outside any WBS phase: .vue, .svelte and .astro component files
are scanned by the TypeScript adapter through their <script> blocks (and Astro
frontmatter) — decision D84. They keep language: "typescript"; markup is not read, so
a component used only in a template is represented by the import that brought it in.
JavaScript (.js/.jsx/.mjs/.cjs, D86) — previously never scanned at all — is read
by the same adapter, also as language: "typescript". salak init (D83), salak query
(D87) and a truthful scan.lsp (D82) ship in the same release.
No further phase is currently open. --update (incremental scanning via salak scan,
using the run_incremental library entry point already proven correct at TASK-081/083)
was wired in as its own decision, D63 (2026-08-27) — not a phase deliverable, the
same way the 0.2.0 PyPI release above was a version bump rather than a phase.
Level 3 (method-call resolution) was decided void for TypeScript and Python at the
v0.1 exit gate (K15, decision D43): it resolved only 23.1% of call sites against
95.4–99.6% for Levels 1–2, with no language server at all. Go is the exception
(D74): gopls resolved 97.7% of Go method calls, so the Go adapter consults it and
Go is the only language that emits calls edges. See "Degraded mode and resolution
levels" below.
Design principles
- Deterministic over clever. Same input, same bytes out. No LLM anywhere — the pipeline is offline, free and reproducible.
- Precision over recall. An
extractededge that is wrong is worse than a missing edge. When unsure, downgrade the provenance tag; never upgrade it. - The schema is the contract. Consumers talk to the schema, never to Salak internals. A formal JSON Schema ships with the package and is authoritative over the prose (SDD §3.7).
- One language, one adapter. Adding a language must not modify core; adapters
register through an entry-point group (decision D3), not an import in
core. - A graph declares its own scope. Reading the file's
scanblock tells you which languages ran, whether anything failed, and what the run did not attempt.
Requirements
- Python 3.11+
- Node.js — optional.
pyrightandtypescript-language-serverare the two language servers Salak knows how to detect, but method-call resolution (Level 3) is void (K15/D43), and Levels 1–2 resolve from tree-sitter and filesystem heuristics alone — no module undersalak.resolutionimportssalak.lsp. The practical result: today,repo-graph.jsoncomes out byte-for-byte the same with or without Node installed.salak doctorstill reports Node/server presence and version drift as information (decision D45 — a missing server is never treated as unhealthy), because that detection work is what a future Level-3 revival would need, not because anything currently emitted depends on it. - TypeScript ≤ 5.9.x as the tsserver host, only relevant if you install the
language servers for
salak doctorto report on. TypeScript 7 ships nolib/tsserver.js, andtypescript-language-serverworks by spawning tsserver, so thelatestdist-tag is broken for this purpose (decision D15). - Go projects only (v1.1): the
gotoolchain andgopls— both optional, both change the output. Unlike the two servers above, these do matter. Withoutgo, standard-library imports cannot be told apart from third-party ones (GO_TOOLCHAIN_UNAVAILABLE); withoutgopls, cross-file Gocallsedges are recorded as unresolved (LSP_UNAVAILABLE). Install withgo install golang.org/x/tools/gopls@latest. Each cross-file call site costs onegoplsquestion (~0.13 s); a very large Go tree is better served by--updateafter the first scan, or--no-lsp(seedocs/troubleshooting.md).
Installing
From PyPI (recommended for using Salak on your own repositories):
uv tool install salak
(No uv? pip install salak works the same way, into whatever environment pip
targets.) That's it for TypeScript and Python — no Node, no language servers required
to run salak scan and get a full graph. Go repositories also want go and gopls
on PATH (see Requirements). This puts a plain salak command on your PATH; verify with:
salak version
If you also want salak doctor to report on the language servers (informational
only — see Requirements below; installing them changes nothing about what scan
emits), install Node.js and then, globally or in whatever project you're scanning:
npm i -g pyright@1.1.411 typescript-language-server@5.3.0 typescript@5.9.3
From source (for contributing to Salak itself):
git clone https://github.com/edhoferdian/salak
cd salak
uv sync
uv run salak ... now works from inside the checkout. uv tool install --editable .
puts an editable salak on your PATH instead of prefixing every call with uv run.
npm ci (from this checkout) installs the exact pinned language-server versions the
test suite spawns — only needed for Salak's own development, not for using it.
Quickstart
Three commands, from install to a generated graph, run against any repository on your machine:
uv tool install salak
salak scan /path/to/your/repo
salak validate /path/to/your/repo/project-memory/repo-graph.json
scan writes the graph; validate confirms it matches its own declared schema
version. salak init /path/to/your/repo then writes a block into that repository's
CLAUDE.md telling AI assistants how to consult the graph — generated from the graph
itself, and refreshed in place on re-run (--file AGENTS.md for another file).
salak query importers src/lib/x.ts (also imports, symbols, callers) answers one
question from the graph without anyone reading the whole file (D87). salak check /path/to/your/repo confirms the graph is still fresh against
the working tree (it will be, immediately after a scan) — useful as a pre-commit
hook or CI step once the graph is committed alongside your source. Run against this
repository itself, scan reports:
salak: wrote <repo>/project-memory/repo-graph.json
salak: 249 parsed, 2 failed, 0 skipped · 3849 nodes, 11856 edges · 2 error(s), 0 warning(s)
(Exact counts drift as the repository grows; the shape — most files parsed, two deliberately-broken fixtures failing, the run still completing — is what to expect.)
The two PARSE_FAILED errors above are two deliberately-broken fixture files under
tests/fixtures/broken/ used to test the parser's recovery path — exit code 2
("completed with diagnostics", not a crash) is expected there. On an ordinary
repository, scan exits 0.
Default output path
salak scan PATH writes to PATH/project-memory/repo-graph.json unless --out
overrides it. From SDD §5: "chosen so the artifact lands where dev-kickoff already
keeps project memory." Overridable per run; --stdout skips the file entirely and
writes the graph to stdout instead (for CI or piping into salak validate -).
Point --out outside the repository when scanning this repository, or the scan
overwrites its own working tree — use an absolute path or a sibling directory that
isn't reached through ..:
salak scan . --out /path/outside/this/repo/salak-graph.json
Degraded mode and resolution levels
A run with no language server available, or one whose version doesn't match the pin,
is not an error — it's a supported, expected state (rule 6). health_check()
never raises, and a missing or mismatched server is reported as an LSP_VERSION_MISMATCH
warning or plain information in doctor, never as unavailable (decision D45).
Concretely, on this machine right now:
$ salak doctor
Node.js: found (...)
go: ok — tree-sitter-go grammar available; resolution is §4.1 Levels 1-2 from
structure and imports alone, plus a `gopls`-backed Level 3 `calls` pass ...
python: ok — tree-sitter grammars available; resolution is §4.1 Levels 1-2 and needs
no language server (K15/D43). pyright 1.1.411 ...
typescript: ok — tree-sitter grammars available; resolution is §4.1 Levels 1-2 and
needs no language server (K15/D43). typescript-language-server 5.3.0 ...
Every adapter reports ok whether or not a language server is present (D45). For
TypeScript and Python that is because neither adapter's resolution ever queries one:
their edges come from tree-sitter structure and filesystem/alias/barrel heuristics
alone (§4.1 Levels 1–2), tagged extracted only when the relation needs no further
guessing (rule 1, rule 10). Go does query gopls, for calls it cannot resolve
locally; without it the Go scan still completes, but those calls stay unresolved and
the run carries an LSP_UNAVAILABLE warning. The scan.lsp field in the artifact
records whether a server was actually consulted during the run (note N2, D82) — so
it is true only for a run whose Go pass reached gopls.
A broken source file behaves the same way at the file level: a parse failure produces
a PARSE_FAILED diagnostic and a thinner entry for that one file (no declarations,
but its file node and any import edges above the syntax error still appear, D41/D42)
— the rest of the scan completes and the graph is still written (rule 7).
Documentation
| Document | Role |
|---|---|
docs/schema-guide.md |
How to read repo-graph.json as a consumer: node/edge kinds, provenance semantics. |
docs/AI-CONSUMER-PLAYBOOK.md |
Task-oriented companion for an AI assistant consuming repo-graph.json — freshness check first, question-to-field map. |
docs/cli-reference.md |
Every subcommand, flag, and exit code. |
docs/troubleshooting.md |
What each diagnostic code means and how to act on it. |
docs/adding-a-language.md |
Contributor guide for a new language adapter: what is reusable, what is not, the traps already found, and the gates. |
docs/RELEASING.md |
TestPyPI rehearsal and real PyPI publish runbook (owner-only). |
The project's internal specification, plan, and decision history are kept locally and are not part of this public repository.
Development
uv sync
uv run ruff check . && uv run mypy src tests && uv run pytest
mypy is given src tests explicitly — the test suite carries part of the contract
(a Protocol-conformance check lives in a test file), so excluding tests would turn
that guarantee into a comment.
Licence
MIT — see LICENSE.
Chosen 2026-07-29, closing gap-analysis item G2. MIT rather than Apache-2.0 because Salak's job is to unblock other projects: it is a build tool that emits a JSON file, its own §1 says it is "not meant to become a product", and the fewest possible conditions on reuse serves that. Apache-2.0's express patent grant is the one thing MIT lacks; it would be the better choice for something with patentable algorithms or corporate contributors, and Salak has neither. Reversible at no cost until the first PyPI upload, and effectively permanent after.
Release files for salak 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| salak-0.3.1.tar.gz | 1.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| salak-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / salak-0.3.1.tar.gz
| Download URL | salak-0.3.1.tar.gz |
|---|---|
| Size | 1.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
486fe9b47d9de4b152a817c40546df12406665fab2de1986a06d470360288541
|
|
BLAKE2b-256 checksum How to use checksums |
1e5acd1ffa9a826fd37e4f7e18dfd5b5af62a6d1d00f1401cffe763237146041
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / salak-0.3.1-py3-none-any.whl
| Download URL | salak-0.3.1-py3-none-any.whl |
|---|---|
| Size | 386.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
df199731a8d044e7b38dd528257550f214086ff0c583001817c7226bd35aa616
|
|
BLAKE2b-256 checksum How to use checksums |
8c13a0eb4d63b0a3a5ce78b38a1ff66882d35b8ed2b5f4ffce4ff5f307f010f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log