DSDS is a pre-1.0 draft, maintained by one person, not a working group. Some parts of the schema can grow to cover new cases without a spec change; other parts are locked in. This page is the compatibility contract behind that status — what can change, how you'll be told, and what never changes regardless. For the rules themselves and how each is enforced, see Conformance.

What counts as a breaking change

A change is breaking if a document, tool, or workflow that worked before it ships stops working (or starts failing validation) after — not just a rename or removal. Concretely, all of the following count as breaking, even when they look like tightening rather than deleting:

  • Renaming, removing, or restructuring a field.
  • Adding a new required field to something that already exists.
  • Narrowing an existing constraint — an enum losing a value, a pattern getting stricter, a type narrowing.
  • Adding a new validator rule to scripts/validate/validate.js's DSDS-01–DSDS-11 catalog. A document that validated cleanly yesterday can fail today with no schema change at all, because the new rule now runs against it. This is easy to overlook precisely because no field moved — treat a new conformance rule with the same weight as a schema change, because it has the same effect on existing documents.
  • Any change to what npm run check accepts that a previously-passing document, run today, would fail.

Not breaking: new optional fields, new union members, relaxed constraints, new advisory (lint-docs.js) rules — a warning never blocks a build — and documentation-only edits.

Version semantics
ChangeVersion bumpYour documents
Documentation-only editsnoneUnaffected
Additions and loosenings (new optional fields, new union members, relaxed constraints, new advisory lint rules)patchRemain valid unchanged
Tightenings and behavior changes (new constraints, new required fields, moved fields, a new validator rule)minorMay need edits; the CHANGELOG lists every affected position
Renames and removalsbatched minor, pre-1.0 (major, post-1.0)Migration script provided (scripts/migrate-to-*.js)

Pre-1.0, a minor release can carry a tightening or a breaking change with no deprecation window first — that is what "pre-1.0" commits to here, and it's why the table above says "minor" rather than "major" for a rename. Read the CHANGELOG before upgrading a minor version, not just a major one. Post-1.0, tightenings and removals wait for a major version and follow the deprecation window below; see "The post-1.0 contract."

Deprecation policy

From the point a form is scheduled for removal, DSDS commits to:

  1. Marking it deprecated in the schema — the standard JSON Schema deprecated: true annotation where the schema allows it, a $comment explaining the removal where it doesn't (e.g. a whole definition being replaced).
  2. Naming its replacement in the same schema description or $comment — never just "removed," always "removed, use X instead."
  3. Flagging it via lint-docs.js (a warning, never a build failure) so it shows up in CI before it disappears.
  4. Keeping it valid for at least one further minor release after the deprecation notice ships, before the removal that breaks it.

This is a process commitment, not yet a schema-level machine-readable one — a consumer currently learns "deprecated, replaced by X" from the annotation and description text, not a dedicated replacedBy/removeIn field. That's a planned schema addition; until it ships, the schema text is the source of truth for a field's deprecation status.

Breaking-change windows

Pre-1.0 breaking changes land batched into a named release, not trickled one field at a time across several minors — a single CHANGELOG entry and migration script to read, not several. When a breaking window is being prepared, it's announced on this page (what's changing, and the release it lands in) before that release ships, the same practice earlier 0.15.x releases followed. Each window ships with its own migration script, following scripts/tools/migrate-to-0.20.js's pattern: convert what can be converted automatically, preserve anything else under $extensions["com.dsds.migration"], and print a warning for every case that needed a manual call.

0.21.0 was a breaking window, and it is now closed. It changed one shape — a component trait — and it changed it twice: traitType became required, and setBy was removed. Both landed in the same release rather than one per minor, which is what "batched" means here, and scripts/tools/migrate-to-0.21.js handles both in a single pass. There was no deprecation window first; pre-1.0 that is allowed, and it is the commitment this page makes above. 0.20.0 was the previous window, replacing the entity/documentBlocks model. No window is open now, and the plan from here is additive releases (new optional fields, new fixtures, restored tooling) until the 1.0 criteria below are met.

How schema changes get made

Every schema file under schema/ has comments explaining why it's shaped the way it is, and often what it replaced. Reading those comments alongside the schema is the best way to understand how the spec has changed — see the CHANGELOG for exactly how each old field maps to its new one.

Migrating a 0.15.2 document

scripts/tools/migrate-to-0.20.js [--dry-run] converts a 0.15.2 .dsds.json document (the entity/documentBlocks model) to a .dsds.yaml one in the current shape — stamped schemaVersion: 0.21.2, with component traits already carrying traitType — following the CHANGELOG's own field-by-field mapping. That one script is the whole migration from 0.15.2; there is no second step. It writes a new sibling file rather than overwriting the input, and it's best-effort, not a guarantee: a few 0.15.2 structures (most notably the api block's inline property/event documentation) have no 0.20.0 equivalent to convert to — 0.20.0 points sourceFiles at real source instead of inlining an extracted API.

Anything the script can't place in a typed field is preserved under the migrated item's own $extensions["com.dsds.migration"] rather than dropped, and every such case — plus every genuinely manual decision, like an alternatives pointer that wasn't really id-shaped in the source, or a ref that pointed at a token-group's own id before its children flattened to top-level entries — prints as a ⚠ line in the script's own output. Run npm run validate on the result afterward.

Migrating a 0.20.x document

A document already in the 0.20 YAML shape takes a different script: scripts/tools/migrate-to-0.21.js [--dry-run] brings it to 0.21.0 by adding traitType to every component trait and removing setBy. It edits the file in place, line by line, so comments survive, and it prints every traitType it had to guess for review. Don't run it on the output of migrate-to-0.20.js — that output is already in the 0.21.0 shape.

Designed to grow without a version bump

A handful of fields accept any string matching a pattern, instead of a fixed list of allowed values — so adding a new value never needs a schema change. Safe to build tooling around, but don't treat today's set as complete:

  • entries/token.tokenType — a token's category (color, spacing, typography, …), checked by pattern, not a fixed list.
  • common/ref.rel — a reference's relationship (depends-on, same-as, implements, …), open-ended; new values just get documented in the schema's own comments.
  • metadata.status's status value — stable, experimental, deprecated, and more, open for the same reason.
  • entry.id and common/id — a lowercase-dash-dot pattern, not a fixed list of parts, so ids fit whatever hierarchy your system uses.
  • $extensions — vendor or tool-specific data, grouped by namespace; a tool integration can add a field of its own any time, without waiting on a release.
More likely to require a spec change

A few fields are locked to a fixed list of values, because the number of possible cases is a fact about how the spec itself works, not an open-ended vocabulary. Be defensive about this list, not the one above:

  • entry.kind — five well-known values (system, component, token, theme, entry), four with their own entries/.schema.yaml file, plus a custom dot-separated kind name (e.g. acme.icon-library) for a document that wants its own recognizable name. Adding a well-known value changes what this spec can describe at all, so that bar stays high.
  • common/requirement-level — five values (must, should, should-not, must-not, may), taken directly from RFC 2119. This vocabulary belongs to that standard, not DSDS.
  • sections/* (the four section kinds) — guidelines, definitions, steps, section. Any entry kind can use any of these. section is the generic fallback, the same role entry plays for entries; freeform isn't a section kind at all, it's a field every section kind can carry. The part of the schema most likely to still change before 1.0.
Criteria for declaring 1.0

1.0 is declared when, at minimum:

  1. The kind lists stop changing — across at least one real pass of merging or splitting them, with no further changes needed.
  2. A second independent tool exists — at least one tool the spec authors don't maintain reads or writes DSDS documents for real.
  3. The validator's extra rules stop changing — scripts/validate/validate.js's DSDS-01–DSDS-11 (see the rule catalog) stop being added or renamed release to release.

Until then, the fixed lists above are the most stable part of the schema. Everything else can still change between minor versions, including the exact fields in any one sections/*.schema.yaml file.

Released versions never change

Whatever else moves, a released version directory doesn't. Every version DSDS has shipped stays served at its own URL — /v0.15.2/dsds.bundled.schema.json, and every earlier version at its own path — frozen at the bytes it shipped with. A document that pins schemaVersion and a $schema URL keeps validating against exactly what it was written against, indefinitely.

Released means tagged. A version is frozen from the moment its vX.Y.Z git tag exists, and scripts/tools/bump-version.js refuses to re-cut a version whose tag it already finds. Before that tag, the version currently in development is a different case: its directory is rebuilt from source on every build, so it tracks the schema it's generated from rather than lagging behind it. 0.21.2 is that version today — treat /v0.21.2/ as still settling, and pin an older, tagged version if you need bytes that cannot move under you.

Three mechanisms keep this honest, and none of them is a promise you have to take on faith. scripts/site/build-site.js only ever writes the current version's directory, so no build can reach an older one. bump-version.js's tag check is what turns "current" into "released" — it refuses to re-cut a tagged version. And the build compares what it just wrote against the bytes the tag actually published, reporting any that moved; npm run build -- --strict-versions turns that report into a failed build, which is what a release job should use.