Skip to content

11 - Evolution ​

Rayfold schemas have no version numbers. Compatibility is enforced by tooling on every change.

Every change is one of three kinds: compatible, a warning, or breaking. rayfold check --against <old> reports all three and fails on breaking ones; --strict also fails on warnings.

Compatible changes (allowed) ​

  • Adding a type, operation, field, enum value, event to an emits list.
  • Removing an error from a throws list: a client's exhaustive handling still compiles.
  • Making a required argument optional (giving it a default), or a nullable result field non-null.
  • Adding an annotation that does not restrict (@cost, @example, @deprecated).

Warnings (allowed, and fatal under --strict) ​

  • Adding an error to a throws list: nothing breaks on the wire, but a client that handles the union exhaustively has a case it does not know.
  • Adding a policy that can deny where none existed.
  • Removing a policy: nothing breaks, but something that was guarded no longer is, which is worth seeing in a diff.

Breaking changes (rejected by rayfold check) ​

  • Removing or renaming anything that is not past its @deprecated(sunset:) date.
  • Changing a field or argument type, including nullability in the restrictive direction (result field non-null to nullable is breaking for clients that rely on it; argument optional to required is breaking for callers).
  • Removing an enum value or union member.
  • Removing a named view (removing a default view is a warning: the type falls back to its derived default): a client that asks for it by name gets nothing.
  • Dropping an interface a type declared.
  • Adding a required argument without a default.
  • Changing an ordinal, or giving a new member one already assigned. Ordinals are kept by name (01 §9.6): against a lockfile, a member inserted between others or moved keeps the ordinal the lock recorded, which is compatible. Without a lock there is no record of assignment, so a member whose position changed is a warning.

Deprecation ​

@deprecated(reason:, sunset: Date, replacement:). Removal is allowed only after sunset. Servers report usage of deprecated members per client (Rayfold-Client) so a removal can rest on recorded usage.

Field usage telemetry ​

Runtimes record (op, field path, client, last seen). rayfold check <schema.rayfold> --unused <usage.json> --since 30d lists members with no traffic, from an exported usage snapshot.

Lockfile ​

rayfold.lock.json records ordinals and the schema hash. rayfold lock updates it, carrying the recorded ordinals forward by name: a member the lock already holds keeps its ordinal wherever it now sits, a member with @ordinal(n) takes n, and a new member takes the next ordinal above the highest its type has ever used (the lock keeps that highest per type in highestOrdinals), so an ordinal is never reused. Committing it and running rayfold check --against rayfold.lock.json in CI makes a change that breaks these rules fail the build.

What implements this ​

Unlike the rest of Core, evolution is a tooling contract, not a wire contract. The rayfold CLI enforces it, and it places no obligation on a server at runtime. An implementation in another language is conformant without shipping a diff tool.

Released under the Apache-2.0 license.