- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
CI enforces `clippy -D warnings`. Paired with a floating `stable` that breaks on Rust's release schedule rather than on anything a commit did: this repo's very first CI run went red because the runner resolved `stable` to 1.98 while the code was written against 1.95, and clippy 1.98 knows lints 1.95 does not. Every ~6 weeks that recurs, turning an unrelated commit red and sending whoever pushed it to diagnose a difference they cannot see locally. Pinning makes `cargo clippy` mean the same thing everywhere. Bumping becomes a deliberate change -- raise the version, fix what the newer lints find, commit the two together -- instead of a surprise. Worth noting this diverges from the other Rust repos on the forge (bridges, ritual-engine, opsl carry no rust-toolchain.toml at all). The difference is that none of them combines a floating toolchain with a deny-all-warnings gate, which is the specific pairing that fails. 230 tests, clippy clean, fmt clean under the pinned 1.98.1. Claude-Session: https://claude.ai/code/session_01PtQXw2Jmqb8ciNab79DLUC |
||
| .forgejo/workflows | ||
| crates | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
| rust-toolchain.toml | ||
leylines
A layering engine for manifests. Load partial configuration from several places, resolve what it selects, map the results onto named destinations, and say what would happen before anything happens.
Those four steps are the whole library. They are also, unchanged, what two quite different tools already do:
- opsl reads a global
patch_manifest.jsonof sync targets alongside folder-local*.opslkit manifests, indexes a sample library, and stagesSFZ, Ableton, LSDJ, and M8 outputs. - spelwork compares a tracked
deploy/tree with what is live on the forge host and reports drift.
The vocabulary differs — kits and banks against services and compose files — but
the machinery does not. The machinery lives in leylines-core; the vocabulary
lives in an adapter per caller.
Layout
crates/leylines-core/ the engine — layer, resolve, plan, diff
crates/leylines-opsl/ patch_manifest.json + *.opsl -> layers
crates/leylines-spelwork/ .spel manifests + deploy/ -> layers
crates/leylines-cli/ the `leylines` binary
crates/leylines-testkit/ temp-tree helper, dev-only
230 tests. No third-party dependencies, asserted in CI.
Using it
opsl links leylines-opsl directly. spelwork's bash shells out to the binary,
keeping the ssh it already has:
ssh toilville-forge 'sha256sum /opt/spelwork/docker-compose*.yml' \
| leylines drift --root . --adapter spelwork --observations -
# exit 0 in sync, 1 drift, 2 error — the same codes bin/forge-deploy-check uses
Four commands:
plan |
every write and backend invocation resolution implies |
drift |
the same, compared with what a host reports |
targets |
the flattened targets and rules, in precedence order |
explain |
which rules matched one entity, and which one won |
explain is what makes a layered configuration debuggable. Against the real
sample library:
$ leylines explain --root .../samplelib --manifest .../patch_manifest.json \
--entity 'sample:Drums/1artists/Circle/OCTO KIT/DRY/KICK/[DRY] OCTO_KICK_V4_RR2.wav'
-> ableton-local
lost [1] circle.opsl#Circle-0
WON [4] octo-kit-dry.opsl#OCTO KIT DRY-0
-> lsdj-staging
lost [1] circle.opsl#Circle-3
WON [4] octo-kit-dry.opsl#OCTO KIT DRY-3
A rule that lost is shown, not merely absent — that is usually the question being asked.
The decisions worth knowing
Globals own targets; locals may not define them. A kit manifest sitting beside its samples says which named target it wants and with what options, never where that target writes. Target paths are a property of the machine, and a manifest that travels with its content should not carry them.
A folder-local layer cannot reach outside its folder. Every selector it
writes is re-anchored into its scope when the stack is flattened, so **/*.wav
in Drums/1artists/Circle/OCTO KIT/DRY/octo-kit-dry.opsl becomes
Drums/1artists/Circle/OCTO KIT/DRY/**/*.wav and cannot claim a file in
ALIVE/. A .. inside a glob is rejected rather than resolved; a path selector
cannot carry an escape at all, because RelPath resolves or refuses every ..
when it is built.
Precedence is globals-then-locals, and later wins. Globals come first in the
order they were added, so forge-manifest.spel followed by
forge-manifest.local.spel gives the local file the last word. Folder-local
layers sit above every global, shallowest first, so a deeper folder refines a
shallower one. Two rules of equal precedence — meaning two rules in one file —
claiming the same entity for the same target is a mistake in that file and is
reported.
A backend means a grouped call. A target with no backend is written to directly, once per entity. A target with one is handed its whole group in a single invocation, because that is the shape such a tool takes: patching an LSDJ ROM with a bank of samples is one call carrying the bank.
Present and current are different answers. Diffing is digest-based, and the
statuses stay distinct: Absent, Present (something is there, but nothing to
compare it with), Current, Stale, Unknown (nobody looked). Only Stale is
drift; only Current means there is no work to do.
Config is separated from content during the walk. A single walk returns
manifests and content as two lists, so a *.opsl cannot be indexed as one of
the artifacts it describes.
The engine decides; callers act. Nothing here opens a destination, runs a
backend, or reaches a remote host — not even the binary. drift reads
sha256sum output that someone else collected. Plan::set_expected is the
seam: a caller renders content, digests it, and hands the digest back, so the
next diff can tell current from merely present.
Imperfect manifests degrade rather than refusing. The real library has four
kits naming an m8-local target that patch_manifest.json never defines.
Treating that as fatal would make the whole library unloadable over one absent
line, so the rule is dropped and a warning says why; --strict restores the
error.
Why no dependencies
opsl is already a dependency-light Rust CLI and links this crate directly, while
spelwork's bash shells out to a binary. Zero dependencies is the only shape that
serves both without FFI or a vendoring argument. The cost is a hand-written
JSONC reader, .spel reader, glob matcher, SHA-256, directory walk, argument
parser, and JSON writer.
SHA-256 specifically, rather than a faster non-cryptographic hash, because a
digest computed here has to be comparable with what sha256sum prints on a
remote host. That is how the drift check learns whether a deployed file is
current, and tests/digest.rs checks every vector against shasum -a 256.
Tests
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
rust-toolchain.toml pins the exact compiler, so those three commands mean the
same thing locally as they do in CI. That pin is not incidental: CI enforces
-D warnings, and with a floating stable the first run went red purely
because the runner had a newer clippy than the author did. Bumping the pin is a
deliberate change — raise the version, fix what the newer lints find, commit
both together.
Two tests are #[ignore]d because they need real local data, and they are the
ones that found the most:
# The 547,360-file sample library: nested kits, an undefined target,
# and 2,000 files with an uppercase .WAV extension.
LEYLINES_SAMPLELIB="$HOME/Music/Ableton/User Library/Samples/samplelib" \
LEYLINES_PATCH_MANIFEST="…/opsl-data/canonical/samplelib/patch_manifest.json" \
cargo test -p leylines-opsl -- --ignored --nocapture
# The real 8KB forge-manifest.spel, tables and all.
LEYLINES_SPEL_FIXTURE="$HOME/.spel/forge-manifest.spel" \
cargo test -p leylines-spelwork -- --ignored --nocapture