Schema Diff
cratestack diff old.cstack new.cstack structurally compares two schema versions and classifies
every change by its effect on the generated wire contract — REST/RPC responses and client-constructed
payloads — rather than by raw text differences.
It exists because that effect is easy to miss by eye. Adding @@paged to a model, for example, looks
like a one-line change in the schema but silently turns every generated client’s Model.list() return
type from Model[] into Page<Model> — a breaking change for every existing consumer, only visible
today by diffing generated TypeScript/Dart output before and after.
Usage
diff takes two file paths, not git refs, so pull the base
branch’s copy of the schema out to a temp file first:
--json for machine-readable output:
Classification
Models, fields, and procedures are matched by name only — the same rename-agnostic philosophycratestack migrate diff uses for DB schema diffing. A rename shows up as a removal plus an addition,
not as a rename; there is no heuristic rename detection to avoid false positives.
Each change lands in one of three buckets:
Breaking
- Removing a model, field, or procedure.
- Adding
@@pagedto a model, or removing it — flips.list()’s response envelope betweenT[]andPage<T>. - Adding a required field or procedure argument with no default — existing client payloads that omit it are rejected.
- Retyping a field, argument, or return type.
- Narrowing arity: optional → required, or introducing/removing a list (
[]). - A procedure changing kind (
query↔mutation) — the generated dispatch/HTTP method differs.
Additive
- Adding a model, an optional field, or a procedure.
- Adding a required field/argument that carries a
@default(...)— the server fills it in, so existing payloads still work. - Widening arity: required → optional.
Internal-only
- Model-level attributes other than
@@paged—@@soft_delete,@@audit,@@retain(...),@@emit(...). These affect server-side behavior (e.g. which rows a query returns) but not the shape of the response the client sees.
Known gaps
This is a first pass scoped to the most common, clear-cut categories — not an exhaustive breaking-change taxonomy. Explicitly out of scope for now:- Behavioral (non-shape) breaking changes.
@@soft_deletechanges which rows.list()/.get()return; that’s a real behavior change for callers, but it doesn’t change the response type, so it’s reported as internal-only today. - Views, mixins (as declarations), and enums are not diffed directly. Mixin fields are already
inlined into
Model.fieldsby the parser before this tool sees them, so a mixin field change is caught as a field change on every model that uses the mixin — but the mixin declaration itself, and view/enum definitions, aren’t compared. - Policy (
@@allow/@@deny) changes are not diffed — a policy that goes from permissive to restrictive is an authorization-breaking change this tool doesn’t yet see. - Field-level attribute changes other than what feeds the type/arity/default checks above (e.g.
@pii,@sensitive,@unique) aren’t classified.
Read Next
- Migrations — the DB-facing counterpart:
cratestack migrate diffdiffs.cstackagainst a committed snapshot to generate SQL, using the same by-name matching philosophy. - Installing the CLI — get
cratestackon your machine.