What it means for a document to follow the DSDS spec: every rule it enforces, and how each one is checked. For what might still change before 1.0, see Stability. For how the schema itself is organized — the building blocks every entry, section, and reference is built from — see How the schema is organized.

The words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY have a specific meaning on this page and inside the DSDS schema files, as defined by RFC 2119 (updated by RFC 8174). They only carry that meaning when written in capital letters.

Where the rules live

DSDS keeps its rules inside the schema's own description text, next to the field they apply to — the schema is the spec, with no separate rulebook to keep in sync. The index of every normative statement below regenerates from the schema on every build, so it can't drift out of date. Each rule has a location-based ID (for example, common/ref§note.1) — if that ID changes, the schema moved, and the rule should be double-checked wherever it's cited.

Conformance classes

DSDS defines four ways something can "follow the spec" — a document, and the tools that create, read, and check it. State which one you mean.

Conforming document

A document that passes schema validation for the version named in its schemaVersion, and the extra rules below (DSDS-01 through DSDS-11) that a schema file alone can't check. Passing schema validation on its own isn't enough.

Conforming producer

A tool or person that creates DSDS documents. A conforming producer:

  • MUST only create documents that follow the spec
  • MUST NOT use an outdated field or structure in new documents (old ones are kept around only so existing documents keep working)
  • SHOULD record how a document was created, using metadata.origin
Conforming consumer

A tool, renderer, or AI agent that reads DSDS documents. A conforming consumer:

  • MUST NOT fail just because it sees an optional field it doesn't recognize
  • MUST keep $extensions data intact even if it doesn't understand it
  • MUST treat an unresolvable reference (an entryId#itemId pointing nowhere) as an error, not silently ignore it
  • MUST take MUST/SHOULD-style guidance as seriously as the spec says to, for a document it has already decided to trust — a must-not guideline is a hard stop for an agent writing code, not a suggestion. This does not extend that same standing to an unvetted or third-party document; see Security considerations for what that trust decision covers and doesn't
  • SHOULD build its own "what points to what" index when it loads a document, rather than expect the document to store that answer directly
  • MUST be able to address every section item, whether or not it was written with an id — when one is missing, derive it from the item's own text (lowercase, non-alphanumeric runs collapsed to a dash), the same way every other conforming tool does. See common/id.
Conforming validator

A tool that checks documents. A conforming validator MUST enforce both the schema itself (with format checks on) and the extra rules below (DSDS-01–DSDS-11). scripts/validate/validate.js is the reference implementation. Its examples/invalid/ folder holds one broken example per semantic rule (DSDS-XX-*.yaml) plus a set of plain schema fixtures (schema-*.yaml, no rule id — a pure JSON Schema rejection) pinning constraints like enum casing, empty arrays, and $extensions namespacing. Every fixture declares its own contract in a leading comment — # rejectedBy: schema|semantic, plus # expect: DSDS-XX or # errorAt: /json/pointer as applicable — and scripts/validate/conformance-test.js confirms each one fails for the exact reason and at the exact layer it claims to, not just that it fails at all.

The words this spec uses

One term per concept, so a reader and a generator mean the same thing by it. The right-hand column is what a reviewer should flag — each of those synonyms was in use somewhere before this table existed.

UseForNotDefined in
entryOne documented thing in the design system graph - a component, token, theme, or anything else.entity, record, item, nodeentries/entry.schema.yaml
documentOne .dsds.yaml or .dsds.json file, whether it holds a whole system or a single entry.spec, spec file, docbase.schema.yaml
specThe DSDS specification itself - this repo, the schema files, and the site that publishes them.(never a document)
sectionOne member of an entry's sections[].block, documentBlock, chunksections/section.schema.yaml
fieldA named slot on an object.property, key, attribute(property table is the one allowed exception - it names the generated tables)
kindThe discriminator value that says which shape an entry or section is.type, variantentry.kind, section.kind
itemOne member of a section's items[].entry, rule, criterion
conforming consumerAnything that reads a document - a renderer, an agent, a site generator.tool, reader, client, parser, rendererConformance
conforming producerAnything that writes a document.generator, author tool, writerConformance
conforming validatorAnything that checks a document against the schema and the rule catalog.linter, checkerConformance
Enforcement tiers

Every rule is enforced one of three ways:

TierHow it's checkedWhat happens if it fails
StructuralDirectly by the schema file (required fields, patterns, and similar built-in checks)Blocks — validation fails
SemanticBy scripts/validate/validate.js's own code (DSDS-01–DSDS-11: do references resolve, are ids unique, and so on)Blocks — validation fails
AdvisoryBy scripts/validate/lint-docs.js (npm run lint, DSDS-12+: RFC 2119 casing, a token description that just restates its id, and similar documentation-quality checks) where a check exists — otherwise a judgment callNever blocks — always exits 0, warnings only

Advisory is the newest tier and doesn't cover every SHOULD/MAY in this spec yet — it's additive, so a gap here is a missing check, not a passing one.

DSDS-05, DSDS-08, DSDS-09, and DSDS-11 are the exception: they report a warning, not a blocking error, unless the validator runs with --strict. Every other rule always blocks.

Every catalog entry declares its own tier explicitly, as enforcement: structural|semantic|advisory|none — not just implied by which row of this table it's listed in. scripts/checks/check-rule-catalog.js (run on every npm run check) enforces two things: every entry's enforcement is one of those four values, and — for the semantic tier specifically — that the catalog and scripts/validate/validate.js agree exactly on which rules exist in both directions. A catalog entry with no matching RULES. reference in validate.js, or a RULES. reference with no catalog entry, fails the build instead of silently producing an untraceable [undefined] finding.

Rule catalog

The table below is generated directly from schema/conformance-rules.yaml by scripts/generate/generate-rule-catalog.mjs — not a hand-kept copy — so it cannot drift from the machine-readable catalog the validator actually reads. scripts/validate/conformance-test.js and scripts/checks/check-rule-catalog.js (both run on every npm run check) additionally confirm the catalog and scripts/validate/validate.js's own implementation agree. The catalog itself is also published as a versioned artifact at /v0.21.2/conformance-rules.yaml alongside the schema bundle, for a tool that wants to read the rule descriptions programmatically instead of scraping this page.

Versions through 0.15.2 used a three-digit catalog (DSDS-001–DSDS-006 and similar, in rules/rules.yaml) covering a different rewrite of the model entirely. 0.20.0 restarted numbering at DSDS-01 for a genuinely different rule set — DSDS-02 here has nothing to do with whatever DSDS-002 meant before. (0.15.2's own DSDS-002 was, fittingly, the rule against reusing an identifier — a reminder this reset itself is worth naming explicitly rather than leaving implicit.) If you're citing a rule id from before 0.15.2, say which catalog it's from; a bare DSDS-0N is ambiguous across the two.

IDRule
DSDS-01At most one sourceFiles entry per platform.
DSDS-02A system entry's metadata.platforms closes the platform vocabulary, once declared.
DSDS-03A checkedBy: automated rule needs somewhere to actually run.
DSDS-04Entry and shared ids are unique within a document.
DSDS-05An entryId#itemId ref must resolve.
DSDS-06A composes ref chain must not cycle.
DSDS-07A depends-on ref chain must not cycle.
DSDS-08A bare to: ref must resolve to a real entry or shared entry.
DSDS-09A combo's subject/items must resolve.
DSDS-10A same-as item's level, when declared, must match its target's.
DSDS-11A relative sourceFiles[].file, source, or rel: file href must exist on disk.
DSDS-12Capitalize RFC 2119 keywords (MUST/SHOULD) in a guideline's own statement.
DSDS-13A token description, when present, shouldn't just restate its id/name or repeat a raw value.
DSDS-14A hard-requirement guideline (must/must-not) with no checkedBy at all.
DSDS-15A component entry with no guidelines section framed when-to-use.
DSDS-16A token description, when present, shouldn't just restate its position on a scale.
DSDS-17An entry's own top-level fields don't follow STYLE_GUIDE.md's field order.
DSDS-18An entry's sections[] doesn't follow STYLE_GUIDE.md's grouping and ordering.
DSDS-19A guidelines section's items aren't ordered must, should, may, should-not, must-not.
DSDS-20A base document's own top-level fields don't follow STYLE_GUIDE.md's field order.
DSDS-21A base document's entries[] aren't ordered by kind.
DSDS-22A nested object's fields don't follow the order its own schema file declares.
DSDS-23An entry's combos[] don't keep one subject together, in level order.

DSDS-06 and DSDS-07 restore a cycle check the pre-0.20.0 spec had — nothing should point back at itself through a chain of composes or depends-on links. The 0.20.0 rewrite dropped it by accident; it's enforced by the validator's own code now, not the schema itself.

Project-scope resolution

DSDS-05, DSDS-08, and DSDS-09 check the whole project, not just the one document they're given. A rel: file link (the way the spec recommends splitting up a large system) joins the resolution pool, and so does every other file passed to the validator in the same run, even without a link between them — the way a standalone entry file, with no field of its own to declare "these are my siblings," still resolves a reference to a component listed alongside it.

The search is bounded to the folder holding the file being validated, and its subfolders — never a parent folder, a sibling folder, or the wider repo. Deliberately narrow: widening it to the nearest .git or package.json would let a CI job checking one untrusted document read far more of a monorepo than it should. A future --root flag could widen it on request; nothing has needed one yet.

An unresolved reference is a warning, not a failure, only in this cross-file case — a self-contained document's unresolved to: is still a hard error. A standalone entry file is the one exception even then, since it can never assert "this is definitely everything" the way a base document's entries/shared arrays can. Run with --strict to turn these warnings into failures.

Whether a to: value even looks like a valid id is checked separately, directly by the schema: common/ref.schema.yaml's to field only accepts id-shaped values, so a display name or a value with a space in it fails before DSDS-05, DSDS-08, or DSDS-09 ever run. See common/ref.

DSDS-11 covers one of these: whether a relative sourceFiles[].file, source, or rel: file href actually points at a file that exists on disk, checked relative to the file being validated and bounded to the same directory-of-the-target boundary as DSDS-05/DSDS-08/DSDS-09. It applies to a base document's own top-level refs as well as to each entry's. Warning-only, promoted to a failure under --strict, same tier and same reason — it's opt-in because it's the one rule that reads the filesystem beyond the document being validated. A related check remains deliberately unbuilt: confirming the exact ids or paths inside another file the way DTCG-path resolution would need — that would mean actually reading and parsing the target's contents, not just confirming it exists.

Conformance suite

scripts/validate/validate.js is the reference implementation, but nothing about DSDS requires it to be the only one — a second, independent implementation is one of this spec's own 1.0 criteria. The conformance suite is what that second implementation checks itself against, without reading this repo's JavaScript at all.

Published as one versioned artifact per release, at /v0.21.2/conformance-suite/manifest.json, alongside a copy of every fixture it references (/v0.21.2/conformance-suite/examples/invalid/). Generated by scripts/generate/generate-conformance-suite.mjs directly from each fixture's own leading-comment contract (the same rejectedBy/expect/errorAt fields Conformance validator describes above) — not hand-maintained, so it can't drift from the fixtures themselves.

The runner contract — what implementing conformance against this artifact means, for a validator in any language:

  1. Fetch the manifest. For each entry in fixtures, fetch the file it names (relative to the manifest itself).
  2. Validate that file against the schema bundle for the manifest's own schemaVersion.
  3. Confirm the document is rejected — and rejected for the declared reason, not just rejected somehow:
    • rejectedBy: "schema" — at least one resulting error must be a pure JSON Schema violation (no DSDS-XX rule id attached). If errorAt is set, one such error's instance path must equal it ("/" for the document root).
    • rejectedBy: "semantic" — every id listed in expect must appear among the resulting errors or warnings tagged with that id (a semantic rule may report as either, depending on whether the validator runs in a --strict-equivalent mode).
  4. A fixture that validates cleanly, or fails for a different reason than it declares, is a conformance failure for the validator under test — passing "some validators reject it" isn't the bar; rejecting it for the right, stated reason is.

This is exactly what scripts/validate/conformance-test.js already does against scripts/validate/validate.js on every npm run check — the manifest just makes the same check runnable by something that isn't that script.

Open conventions

The schema deliberately leaves some questions unanswered — not oversights, but places where a fixed rule would fit some teams and not others.

  • Where does a guideline item's pointer go — refs, or a named field? alternatives, evidence, related, and checks each exist for one specific rel: alternative-to, an external standard, refines, and test/lint-rule. Any other rel — extends, depends-on, composes, part-of, replaces, implements, relates-to, pairs-with, excludes, see-also — goes in the general-purpose refs field instead, alongside same-as and external-link. See sections/guidelines.
  • Where does an entry's primary source file go? refs with rel: source — see common/ref.
  • What does tags[0] mean? The first tag, by convention, is the entry's main category — see metadata.
  • How does a token's source point at one key inside a shared DTCG file, not just the whole file? By convention, a token's own id doubles as its path in the DTCG token tree — color.action.primary names the same token in both places, which is why entries/token.id allows slash separators DSDS ids otherwise don't. When a project's DTCG paths don't line up with its DSDS ids, point source's href at the file plus a JSON Pointer fragment instead (./tokens.dtcg.json#/color/action/primary) — ordinary URI syntax, no schema change.
  • What is a component's status when it ships on more than one platform? Author one metadata.status entry per platform and let the overall status be inferred from them, rather than stating a separate overall value that can silently disagree. See Status across platforms below.
Status across platforms

A component rarely reaches the same maturity everywhere at once: stable on web, beta on iOS, not started on Android. metadata.status accepts either a single object or a list of them, each optionally naming its own platform.

metadata: status: - platform: react status: stable since: "1.0.0" - platform: ios status: beta since: "2.0.0"

A consumer SHOULD derive a component's overall status from the aggregate of its per-platform entries, rather than expect a separately authored overall value. There is no overall field, deliberately: a stated overall status is a second source of truth that drifts the moment one platform moves, and every consumer that needs one can compute it consistently from the entries that are already there.

How to aggregate is the consumer's decision, because it depends on the question being asked. Two conventions cover most cases:

  • Least-mature wins — the honest answer to "can I depend on this everywhere?" A component that is stable on web and beta on iOS is beta overall.
  • Per-platform, unaggregated — the honest answer to "can I depend on this here?" A renderer showing a React developer the React status shouldn't dim it because iOS lags.

A producer SHOULD declare metadata.platforms on the system entry when a document uses per-platform statuses, so a consumer can tell the difference between a platform that is deliberately unsupported and one that simply has no status entry yet (DSDS-02 then closes that vocabulary). A single status object with no platform still means exactly what it always did: this entry's status, everywhere.

Passing isn't the same as good

A document with zero errors and zero warnings can still be bad documentation — the schema checks structure, not judgment. examples/anti-patterns/ collects a few small documents that validate cleanly and are still worth avoiding: a definition that only restates its own term, a checkedBy: manual claim too vague to actually check, and guideline prose that names a concept (a "token-group" entry) the spec doesn't have. Each file's own leading comment says what's wrong with it and why the schema can't catch it. Deliberately excluded from the default npm run check sweep — not meant to be copied.

Index of every normative statement

Every RFC 2119 sentence (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY) in the schema's own text — both description and $comment — extracted and indexed by scripts/generate/extract-normative.mjs directly from schema/**/*.schema.yaml, not hand-copied, and regenerated on every build (npm run generate, checked with --check in generate:check). Every statement below is in a description, where this spec keeps its rules. $comment holds the reasoning beside them; a requirement that turns up there is marked (in $comment) and fails npm run check. Each statement's id is location-based (

/§.), so citing one and having the schema move underneath it is a visible, checkable break rather than a silent one.

Generated from the v0.21.2 schemas by scripts/generate/extract-normative.mjs — do not edit by hand. 16 statements: 12 MUST, 1 MUST NOT, 3 SHOULD, 0 SHOULD NOT, 0 MAY.

common common/extensions
  • MUST — Each top-level key MUST be a dotted namespace, like com.acme, matching the Design Tokens Community Group's own $extensions convention. common/extensions§(root).1
common/id
  • MUST — A section item's own id is optional, but a conforming consumer MUST be able to address every item. common/id§(root).1
  • MUST — When an item has no id, consumers MUST derive one from the item's own text so content has a consistent id. common/id§(root).2
entries entries/component
  • MUST — When the document declares a metadata.platforms list, this MUST be one of its entries. entries/component§[allOf][1].sourceFiles.items.platform.1
  • MUST — When the document declares a metadata.platforms list, this MUST be one of its entries. entries/component§[allOf][1].imports.items.platform.1
metadata metadata/metadata
  • MUST NOT — MUST NOT contain markup. metadata/metadata§note.1
  • SHOULD — A conforming consumer SHOULD treat updated.date as the cache key for this item's documentation specifically. metadata/metadata§.updated.1
metadata/system-metadata
  • MUST — When this list is present, every other platform value used anywhere in the document MUST match one of these. metadata/system-metadata§[allOf][1].platforms.1
sections sections/definitions
  • MUST — id is optional, but a conforming consumer MUST derive one from this item's own text. sections/definitions§[allOf][1].items.items.id.1
sections/guidelines
  • SHOULD — When an item carries a level and a ref but no statement, a conforming consumer SHOULD show the level and the link rather than rendering it as empty content. sections/guidelines§(root).1
  • SHOULD — An author who expects a person to read the guideline without following the link SHOULD add a statement instead. sections/guidelines§(root).2
  • MUST — id is optional, but a conforming consumer MUST derive one from this item's own text. sections/guidelines§[allOf][1].items.items.id.1
  • MUST — It MUST reflect level. sections/guidelines§[allOf][1].items.items.example.1
  • MUST — A must-not item's example MUST show what not to do. sections/guidelines§[allOf][1].items.items.example.2
sections/section
  • MUST — id is optional, but conforming consumer MUST derive one from this item's own text. sections/section§freeformEntry.id.1
sections/steps
  • MUST — id is optional, but a conforming consumer MUST derive one from this item's own text. sections/steps§[allOf][1].items.items.id.1