Rust core for the sase ChangeSpec backend.
This repo is the eventual home of the Rust ChangeSpec parser, query engine, graph index, and bead data backend. The
Python sase_100 repo remains the product shell; this crate owns deterministic core data operations as they are ported.
crates/
sase_core/ # pure-Rust core: wire types + full-file parser
sase_core_py/ # PyO3 extension crate for the sase-core-rs Python package
sase_gateway/ # local host HTTP gateway for SASE mobile clients
The pure crate has no PyO3 dependency. This is deliberate — later UniFFI, WASM, or server crates need to consume
sase_core without dragging a Python toolchain into their build.
sase_gateway is also pure Rust. It owns the mobile gateway HTTP wire contract and server skeleton without depending on
the Python binding crate.
cargo fmt --all
cargo fmt --all -- --check # CI gate
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo run --release --example bench_parse # direct-parser benchmarkMobile gateway hardening subsets:
cargo test -p sase_gateway push_subscription
cargo test -p sase_gateway test_push_provider_records_hint_attempts
cargo test -p sase_gateway listener_smoke_exercises_pairing_auth_and_sessionThe mobile MVP packaging, private remote-access, rollback, and threat-model runbook is maintained in the SASE shell
repo at ../sase_100/docs/mobile_mvp_runbook.md. The gateway README at crates/sase_gateway/README.md documents the
wire routes, push subscription endpoints, and hint-only push boundary.
rust-toolchain.toml pins the stable channel and installs rustfmt and clippy. Cargo.lock is committed so the
workspace builds reproducibly.
release-plz owns the workspace and crate release versions. Normal feature and fix PRs must not edit
[workspace.package].version, crate [package].version, or local path-dependency version pins in Cargo.toml; use
Conventional Commits metadata instead and let release-plz calculate the next version from the merged commits. For a
breaking change on the 0.x line, mark the commit or squash-merge title with ! (for example, feat(core)!: remove legacy API) or include a BREAKING CHANGE: footer so release-plz computes the minor bump.
The Cargo version guard PR check blocks release-owned Cargo version edits outside release-plz branches. The
manual-version PR label is reserved for deliberate release recovery or other explicitly approved version overrides.
Agents working in this repo should leave Cargo versions alone unless the user specifically asks for that recovery flow.
The companion sase_100 repo ships matching just targets so a contributor can drive both repos from one tree:
just rust-install # maturin develop --release into .venv
just rust-test # cargo test --workspace
just rust-fmt-check # cargo fmt --all -- --check
just rust-clippy # warnings-as-errors
just rust-bench # cargo run --release --example bench_parse
just rust-check # fmt-check + clippy + tests
just bench-core # Python-side end-to-end benchmarkAll rust-* targets short-circuit with a friendly message when ../sase-core is not present, so a pure-Python
just install/just check flow is unaffected.
crates/sase_core/src/wire.rs mirrors sase_100/src/sase/core/wire.py:
| Rust type | Python dataclass |
|---|---|
SourceSpanWire |
SourceSpanWire |
CommitWire |
CommitWire |
HookStatusLineWire |
HookStatusLineWire |
HookWire |
HookWire |
CommentWire |
CommentWire |
MentorStatusLineWire |
MentorStatusLineWire |
MentorWire |
MentorWire |
TimestampWire |
TimestampWire |
DeltaWire |
DeltaWire |
ChangeSpecWire |
ChangeSpecWire |
ParseErrorWire |
ParseErrorWire |
crates/sase_core/src/agent_scan/wire.rs mirrors
sase_100/src/sase/core/agent_scan_wire.py (Phase 3B):
| Rust type | Python dataclass |
|---|---|
AgentArtifactScanOptionsWire |
AgentArtifactScanOptionsWire |
AgentArtifactScanStatsWire |
AgentArtifactScanStatsWire |
DoneMarkerWire |
DoneMarkerWire |
AgentMetaWire |
AgentMetaWire |
RunningMarkerWire |
RunningMarkerWire |
WaitingMarkerWire |
WaitingMarkerWire |
WorkflowStateWire |
WorkflowStateWire |
WorkflowStepStateWire |
WorkflowStepStateWire |
PromptStepMarkerWire |
PromptStepMarkerWire |
PlanPathMarkerWire |
PlanPathMarkerWire |
AgentArtifactRecordWire |
AgentArtifactRecordWire |
AgentArtifactScanWire |
AgentArtifactScanWire |
JSON shape rules (enforced by tests):
Option<T>::None→ JSONnull(never omitted).- Empty list fields → JSON
[](nevernull). schema_versionis the first field ofChangeSpecWireso a Rust parser can refuse to deserialize newer records.- Field declaration order matches the Python dataclasses, so byte-for-byte parity is reachable when both sides preserve declaration order.
crates/sase_core/tests/python_wire_parity.rs checks Rust JSON against a captured Python fixture in both directions.
crates/sase_core/src/bead/ mirrors the portable pieces of sase_100/src/sase/bead/ for the bead backend migration:
wire.rsdefinesIssueWire,DependencyWire,StatusWire,IssueTypeWire, validation errors, and operation outcomes.config.rsloads and savessdd/beads/config.jsonusing the same pretty JSON shape as Python.jsonl.rsimports and exportssdd/beads/issues.jsonl, skips corrupt lines, applies legacy defaults, validates records, sorts import rows as Python does for parent-before-child loading, and exports compact JSON sorted by issue ID.schema.rspins the current SQLite schema plus migration fragments for legacy issue type names,is_ready_to_work, and ChangeSpec metadata columns.
crates/sase_core/tests/bead_storage_parity.rs carries the Phase A bead fixtures forward into Rust and checks the
current JSONL/config shape, legacy defaults, tolerant corrupt-line handling, missing-file behavior, and byte-compatible
JSONL export. No production Python code routes through these bead APIs yet; read bindings and store operations land in
later phases.
crates/sase_core/src/parser.rs exposes:
pub fn parse_project_bytes(
path: &str,
data: &[u8],
) -> Result<Vec<ChangeSpecWire>, ParseErrorWire>;Phase 1C handles ChangeSpec boundaries (## ChangeSpec headers, direct NAME: starts, two-blank-line / new-NAME
terminators), the scalar fields NAME, DESCRIPTION, PARENT, PR (legacy CL is accepted), BUG, and STATUS,
and structured section parsing for COMMITS, HOOKS, COMMENTS, MENTORS, TIMESTAMPS, and
DELTAS. Suffix-prefix parsing matches sase.ace.changespec.suffix_utils (including ~!:, ~@:, ~$:, ?$:, !:,
@:, $:, %:, ^:, the legacy ~: plain form, the standalone @/%/^ markers, and the !: metahook | ... →
metahook_complete promotion).
source_span.start_line / end_line are inclusive 1-based and reflect the real last non-blank line of the spec, which
improves on Phase 0's Python placeholder (end_line == start_line).
Python's changespec_to_wire writes end_line == start_line because the Python parser does not track end positions.
Rust tracks real end lines (a deliberate Phase 1 improvement, per sase_100/plans/202604/rust_backend_phase1.md).
crates/sase_core/tests/golden_corpus_parity.rs normalizes Rust's end_line down to start_line before comparing
against the Python golden snapshot, so the rest of the wire is checked byte-for-byte. The real end-line behavior is
exercised by parser unit tests instead. Phase 1F decides whether to backfill end-line tracking in Python or keep this
normalization at the parity boundary.
Currently complete: Phase 1A (workspace + wire types), Phase 1B (scalar parser skeleton), Phase 1C (section
parser parity), Phase 1D (PyO3 binding + Python adapter in sase_100), and Phase 1E (dev workflow, benchmarks,
packaging decision) of sase_100/plans/202604/rust_backend_phase1.md. Remaining work:
- 1F — cross-repo parity gate and handoff.
For Phase 3 (rust_backend_phase3_agent_scan.md): Phase 3B added the
pure-Rust artifact filesystem snapshot scanner under
crates/sase_core/src/agent_scan/ and parity tests in
crates/sase_core/tests/agent_scan_parity.rs. The PyO3 binding for
scan_agent_artifacts lands in Phase 3C.
crates/sase_core/src/agent_scan/scanner.rs exposes:
pub fn scan_agent_artifacts(
projects_root: &Path,
options: AgentArtifactScanOptionsWire,
) -> AgentArtifactScanWire;It walks projects_root/<project>/artifacts/<workflow>/<timestamp>/ for
the workflow folder families pinned in agent_scan_wire.py
(ace-run, run, fix-hook, crs, summarize-hook, plus mentor-*
and workflow-* prefixes), and parses the marker files agent_meta.json,
done.json, running.json, waiting.json, workflow_state.json,
plan_path.json, and prompt_step_*.json. Soft errors (unreadable
directories, malformed marker JSON, marker JSON whose top level is not a
JSON object) are absorbed silently and counted on
AgentArtifactScanStatsWire. Records are sorted by
(project_name, workflow_dir_name, timestamp) before returning, matching
scan_agent_artifacts_python in sase_100.
crates/sase_core_py is a cdylib that builds the Python extension module
sase_core_rs. Among its JSON-shaped APIs are:
sase_core_rs.parse_project_bytes(path: str, data: bytes) -> list[dict]
sase_core_rs.axe_status_wire_schema_version() -> int
sase_core_rs.classify_axe_status(request: dict) -> dictThe result is plain Python dict/list/str/int/bool/None mirroring the ChangeSpecWire JSON shape — no PyO3
classes leak across the boundary in Phase 1. A Rust ParseErrorWire is surfaced as a Python ValueError whose message
is the wire error's Display form ("kind: message (file_path)").
For AXE status, Python supplies already-collected lock, process, marker,
runner, and lumberjack observations. Rust performs pure validation,
normalization, and classification without filesystem or process I/O and
returns only plain Python dictionaries, lists, scalars, and None.
Building the wheel requires a Python interpreter on the host (maturin develop or maturin build from
crates/sase_core_py). It is opt-in: the Python sase install does not require Rust, and SASE_CORE_BACKEND=python
(the default) ignores the binding entirely. The is_rust_available() probe in sase.core.backend lazy-imports
sase_core_rs, so a missing module never breaks startup.
Phase 1E adds two benchmark harnesses so future phases can decide whether to default SASE_CORE_BACKEND to rust:
- Rust direct —
cargo run --release --example bench_parsefrom this repo (orjust rust-benchfromsase_100). Times only the pure Rust parser over the golden corpus and a synthetic multi-spec file. No Python in the loop. - End-to-end Python —
python tests/perf/bench_core_parse.pyinsase_100(orjust bench-core). Times Python direct, Python facade,sase_core_rs.parse_project_bytesdirect, the Rust facade (PyO3 + dict→ChangeSpecWirerehydration), and theSASE_CORE_DUAL_RUN=1overhead. The facade number is the one to compare against Python — the direct number isolates how much of the cost is PyO3/dict marshaling vs. parsing.
Both harnesses accept --num-specs and --runs flags so they can be tuned for noise floor vs. wall-clock budget.
Current status: this Phase 1E decision has since been superseded.
sasenow declaressase-core-rsas a hard runtime dependency with no pure-Python fallback for ported operations, and the recommended install isuv tool install sase, which pulls a prebuilt wheel on supported platforms. The notes below are kept as the historical record of the original opportunistic-detection rollout.
sase does not depend on a built sase_core_rs wheel. The extension is detected opportunistically at import time:
- Pure-Python install:
pip install sase(orjust install) succeeds with no Rust toolchain.is_rust_available()returnsFalse,parse_project_bytesruns through the Python implementation. - Rust-enabled install: a contributor runs
just rust-install(which usesmaturin develop --releasefromcrates/sase_core_py). After that,is_rust_available()returnsTrueandSASE_CORE_BACKEND=rust(or dual-run) routes through the binding. - CI:
just checkdoes not invoke any Rust target, so Python-only jobs cannot be broken by Rust packaging churn. A separatejust rust-check(fmt-check + clippy + tests) runs on demand.
This keeps the rollout reversible: Phase 1F can flip the default to rust without touching the install path, and a
future hard dependency on the wheel is a separate decision that can be staged behind an extra (pip install sase[rust])
if it ever becomes desirable.
Dual-licensed under MIT or Apache-2.0, at your option.