Spec-to-code traceability and invariant checks for Python repositories.
$ uv tool install backstitch
$ cd your-project
$ mkdir -p docs/specs docs/plans src
$ backstitch check --spec-root docs/specs --code-root srcBackstitch verifies that spec requirements point to real implementation owners, code points back to the requirements it implements, and declared invariants are bound to real tests. It runs as two lanes under one policy layer:
- Trace lane — deterministic facts about declared relationships. No model, byte-stable reports, hard-gate by default.
- Semantic lane — bounded model review of finite, reproducible packets
through the
llmecosystem, without open-ended repository access. Advisory by default; every verdict is evidence-bound and cached as a frozen, replayable artifact. - Policy layer — repository configuration decides which findings, at which evidence class, become blocking. The decision to block is always a reviewable config diff, never a model output.
Backstitch does not promise that models prove correctness. It promises that semantic review becomes repeatable enough to run in CI, diffable enough to audit, and structured enough to improve. The workflow stays deterministic even where one lane uses probabilistic judgment, because everything around the judgment is contract: what evidence the model sees, which requirement it judges, which prompt and model produced the verdict, what shape must come back, how uncertainty is represented, and how severities map to exit codes.
Backstitch is a standalone tool. Weft is a reference target and eventual consumer, not a package dependency.
- Spec-driven Python projects. Stable requirement IDs and reciprocal links make intended behavior navigable from docs to code and back.
- Repositories maintained by coding agents. Deterministic ownership, diagnostics, and acceptance gates replace prose-only compliance claims.
- Critical behavior that needs explicit test bindings. Required and draft invariants make missing, malformed, duplicate, or misplaced bindings visible.
- Teams that want bounded semantic review. Backstitch generates finite, reproducible packets before any model call and validates returned evidence.
- Backstitch
- Bidirectional traceability - Resolves spec mappings and Python backlinks
- Invariant checking - Connects required or draft guarantees to real tests
- Stable diagnostics - Canonical names, short aliases, contexts, and levels
- Configurable policy - Ordered selectors,
fail_on, and suppressibility - Documented suppression - Governed exceptions require spec declarations and remain auditable with their rationales
- Deterministic reports - Stable JSON, ordering, locators, and content hashes
- Bounded semantic review - Section and invariant packets with evidence limits
- Runtime-independent parsing - CommonMark and modern Python syntax support
- Self-application - Backstitch's own repository is a strict acceptance corpus
# Install the CLI globally with uv (recommended)
uv tool install backstitch
# Or with pipx
pipx install backstitch
# Or add it to a project environment
uv add --dev backstitch
pip install backstitchFor a source checkout:
$ uv sync --extra dev
$ uv run backstitch --version
backstitch 0.3.0Requirements:
- Python 3.11+
llm,markdown-it-py,tree-sitter, andtree-sitter-python(installed automatically)
Repository commands currently require a POSIX system, including Linux and macOS, with the no-follow descriptor primitives defined in EVC-8.2; Windows is not supported.
For a project with application code under src, create the scan roots and add
the repository profile:
$ mkdir -p docs/specs docs/plans src tests# pyproject.toml
[tool.backstitch]
default_command = "check"
[tool.backstitch.profile]
name = "backstitch-style-v1"
spec_roots = ["docs/specs"]
plan_roots = ["docs/plans"]
code_roots = ["src", "tests"]
test_roots = ["tests"]Give a requirement a stable ID and map it to an exact repository-relative implementation path:
## Authentication boundary [AUTH-1]
The request path must reject unauthenticated callers.
_Implementation mapping_:
- `src/auth.py`Add the reciprocal backlink in the owning Python docstring or a nearby comment:
def authenticate(request: Request) -> User:
"""Authenticate one request.
Spec: docs/specs/auth.md [AUTH-1]
"""Run the deterministic checker from the repository root:
$ backstitch obligation list
$ backstitch obligation docs/specs/01-example.md#EXAMPLE-1 --summarize-evidence
$ backstitch obligation docs/specs/01-example.md#EXAMPLE-1 --find-evidence
$ backstitch guide alignment
$ backstitch
$ backstitch check
$ backstitch coverage
$ backstitch check --format json --output spec-trace.json
$ backstitch coverage --format json --output intent-coverage.json
$ backstitch check --show-suppressionsThe obligation reads help bootstrap alignment before it is complete. Declared evidence comes only from source mappings, backlinks, invariant binds, and binding tests. Discovered candidates are deterministic review advice; a human or agent must still author an ordinary source diff, and Backstitch never applies that diff itself.
The built-in backstitch-style-v1 profile defaults to docs/specs for specs
and backstitch plus tests for code. Override roots in configuration or with
repeatable --spec-root, --code-root, and --test-root options.
Bare backstitch delegates to the repository's configured default command.
Global configuration options may appear before a command. Command-specific forms are also available where relevant.
--config PATH- Use exactly one configuration file; skip discovery--no-config- Skip repository configuration; packaged defaults still load--version- Show the installed Backstitch version--help- Show help
Common deterministic examples:
$ backstitch obligation list --repo-root .
$ backstitch obligation docs/specs/01-example.md#EXAMPLE-1 --find-evidence
$ backstitch check --repo-root . --warnings-as-errors
$ backstitch check --repo-root . --format json --output spec-trace.json
$ backstitch coverage --repo-root . --format json --output intent-coverage.json
$ backstitch packets --repo-root . --kind invariant --output invariants.jsonl
$ backstitch packets --repo-root . --kind all --output packets.jsonlRun backstitch <command> --help for the full option set.
coverage report mode works without Git and uses the same immutable snapshot
and resolved trace graph as check. A whole-file edge is inherited coverage;
an exact symbol, non-module owner, invariant declaration, or invariant binding
is direct coverage. Reasoned inline or configured exemptions account for
deliberate glue without manufacturing spec text. Ratchet mode compares the
accepted snapshot with a repository-owned Git baseline and must be pinned in
CI with --require-ratchet REF.
0- Command completed and no effectivefail_onlevel was present1- The target repository has a finding at an effective failing level2- Arguments, configuration, input artifacts, output, or the tool failed
Exit 1 describes the target repository. Exit 2 describes the invocation or
tool. Backstitch contains per-file failures where possible and never exposes a
Python traceback as normal CLI output.
Backstitch parses Markdown sections with IDs such as [AUTH-1], implementation
mapping blocks, and Python references in module, class, function, and method
docstrings or comments. Mappings use exact repository-relative paths. Bare IDs
resolve only when unique; Backstitch reports ambiguity instead of guessing.
The deterministic report contains normalized sections, code references, mappings, resolved edges, invariants, binds, issues, and summary counts. Identical inputs produce stable JSON ordering.
Declare behavior in an owning Python docstring or inside an ID-bearing spec section:
def resolve() -> Report:
"""Build the deterministic report.
Invariant: [INV.RES.1] Identical inputs produce byte-identical JSON.
"""Bind the invariant inside a real test definition:
def test_report_is_stable() -> None:
"""Tests-invariant: [INV.RES.1]"""Use Invariant (draft): for an advisory declaration. Under packaged defaults,
an untested required invariant is an error and an untested draft invariant is a
warning. packets --kind invariant includes bounded target and binding-test
snippets for advisory semantic review.
Diagnostics have canonical names such as CODE_REF_UNMAPPED_FROM_SPEC and
stable short aliases such as BSC008. Rules are ordered and the last matching
rule wins:
[tool.backstitch.diagnostics]
fail_on = ["error", "warning"]
suppressible_levels = ["warning", "info"]
[[tool.backstitch.diagnostics.levels]]
select = ["BSC*", "INVARIANT_UNTESTED:draft"]
level = "warning"Selectors accept canonical codes, short codes, *, family prefixes, and
supported contexts. A diagnostic set to off is omitted from normal findings
but remains available in suppressed_issues under --show-suppressions.
Repositories may opt into documented suppression governance:
[tool.backstitch.lint]
require_suppression_declarations = true
[[tool.backstitch.lint.suppressions]]
mechanism = "ignore"
path = "tests/*"
sections = []
codes = ["CODE_REF_UNMAPPED_FROM_SPEC"]
declaration = "docs/specs/04-exclusions.md#SUP-TEST-CITATIONS"The referenced spec contains a CommonMark declaration such as
_Traceability: suppression-declaration [SUP-TEST-CITATIONS] "Tests cite the contract but are not implementation owners."_. Strict mode leaves the
original finding active when the declaration is invalid and reports every
applied declaration and decoded rationale through --show-suppressions.
Backstitch always starts with packaged defaults, then applies repository
configuration, supported environment values, and explicit CLI options. It
searches upward for the nearest .backstitch.toml or pyproject.toml with a
[tool.backstitch] table. A standalone file uses the same key layout without
the tool.backstitch prefix. Explicit --config PATH and extend may name
any TOML filename; those names never become implicit discovery conventions.
[tool.backstitch]
default_command = "check"
[tool.backstitch.profile]
name = "backstitch-style-v1"
spec_roots = ["docs/specs"]
code_roots = ["src", "tests"]
test_roots = ["tests"]
[tool.backstitch.check]
format = "text"
warnings_as_errors = falsedefault_command accepts false, "check", or "analyze". Packaged
defaults use false, so a repository must opt in. "check" is equivalent to
backstitch check --repo-root .; "analyze" is equivalent to
backstitch analyze --repo-root .. The value is one closed command name, not
a shell command or a list, and it cannot contain arguments. Arguments typed
after backstitch are forwarded to the selected command. A leading path is
--repo-root shorthand, so backstitch . works, and an analyze default accepts
the usual flags such as backstitch --model gpt-5.6-luna. Set false in a
child config to disable an inherited default.
Bare "analyze" has the same credential reads, cache writes, bounded provider
calls, and possible cost as explicit current-repository analysis. Do not use
bare invocation in secret-bearing hostile-target automation. Such workflows
must name analyze, the trusted config, and workflow-owned overrides
explicitly.
Roll out a Backstitch version that supports default_command before adding the
key to downstream repositories. Remove the key before downgrading to an older
strict version, which will correctly reject it as unknown.
One invocation-scoped resolver applies the cascade `CLI > defined environment
selected config > extended config > packaged defaults`, then passes an immutable settings object into command code. Repeat a generic override before or after a config-consuming command:
backstitch --option analyze.cache_mode read-write analyze ...
backstitch config show --option check.warnings_as_errors trueKEY is a known runtime-consulted dotted leaf. VALUE is parsed as one TOML
value when possible, otherwise as a bare string. Quote ambiguous strings.
summarize-analysis, guide, and cache cleanup-lock reject configuration
controls because they do not consume settings.
Test roots classify paths within code roots. Replacing code_roots without
also supplying test_roots resets test roots for that configuration layer;
every nonempty final test root must be contained by a final code root.
Deterministic checks never call a model. Generate packets explicitly, then analyze and summarize them:
$ backstitch check --show-suppressions --format json --output spec-trace.json
$ backstitch packets --kind all \
--output packets.jsonl \
--report packet-report.json
$ backstitch analyze \
--packets packets.jsonl \
--packet-report packet-report.json \
--output analysis.jsonl \
--report analysis-report.json
$ backstitch summarize-analysis \
--deterministic-report spec-trace.json \
--analysis-results analysis.jsonlPackets bind section, invariant, or documented-suppression intent; code snippets, tests, deterministic findings; and exact evidence regions shown to the model. The provider receives a packet-derived response schema, but model output remains untrusted: Backstitch owns packet identity, validates structured rows and evidence locality, and contains malformed output per packet.
The committed cloud default selects GPT-5.6 Luna through llm 0.33's OpenAI
Responses adapter. Backstitch sends logical max_tokens = 16384 and
reasoning_effort = "max"; the adapter serializes those as
max_output_tokens and reasoning.effort. Temperature and seed are absent.
Optional request fields have no omission sentinel: an absent key accepts the
provider default, while any present value is validated, identity-bearing, and
frozen before the adapter applies provider wire spelling. The provider's JSON
Schema envelope may use strict = false;
Backstitch's closed, packet-local normalizer remains authoritative.
Semantic verdicts can be stored in an immutable, content-addressed cache.
cache_mode = "read-write" calls the provider only for misses; "require"
forbids provider calls and fails on a miss. Cache identity includes the packet,
prompt, provider, request controls, contract version, and explicit search
epoch. Policy is deliberately excluded, so a policy-only change reprojects the
same frozen result with zero calls.
result_reuse = "evidence-stable" is the default. For each unchanged
packet/prompt/request/epoch review identity, the first complete result remains
in force even when a later run selects a different model. Changed evidence is
an ordinary miss under the newly selected model. Set
result_reuse = "exact-inference" to require the selected provider's exact
cache key, or change search_epoch to resample the evidence-stable decision.
Reports preserve the original producing provider for carried results.
Model age, provider qualification, and elapsed time do not invalidate an
unchanged evidence-stable baseline. Release candidates qualify the exact
current Luna and protected GPT-5.5 requests live; qualification is bounded
event evidence, not a persisted or expiring Backstitch artifact.
Trusted model descriptors use Model Monster pkg:service PURLs as stable
identity, for example pkg:service/openai.com/gpt-5.6-luna. The separate
adapter_model_id is the raw name passed to llm. LLM_MODEL and --model
may select a trusted descriptor by its PURL or by an unambiguous adapter model
ID; provider identity remains the PURL.
The repository ignores .backstitch/. Treat its semantic cache as disposable
acceleration state and its reports as fresh run outputs, not as source or
reviewed evidence to commit. The usual local flows are:
- update findings with bounded provider calls by explicitly selecting the
trusted
pyproject.tomland applying the reviewed staticread-writeoptions; valid hits are reused and only misses call the provider; - replay with zero provider calls by overriding
cache_mode = "require"; a missing baseline or exact object fails with exit2; - diagnose without reading or writing cache objects by using a trusted config
whose
[analyze] cache_modeis"off"; and - retain a deliberate new sample by changing the trusted
search_epoch(and verifiersearch_epochs, when verification is enabled) before aread-writerun.
When no Backstitch process is using it, the whole .backstitch/ directory can
be removed. The next read-write run rebuilds needed objects; the next
require run reports misses. Backstitch does not currently provide
fine-grained pruning or concurrent whole-root deletion. A durable reviewed
snapshot for indefinite provider-free replay would be a separate future
feature, not a use of this cache.
The analysis report records completeness, cache hits and misses, provider
calls, budgets, evidence-bound diagnostics, candidate debt, applied policy,
problems, and the result digest. Exit 0 means the required review completed
and policy allowed it. Exit 1 is reserved for policy-failing human or
mechanically verified target findings. Exit 2 means the invocation, cache,
provider, normalization, completeness, budget, or publication failed.
Measure one exact semantic identity against the committed mutation and negative-control corpus with:
$ backstitch eval \
--corpus tests/semantic_eval/v3/manifest.json \
--config pyproject.toml \
--option analyze.cache_mode read-write \
--option verify.enabled true \
--option verify.cache_mode read-write \
--output semantic-eval-report.jsonThe committed schema-3 smoke corpus exercises the current evaluator in report mode only. The eval report separates evidence sufficiency from conditional and end-to-end recall. It also records precision, false-positive and indeterminate rates, uncached flip rate, required-cache replay stability, cost, and latency. Report mode is observational. Enforce mode can grant stronger policy authority only after a reviewed historical corpus, a passing report for the exact analyzer/verifier composition, and an explicit human-pinned report digest.
Use backstitch doctor --probe before semantic analysis to check model
registration, credentials, constrained decoding, and endpoint reachability.
See the semantic gate implementation guide
for cache trust, refresh, replay, evidence-authority, and rollout details.
Backstitch uses markdown-it-py for CommonMark block structure and
tree-sitter-python for Python ownership, comments, and docstrings. Running on
Python 3.11 can therefore analyze newer target syntax such as PEP 695 generics
and PEP 701 f-strings without relying on the host interpreter's ast grammar.
Backstitch's own committed repository config deliberately selects
default_command = "analyze". Bare backstitch here is provider-capable; use
explicit backstitch check --repo-root . for the hermetic self-corpus gate.
The repository pytest configuration runs the live cloud-provider contract test
in the default local suite. A normal local run needs a working llm model and
credential:
$ uv run pytest -qFor an intentionally hermetic run:
$ uv run pytest -q -m "not live_llm and not benchmark"
$ uv run pytest tests -q -n 0 -m benchmark
$ uv run pytest tests/live/test_live_llm.py -q -o run_live_llm=falseThe serial benchmark lane runs each real self-corpus command once to warm up and five times for a reported median. It always fails command errors, timeouts, and catastrophic regressions. Relative regression checks remain explicitly unavailable until the repository pins a matching wall-clock runner contract and content-bound baseline.
To reproduce the CI coverage report locally:
$ uv run pytest tests -q -n auto --dist loadgroup -m "not live_llm and not benchmark" --cov=backstitch --cov-report=term-missingThe completion gate also includes:
$ uv run pytest tests/acceptance -q
$ uv run ruff check backstitch tests bin
$ uv run ruff format --check backstitch bin .github/scripts tests
$ uv run mypy backstitch bin/release.py tests
$ uv run backstitch check --repo-root . --show-suppressionstests/live/test_live_llm.py drives the real packets to analyze to check
to summarize-analysis path. It asserts structured contracts, transport, and
model success for cloud runs, not exact wording or classification. The cloud
fixture includes a documented suppression packet and immediately proves a
zero-call require replay after the live read-write miss.
# Store a provider key once, then run the local-default live test
$ uv run llm keys set openai
$ LLM_MODEL=gpt-4.1-mini uv run pytest tests/live/test_live_llm.py -q
# Or use a provider environment variable
$ OPENAI_API_KEY=... LLM_MODEL=gpt-4.1-mini uv run pytest -m live_llm -qThe same test supports a loopback OpenAI-compatible endpoint such as Ollama:
$ docker run -d --name backstitch-llm \
-p 127.0.0.1:11434:11434 \
-v "$PWD/.ollama-cache:/root/.ollama" \
ollama/ollama
$ docker exec backstitch-llm ollama pull llama3.2:3b
$ BACKSTITCH_LOCAL_LLM_UPSTREAM=http://127.0.0.1:11434/v1 \
BACKSTITCH_LOCAL_LLM_SERVED_MODEL=llama3.2:3b \
BACKSTITCH_LIVE_LLM_KIND=local \
uv run pytest -m live_llm -qCloud tests cost money and can fail because of provider outages, rate limits,
or model retirement. Local endpoints avoid provider credentials but add model
startup and output-quality variance. See
docs/implementation/06-choosing-a-local-model.md for measured local-model
guidance.
The hermetic CI matrix always deselects live tests and receives no provider
secret. The manual semantic-refresh workflow binds its trusted tool checkout
to the dispatch run's exact default-branch revision, requires the
OPENAI_API_KEY repository secret, restores only validated immutable cache
object trees, runs the bounded dogfood and eval refresh, and uploads fresh
review reports. Cache service state is optional and untrusted. Missing
credentials, incomplete analysis, or corrupt restored objects fail; cache
eviction causes a bounded cold refresh. The workflow never commits or pushes
cache or report content. It uses repository_dispatch, whose run ref is the
default branch, instead of branch-selectable workflow_dispatch. Trigger it
with write access:
$ gh api --method POST repos/{owner}/{repo}/dispatches \
-f event_type=semantic-refreshRepository collaborators with permission to create a repository dispatch can request a report-only analysis of an open pull request. Supply the PR number and the exact lowercase 40-hex head SHA shown by the GitHub API:
$ gh api --method POST repos/{owner}/{repo}/dispatches \
-f event_type=semantic-pr-report \
-F 'client_payload[pull_request_number]=123' \
-f 'client_payload[head_sha]=0123456789abcdef0123456789abcdef01234567'That workflow runs trusted Backstitch code, dependencies, prompts, provider controls, and configuration from the exact default-branch workflow revision. It treats the API-confirmed PR checkout as read-only data and never installs or executes its workflows, hooks, plugins, build configuration, or commands. The provider receives only the bounded packet projection selected by trusted Backstitch: relevant specification text, code and test evidence, deterministic findings, and exact evidence regions. Provider credentials exist only during the credential check and analysis step. The resulting artifact is observational; it does not comment on the PR, create a status, or gain merge authority.
The separate local-llm workflow owns the Ollama canary.
The local release helper runs regular, cloud-live, and local-live checks before creating and pushing a version tag. The tag-triggered workflow publishes to PyPI through Trusted Publishing and creates the GitHub Release.
$ bin/release.py --version X.Y.Z --dry-run
$ bin/release.py --version X.Y.Z
# When version files and CHANGELOG.md are already prepared
$ bin/release.py all --dry-run
$ bin/release.py allSee docs/implementation/05-release-publishing.md for release setup, rollback,
and verification.
Backstitch is released under the MIT License. See LICENSE.
