No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Peter Swimm 8e68c944e6
All checks were successful
ci / lint (push) Successful in 17s
ci / test (push) Successful in 18s
Pin the toolchain so local and CI agree
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
2026-09-03 12:17:51 -07:00
.forgejo/workflows Pin the toolchain so local and CI agree 2026-09-03 12:17:51 -07:00
crates Fix five clippy lints that only a newer toolchain reports 2026-09-03 12:07:02 -07:00
.gitignore leylines-core: the layering engine and its foundation 2026-09-03 10:23:39 -07:00
Cargo.lock leylines-cli, CI, and the docs 2026-09-03 11:05:42 -07:00
Cargo.toml leylines-cli, CI, and the docs 2026-09-03 11:05:42 -07:00
LICENSE Initial commit 2026-09-03 14:43:31 +00:00
README.md Pin the toolchain so local and CI agree 2026-09-03 12:17:51 -07:00
rust-toolchain.toml Pin the toolchain so local and CI agree 2026-09-03 12:17:51 -07:00

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.json of sync targets alongside folder-local *.opsl kit manifests, indexes a sample library, and stages SFZ, 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