What Is Contract, and What Is Internal
This page answers one question: is this change breaking?
Contract does not mean permanent. This repository does not preserve backwards compatibility and ships no adapters; its users are the maintainer and downstreams who port. What contract buys is a rule about cost. Changing a contract surface is a breaking change: it takes a major version and a line in the migration notes, and it cannot ride along inside a refactor. Changing an internal surface takes neither.
Contract
Six things, and nothing else.
1. The blessed idioms
Each axis of wiring an application — registration, adaptation, declaration, template provision, theme provision, entry points — has exactly one blessed item, and a small number of secondary paths that survive because they name a capability nothing else on the same axis covers. All of them are contract, by name, signature and meaning. A secondary path is contract for exactly the capability its stated reason names.
The blessed set:
#[derive(Parser)] // declaration: clap-derive
struct Cli { /* … */ }
#[derive(Subcommand, Dispatch)] // registration
#[dispatch(handlers = handlers)]
enum Commands { /* … */ }
#[handler] // adaptation
fn list(#[flag] all: bool, #[ctx] ctx: &CommandContext) -> Result<Output<Listing>, anyhow::Error> {
/* … */
}
App::builder()
.templates(embed_templates!("src/templates")) // template provision
.styles(embed_styles!("src/styles")) // theme provision
.default_theme("myapp")
.commands(Commands::dispatch_config())? // registration
.build()?
.run(Cli::command(), std::env::args()); // entry point
Dropping a secondary path later is itself a major version. That friction is deliberate: an item kept with a stated reason cannot be quietly removed as "internal, nobody used it".
2. The structural shape of each --output mode's bytes
--output accepts auto, term, text, term-debug, json, yaml, xml
and csv. All eight are classified here.
Structured modes (json, yaml, csv, xml): the document a handler's
data produces — its field names and its nesting — is contract. Changing it
changes what a consuming script parses.
Human modes (text, term): the bytes are not contract. Themes,
wording, column widths and layout may change in any release. What is contract
is the pair of properties a script can rely on without reading words:
- The style transformation.
textremoves Standout's style tags and adds no ANSI of its own.termturns every resolved style tag into ANSI. Neither half reaches ANSI that a handler or a template writes literally — the framework does not sanitize those bytes and does not promise to, so a caller who needs them gone strips them itself. - The split between the streams. Data goes to stdout; diagnostics and warnings go to stderr.
auto is contract as a resolution rule rather than as bytes: it resolves
to term when the destination reports color capability and to text when it
does not, and a term request under a never-color policy resolves to text.
What a caller may rely on is which of the two modes it lands in, and then that
mode's own contract.
term-debug is internal. It prints style tags unresolved, as evidence
for the framework's own snapshots; both that tag vocabulary and its spelling
may change in any release.
One more byte-level rule belongs here because a script can see it: the render pipeline consumes the template's final newline and the process edge appends exactly one. See the trailing-newline contract.
3. Exit statuses
Zero means success, and each documented nonzero status keeps its documented
meaning. An application-owned status is the application's to choose, and the
framework emitting it verbatim is the contract — that is AppFailure, and
ExternalFailure for a status another operation declared. The wording of the
diagnostics the framework writes for itself is not contract; see
Error Handling.
4. The two name mappings a user types on the command line
#[handler]'s parameter name to clap argument id — underscores become hyphens, sono_legendreads the argument idno-legend.#[derive(Dispatch)]'s variant name to command name — kebab-case, soListUnitsregisterslist-units, and#[dispatch(name = "…")]renames one variant.
These are contract because they are not source-level at all. They decide the
words in a shell script, and a change to either breaks callers who never
recompile. Both rules are stated in full in the
#[dispatch(…)] and #[handler] reference.
5. Re-export from the standout crate root
An item's availability through standout is contract; its location is not.
A type may move between leaf crates in any release as long as the root
re-export still names it, and a leaf crate's own API is contract only where
standout re-exports it. That rule is what makes one standout dependency
enough, and what keeps reorganizing the crates from being a breaking change on
its own.
6. standout-test's assertion API
A downstream's test suite depends on it, so a rename there breaks a build that never touched the framework. Contract by name, signature and meaning:
TestHarness, with its injection methods and its run forms —runandrun_processon every supported platform,run_ptyon Unix only, where the pseudo-terminal it opens exists.TestResult, which an in-processrunreturns: itsoutcome, its exit status and success and error kinds, its raw and plain streams andbinary, its style-tag resolutions, itswarnings, its artifact accessors and itsassert_*methods.ProcessResult, which a spawnedrun_processorrun_ptyreturns: its process status, code and success, its raw, plain and byte streams, its tempdir and itsassert_*methods.assert_page_snapshot!withSnapshotCase, andmatrixwithMatrixCell.- The
clap_parityandinvariantsmodules.
An item behind a cfg is contract on the platforms it compiles for, so
narrowing its cfg is a breaking change on the platforms it leaves. A
snapshot's contents are not contract — a snapshot is evidence, and evidence
changing is the point of a snapshot. The serial re-export is serial_test's
API, not this repository's.
Internal
Everything else, explicitly including:
- any path the blessing deleted;
- the internals that lost their
pubin the visibility sweep; - module paths within a crate;
- rendered help layout, and the wording of diagnostics and warnings;
- the framework's own template and style names, beyond the fact that
include_framework_templates(false)andinclude_framework_styles(false)decline them; - the leaf crates' APIs where
standoutdoes not re-export them.
One boundary worth naming
A machine-readable schema — a versioned envelope a consumer can validate against — is not part of this statement. What this statement establishes is that structured output has a contract shape at all, not that the shape is published as a schema.
Where this comes from
ADR-0033 decides what this page says, including the alternatives that were rejected: declaring the whole public API contract, declaring nothing contract, and declaring the human-mode bytes contract. ADR-0032 carries the blessed set and the capability map behind item 1.