docs(design): C30 — stability discipline: artifacts stable, code movable

Decide #296: no Rust-API stability before 1.0 (no semver, no shims; the
workspace version communicates nothing). Two consumer classes with two
mechanisms — native node crates ride the engine checkout as path deps
(what the scaffolder already emits), external embeddings pin a rev. The
compatibility promise attaches to the artifact plane (C18/C29/invariant
8), never to Rust API, with an erosion guard against 'stable API' claims.

The skeptic pass surfaced that a frozen 0.1.0 disarms the cdylib
handshake's CORE_VERSION refusal; the contract commits the load seam to a
build-identity fingerprint instead (transition tracked as #348).
Decision minutes: #296 (options a-e, rationale).

closes #296
This commit is contained in:
2026-07-26 16:15:03 +02:00
parent 024e8652c0
commit d87f534e85
3 changed files with 95 additions and 0 deletions
+7
View File
@@ -201,6 +201,13 @@ the repo, not session memory, is the source of truth.
optional gated document `description`); descriptions never influence
execution or identity ids, and registered artifacts are never
retroactively invalidated.
- **[C30 — Stability discipline: artifacts are stable, code is movable](contracts/c30-stability-discipline.md)** —
No Rust-API stability before 1.0 (no semver, no shims): node crates ride
the engine checkout as path deps, external embeddings pin a rev, and the
cdylib handshake refuses non-identical aura-core builds (build-identity
stamp, #348). The compatibility promise attaches to the artifact plane
(C18/C29/invariant 8); no surface may claim "stable API" while this
contract stands.
---
@@ -0,0 +1,79 @@
# C30 — Stability discipline: artifacts are stable, code is movable
**Guarantee.** The project's compatibility promise attaches to recorded
data, never to Rust API. Before 1.0 the workspace's library surface
promises nothing between commits; every cargo consumer either moves with
the engine checkout (path-dep node crates, staleness refused at the load
handshake) or pins a rev deliberately (external embeddings). The one
handshake guarding the ABI seam compares build identity, never a
hand-maintained version.
1. **No API stability before 1.0.** Signatures, module homes, crate
rosters, and trait shapes move freely, commit over commit — no
deprecation cycles, no compat shims, no semver bumps. The workspace
version (0.1.0) is not a communication channel; nothing downstream
reads it. What a change may *not* silently break is behaviour pinned
by the artifact plane (point 4).
2. **Two consumer classes, two mechanisms.**
- A **native node crate** (scaffolded by `aura nodes new`) consumes
the engine as a cargo *path* dependency into the local engine
checkout — the scaffolder emits exactly this
(`crates/aura-cli/src/scaffold.rs`, template `CARGO_TOML`). Node
crate and engine move together: cargo rebuilds the crate when
aura-core changes, and a dylib from any other engine state is
refused at load (point 3), never trusted.
- An **external embedding** (a World program in its own repo)
consumes the workspace crates as a cargo git dependency pinned to a
rev. The pin is the whole contract: updating it is a deliberate act
that accepts whatever moved, with the design ledger and commit
bodies as the changelog.
3. **The load handshake refuses non-identical builds.** The cdylib
handshake already refuses a rustc mismatch (`RUSTC_VERSION`, emitted
by build.rs). The aura-core stamp must be equally honest: a
build-identity fingerprint derived mechanically from aura-core's
sources, so a node dylib loads only against the aura-core it was
built from. A hand-maintained version that never moves disarms the
refusal (the pre-C30 state: `CORE_VERSION` frozen at 0.1.0 matched
every stale dylib); a hand-bumped one re-arms it only between bumps —
an implicit ABI promise this contract refuses to make. *(Transition:
the shipped handshake still stamps the crate version; re-arming it is
tracked as #348.)*
4. **The artifact plane is the stable tier.** What was already law,
stated as the positive promise: registered artifacts are never
retroactively invalidated ([C29](c29-self-description.md)); document
format changes carry `format_version` with readers keeping old forms
readable ([C18](c18-registry.md)); deploy artifacts are frozen
([C13](c13-hot-reload-frozen-deploy.md), invariant 8). Compatibility
work happens here: a change that would orphan recorded artifacts is a
design decision for the ledger, never a refactor.
5. **The erosion guard.** No doc, README, or scaffold may describe any
Rust surface as "stable API" while this contract stands. Inviting a
consumer means naming the point-2 mechanism (path or pin), never
promising stillness. Amending this contract is the only way to
promise more.
**Rationale.** #296 recorded the tension: an invited external consumer
(#295 deliberately enlarges the public library surface) versus a working
discipline of behaviour-preserving reshaping ([C28](c28-stratification.md)
relocated whole rosters twice in one pass). The resolution promises where
the project can keep promises — the recorded-data plane, whose guarantees
already exist — and refuses to promise where it cannot. The empirical
ground at decision time: the one real downstream project is data-only (an
`Aura.toml` directory with no `Cargo.toml`, consuming the released binary
and documents), so the only cargo consumers are engine-scaffolded node
crates (path-dep by construction) and the engine's own fieldtest
fixtures. A stability promise to nobody would cost every cycle and inform
no one; hardening toward 1.0 stays available at any time, while stability
promised early is practically irrevocable.
## See also
- [C13](c13-hot-reload-frozen-deploy.md) — frozen deploy artifacts (the promise at the deploy edge)
- [C16](c16-engine-project-split.md) — engine/project split; reuse is cargo-native (invariant 9)
- [C18](c18-registry.md) — the run registry and `format_version` (the artifact plane's mechanics)
- [C28](c28-stratification.md) — the movable crate ladder this contract licenses
- [C29](c29-self-description.md) — registered artifacts never retroactively invalidated
- Decision record: #296 (options weighed, skeptic findings); re-arming the handshake: #348
+9
View File
@@ -91,6 +91,15 @@ deploy, spanning both halves.
> nodes new` scaffolder emits this line; a hand-rolled node crate adds it
> itself.
> **Compatibility across the seam
> ([C30](design/contracts/c30-stability-discipline.md)).** The node crate
> consumes `aura-core` as a *path* dependency into the engine checkout —
> the pair moves together, and cargo rebuilds the crate when the engine
> changes. No Rust API stability is promised before 1.0: an external
> embedding in its own repo instead pins the engine as a git dependency at
> a rev, and the load handshake refuses a dylib built against a different
> aura-core.
## Where reusable nodes live (three tiers)
Everything that plugs into the engine is fractally a `Node`. Reuse is plain