# Design System Doc Spec (DSDS) — full text > A machine-readable format for design system documentation. DSDS structures a design system as a graph of entries (systems, components, tokens, themes, and custom kinds) and sections (definitions, guidelines, steps, and freeform content) for humans, parsers, and agents. Everything needed to understand DSDS in one file: every guide below in full, then the complete bundled JSON Schema (every entity, document block, and shared definition). For direct links to each definition's own page, see llms.txt instead. # Conformance — DSDS 0.23.0 What it means to follow the DSDS spec, every rule that's checked, and how each one is checked. For what could still change before 1.0, see [Stability](stability.html). For how the schema is put together, see [How the schema is organized](schema.html#how-the-schema-is-organized). **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT** and **MAY** have a set meaning on this page and in the DSDS schema files. It's the meaning [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) gives them, as updated by [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174), and only when they're written in capitals. ## Where the rules live Rules about a single field live in the schema, in that field's `description`. The [index of every requirement](#index-of-every-normative-statement) at the end of this page is rebuilt from the schema on every build, so it can't go out of date. The [Specification](specification.html) puts them together in numbered sections. Each requirement there names the schema file, rule or test vector that checks it, and `npm run check` makes sure it does. Each requirement in the index has an id based on where it lives, such as `common/ref§note.1`. If that id changes, the schema moved, and anywhere that cites the requirement should be checked. ## Conformance classes "Follows the spec" can mean four different things: a document, or a tool that writes, reads or checks one. Say which one you mean. ### Conforming document A document that passes the schema for the version in its `schemaVersion`, **and** passes the rules in the [rule catalog](#rule-details) that the schema can't check on its own. Passing the schema alone isn't enough. ### Conforming producer A tool or a person that writes DSDS documents. A conforming producer: - MUST only write documents that follow the spec. - MUST NOT use an outdated field or structure in a new document. Old ones are only kept so existing documents keep working. - SHOULD record how a document was made, in `metadata.origin`. ### Conforming consumer A tool, a renderer or an AI agent that reads DSDS documents. A conforming consumer: - MUST NOT fail just because it finds an optional field it doesn't recognize. - MUST keep `$extensions` data, even when it doesn't understand it. - MUST report a reference that points at nothing, such as an `entryId#itemId` with no target, instead of quietly ignoring it. The [processing model, §3](processing.html#3-projects) says when that's an error and when it's a warning. - MUST take the document's rules as seriously as their `level` says, **for a document it has already decided to trust**. For an agent writing code, a `must-not` guideline is a hard stop, not a suggestion. That doesn't extend to a document from an unknown or untrusted source. [Security considerations](security.html) explains what that trust decision covers. - SHOULD work out "what points at what" itself when it loads a document, instead of expecting the document to store the answer. - MUST be able to address every section item, whether or not it has an `id`. When one is missing, it makes the id from the item's own text, the same way every other conforming tool does. See the [processing model, §2](processing.html#2-addresses). ### Conforming validator A tool that checks documents. A conforming validator MUST check both the schema (with format checks turned on) and every semantic rule in the [rule catalog](#rule-details). `scripts/validate/validate.js` is the reference implementation. Its [`examples/invalid/`](examples.html#invalid) folder holds documents that are broken on purpose: - **`DSDS-XX-*.yaml`**: at least one for each semantic rule. - **`schema-*.yaml`**: documents only the schema rejects, with no rule id. They cover things like the casing of a value, an empty list, and a `$extensions` key with no namespace. Each one says, in a comment at the top, how it should fail: `# rejectedBy: schema|semantic`, plus `# expect: DSDS-XX` or `# errorAt: /json/pointer` where they apply. `scripts/validate/conformance-test.js` checks that each one fails for exactly that reason, in exactly that place, not just that it fails. ## The words this spec uses One word for each idea, so a person and a tool mean the same thing by it. The "Not" column lists words a reviewer should flag: each was in use somewhere before this table existed. {/* dsds:terms */} | Use | For | Not | Defined in | |---|---|---|---| | **entry** | One documented thing in the design system graph - a component, token, theme, or anything else. | entity, record, item, node | `entries/entry.schema.yaml` | | **document** | One `.dsds.yaml` or `.dsds.json` file, whether it holds a whole system or a single entry. | spec, spec file, doc | `base.schema.yaml` | | **spec** | The DSDS specification itself - this repo, the schema files, and the site that publishes them. | (never a document) | | | **section** | One member of an entry's `sections[]`. | block, documentBlock, chunk | `sections/section.schema.yaml` | | **field** | A named slot on an object. | property, key, attribute | (property table is the one allowed exception - it names the generated tables) | | **kind** | The discriminator value that says which shape an entry or section is. | type, variant | `entry.kind`, `section.kind` | | **item** | One member of a section's `items[]`. | entry, rule, criterion | | | **conforming consumer** | Anything that reads a document - a renderer, an agent, a site generator. | tool, reader, client, parser, renderer | [Conformance](https://designsystemdocspec.org/conformance#conformance-classes) | | **conforming producer** | Anything that writes a document. | generator, author tool, writer | [Conformance](https://designsystemdocspec.org/conformance#conformance-classes) | | **conforming validator** | Anything that checks a document against the schema and the rule catalog. | linter, checker | [Conformance](https://designsystemdocspec.org/conformance#conformance-classes) | {/* /dsds:terms */} ## Enforcement tiers Every rule is checked in one of three ways: | Tier | How it's checked | What happens when it fails | |---|---|---| | Structural | By the schema itself: required fields, patterns, and other built-in checks | Blocks: validation fails | | Semantic | By the code in `scripts/validate/validate.js` (`DSDS-01`–`DSDS-11`, `DSDS-24` and `DSDS-25`): do references point at something, are ids unique, and so on | Blocks: validation fails | | Advisory | By `scripts/validate/lint-docs.js` (`npm run lint`, `DSDS-13`–`DSDS-21`) where a check exists, such as a token description that only repeats its id, or a hard requirement with no `checkedBy`. Otherwise it's a judgment call. Field order isn't a rule: `dsds-format` applies it | Never blocks: always exits 0, warnings only | The advisory tier doesn't yet cover every SHOULD and MAY in the spec. It grows over time, so a gap means a check nobody has written, not a document that passed. `DSDS-05`, `DSDS-08`, `DSDS-09`, `DSDS-11` and `DSDS-24` are the exceptions. They report a warning instead of an error, unless the validator runs with `--strict`. Every other semantic rule always blocks. Each rule in the catalog states its tier, as `enforcement: structural|semantic|advisory|none`. It isn't inferred from which row of this table the rule fits. `scripts/checks/check-rule-catalog.js` runs on every `npm run check` and checks two things: - every rule's `enforcement` is one of those four values; - the catalog and `scripts/validate/validate.js` agree on exactly which semantic rules exist. A rule in the catalog with no code, or code with no rule in the catalog, fails the build. Otherwise a finding could show up with no rule id at all. ## Rule catalog The table below is generated from [`schema/conformance-rules.yaml`](https://github.com/somerandomdude/design-system-documentation-schema/blob/main/schema/conformance-rules.yaml) by `scripts/generate/generate-rule-catalog.mjs`. It isn't a copy kept by hand, so it can't drift from the catalog the validator reads. `scripts/validate/conformance-test.js` and `scripts/checks/check-rule-catalog.js` also run on every `npm run check`, and confirm the catalog and the validator agree. The catalog is published with each release, next to the schema bundle, at [`/v0.23.0/conformance-rules.yaml`](/v0.23.0/conformance-rules.yaml). A tool can read the rules from there instead of from this page. Versions up to 0.15.2 had a different catalog, with three-digit ids (`DSDS-001`–`DSDS-006` and so on, in `rules/rules.yaml`), for a different model. 0.20.0 started again at `DSDS-01` for a new set of rules. So `DSDS-02` here has nothing to do with the old `DSDS-002`. (Fittingly, the old `DSDS-002` was the rule against reusing an id.) If you cite a rule id from 0.15.2 or earlier, say which catalog it's from. A bare `DSDS-0N` could mean either. {/* dsds:rule-catalog */} | ID | Rule | |---|---| | [`DSDS-01`](#dsds-01) | At most one `sourceFiles` entry per platform. | | [`DSDS-02`](#dsds-02) | Once a system lists its platforms in `metadata.platforms`, no other platform can be used. | | [`DSDS-03`](#dsds-03) | A `checkedBy: automated` rule needs something that actually runs it. | | [`DSDS-04`](#dsds-04) | Entry and shared ids are unique within a document. | | [`DSDS-05`](#dsds-05) | An `entryId#itemId` ref must point at something that exists. | | [`DSDS-06`](#dsds-06) | A `uses`, `extends`, `same-as` or `replaced-by` chain must not loop. | | [`DSDS-07`](#dsds-07) | Retired: merged into DSDS-06. | | [`DSDS-08`](#dsds-08) | A `to` with no `#` must point at an entry that exists. | | [`DSDS-09`](#dsds-09) | A combo's `subject` and `items` must point at things that exist. | | [`DSDS-10`](#dsds-10) | If a `same-as` item states a `level`, it must match its target's. | | [`DSDS-11`](#dsds-11) | A relative file path in `sourceFiles`, `source`, `rel: file` or `rel: source` must point at a file that exists. | | [`DSDS-12`](#dsds-12) | Retired: a guideline's `level` already says how strong it is. | | [`DSDS-13`](#dsds-13) | A token's `description` shouldn't just repeat its id, its name or its value. | | [`DSDS-14`](#dsds-14) | A `must` or `must-not` guideline has no `checkedBy`. | | [`DSDS-15`](#dsds-15) | A component has no `guidelines` section framed `when-to-use`. | | [`DSDS-16`](#dsds-16) | A token's `description` shouldn't just repeat its place on a scale. | | [`DSDS-17`](#dsds-17) | Retired: `dsds-format` applies this order. | | [`DSDS-18`](#dsds-18) | An entry's `sections` aren't grouped and ordered as STYLE_GUIDE.md asks. | | [`DSDS-19`](#dsds-19) | A `guidelines` section's items aren't ordered must, should, may, should-not, must-not. | | [`DSDS-20`](#dsds-20) | Retired: `dsds-format` applies this order. | | [`DSDS-21`](#dsds-21) | A base document's `entries` aren't ordered by `kind`. | | [`DSDS-22`](#dsds-22) | Retired: `dsds-format` applies this order. | | [`DSDS-23`](#dsds-23) | Retired: `dsds-format` applies this order. | | [`DSDS-24`](#dsds-24) | A generic `entry` or `section` has a field no DSDS schema declares. | | [`DSDS-25`](#dsds-25) | A pointer must sit where its kind belongs. | {/* /dsds:rule-catalog */} `DSDS-06` brings back a check the model before 0.20.0 had: nothing can point back at itself through a chain of `uses` links. The 0.20.0 rewrite dropped it by mistake. Until 0.22.0 it covered `composes`, and `DSDS-07` covered `depends-on`. 0.22.0 merged both into `uses` and retired `DSDS-07`, keeping its id so it's never reused. The validator's own code checks it, not the schema. ### Project-scope resolution `DSDS-05`, `DSDS-08` and `DSDS-09` look across the whole **project**, not only the document they're given. The project includes every file reached through a `rel: file` link, which is how the spec suggests splitting up a large system. It also includes every other file passed to the validator in the same run, even with no link between them. That's how a standalone entry file, which has no field to list the files next to it, can still point at a component listed beside it. The search stays inside the folder of the file being validated, and its subfolders. It never goes into a parent folder, a sibling folder or the rest of the repo. That's on purpose: if it went up to the nearest `.git` or `package.json`, a CI job checking one untrusted document could read far more of a large repo than it should. A future `--root` option could widen it when asked to. Nothing has needed one yet. An unresolved reference is a **warning** rather than a failure only in this cross-file case. In a document that's complete on its own, an unresolved `to` is still an error. A standalone entry file is always the exception, because it can never claim to be everything, the way a base document's `entries` and `shared` lists can. Run with `--strict` to turn these warnings into failures. Whether a `to` even *looks like* an id is checked separately, by the schema. The `to` field in `common/ref.schema.yaml` only accepts values shaped like an id. So a display name, or a value with a space in it, fails before `DSDS-05`, `DSDS-08` or `DSDS-09` run. See [common/ref](schema.html#common-ref). **`DSDS-11`** checks files the same way: that a relative path in `sourceFiles[].file`, `source`, or a `rel: file` or `rel: source` `href` points at a file that exists. A path starts from the file being validated, or for `sourceFiles`, from the system's `sourceRoot`. It stays inside the same folder as `DSDS-05`, `DSDS-08` and `DSDS-09`. It covers a base document's own `refs`, as well as each entry's. It's a warning, which becomes a failure with `--strict`, like those three. It's the one rule that opens files beyond the document being validated. One related check isn't built, on purpose: confirming that an id or path *inside* another file exists, as checking a DTCG path would need. That means reading and parsing the other file, not only checking it's there. ## Rule details One section for each rule: what it checks, how to fix a finding and, for most validator rules, a document that breaks it on purpose. `dsds-validate --json` and `dsds-lint --json` link each finding to its rule's section here. This is generated from the same catalog as the table above. {/* dsds:rule-details */} ### DSDS-01 **At most one `sourceFiles` entry per platform.** *Checked by `dsds-validate`.* A component can't have two `sourceFiles` entries for the same `platform`, or two with no `platform` at all. A tool reads each platform's API from exactly one source file, so it needs to know which one. **How to fix:** Merge the duplicate `sourceFiles` entries, or give each one its own `platform`. **Example that breaks it:** [`DSDS-01-one-api-per-platform.yaml`](/examples/invalid/DSDS-01-one-api-per-platform.yaml) ### DSDS-02 **Once a system lists its platforms in `metadata.platforms`, no other platform can be used.** *Checked by `dsds-validate`.* When a `kind: system` entry lists `metadata.platforms`, every `platform` used anywhere in the document has to be on that list. That includes a component's `sourceFiles` and `imports` entries, and a status. **How to fix:** Use a platform listed in the system entry's `metadata.platforms`, or add the new platform to that list. **Example that breaks it:** [`DSDS-02-platform-vocabulary-status-list.yaml`](/examples/invalid/DSDS-02-platform-vocabulary-status-list.yaml), [`DSDS-02-platform-vocabulary.yaml`](/examples/invalid/DSDS-02-platform-vocabulary.yaml) ### DSDS-03 **A `checkedBy: automated` rule needs something that actually runs it.** *Checked by `dsds-validate`.* A guideline that says it's checked automatically has to list, in its `checks`, at least one test, lint rule or agent eval: a pointer with an `href` to something that runs outside the document. A checklist step (a `to`) doesn't count, because a person works through it. Without one, "automated" is a label nobody can verify. **How to fix:** Add the test, lint rule or agent eval to `checks` as an `href`, with `role` saying which. If nothing runs it, change `checkedBy` to `assisted` or `manual`. **Note:** This only checks that such a pointer exists, not what it proves. An agent eval often gives a pass rate or a judged score rather than a clear pass or fail. So `checkedBy: assisted` is the more honest choice, unless the eval really does give the same yes-or-no answer every time. **Example that breaks it:** [`DSDS-03-checked-by-needs-ref.yaml`](/examples/invalid/DSDS-03-checked-by-needs-ref.yaml) ### DSDS-04 **Entry and shared ids are unique within a document.** *Checked by `dsds-validate`.* `entries` and `shared` draw their ids from the same pool. A ref like `entryId#itemId` can't say which of the two lists it means. So no two entries can share an `id`, whichever list each one is in. **How to fix:** Rename one of the entries so every id across `entries` and `shared` is different. **Example that breaks it:** [`DSDS-04-unique-entry-id.yaml`](/examples/invalid/DSDS-04-unique-entry-id.yaml) ### DSDS-05 **An `entryId#itemId` ref must point at something that exists.** *Checked by `dsds-validate`.* When a ref's `to` has a `#` (see [common/ref](schema.html#common-ref)), the entry before the `#` has to exist. The part after it has to name something on that entry: an item, a trait (`#traitId`) or a trait value (`#traitId.valueId`). It also can't name two things equally (processing model §2). This is what makes a `same-as` reuse a real, checked link, rather than text that only looks right. It applies whether the target is in `shared` or `entries`. Since 0.23.0 it also checks the target of an inline link in a Markdown field, like `[focus visible](dsds:shared-a11y#focus-visible)` (processing model §2.4). **How to fix:** Change the part after `#` to an address that exists on the target entry, or give the item you mean that `id`. If it names two things, give one of them its own `id`. In an inline link, fix the part after `#` in the `dsds:` URL the same way. **Example that breaks it:** [`DSDS-05-inline-ambiguous-address.yaml`](/examples/invalid/DSDS-05-inline-ambiguous-address.yaml), [`DSDS-05-inline-unknown-address.yaml`](/examples/invalid/DSDS-05-inline-unknown-address.yaml), [`DSDS-05-item-ref-resolves.yaml`](/examples/invalid/DSDS-05-item-ref-resolves.yaml) ### DSDS-06 **A `uses`, `extends`, `same-as` or `replaced-by` chain must not loop.** *Checked by `dsds-validate`.* A consumer follows these four kinds of pointer from one thing to the next, to find out what an entry is built from, what it inherits from, where an item's content is stated, and what replaces something deprecated. Following any of them must never lead back to where it started, or the question has no answer. `uses` and `extends` link entries. `same-as` and `replaced-by` link items, trait values and entries. Each kind is checked on its own. **How to fix:** Remove or change one ref so the chain no longer leads back to where it started. **Note:** Until 0.22.0 this checked `composes` refs only, and DSDS-07 checked `depends-on`. 0.22.0 merged both into `uses`, and added the other three chains the processing model follows (§9). **Example that breaks it:** [`DSDS-06-same-as-cycle.yaml`](/examples/invalid/DSDS-06-same-as-cycle.yaml), [`DSDS-06-uses-cycle.yaml`](/examples/invalid/DSDS-06-uses-cycle.yaml) ### DSDS-07 **Retired: merged into DSDS-06.** *Retired in 0.22.0. No tool checks it.* Checked that a chain of `depends-on` refs didn't loop. 0.22.0 merged `depends-on` into `uses`, which DSDS-06 checks. The id is kept so it's never reused for a different rule. **How to fix:** Nothing to fix. A finding citing DSDS-07 came from a validator older than 0.22.0; see DSDS-06. ### DSDS-08 **A `to` with no `#` must point at an entry that exists.** *Checked by `dsds-validate`.* When a ref's `to` is just an id, with no `#`, the entry it names has to exist, in `entries` or `shared` (see `DSDS-04`). The validator looks across the whole project, not only the document it was given. It follows `rel: file` links, and the links in those files, and treats every file given to it in one run as part of the same project. If the target still isn't found, that's a warning rather than an error, unless there was nothing else to look in. Since 0.23.0 it also checks the target of an inline link in a Markdown field, like ``, including one that isn't shaped like a `to` at all, which the schema can't see inside a string. **How to fix:** Change `to` to an id that exists. If the target lives in another file, validate both files in the same run, or link them with a `rel: file` ref. In an inline link, change the part after `dsds:` to an id that exists, written the way a `to` would be. **Example that breaks it:** [`DSDS-08-entry-ref-resolves.yaml`](/examples/invalid/DSDS-08-entry-ref-resolves.yaml), [`DSDS-08-inline-malformed-target.yaml`](/examples/invalid/DSDS-08-inline-malformed-target.yaml), [`DSDS-08-inline-unknown-entry.yaml`](/examples/invalid/DSDS-08-inline-unknown-entry.yaml) ### DSDS-09 **A combo's `subject` and `items` must point at things that exist.** *Checked by `dsds-validate`.* Each `subject` and item in a combo names one of three things: a trait on the same entry (its id for a boolean trait, `traitId.valueId` for an enum value), a token in braces (`{color.action.primary}`), or an entry's id. So `subject: size.small` on a component with no `size` trait fails, and so does a token that doesn't exist. A trait is always looked up on the same entry, which the validator can always see in full, so a missing one is always an error. A token or an entry is looked up across the project, the same way as `DSDS-05` and `DSDS-08`, and is a warning when the search couldn't see every file. **How to fix:** Name a trait on this entry (`traitId` or `traitId.valueId`), a token in braces (`{token-id}`), or an existing entry id. **Example that breaks it:** [`DSDS-09-combo-target-resolves.yaml`](/examples/invalid/DSDS-09-combo-target-resolves.yaml) ### DSDS-10 **If a `same-as` item states a `level`, it must match its target's.** *Checked by `dsds-validate`.* An item that reuses another with `same-as` takes the target's `level`, so it doesn't need to state one: the pointer is the whole item. An author can still write a `level`, and then there are two copies with nothing else keeping them the same. When the target can be found, has a `level`, and the reusing item states one too, this checks the two match. An item meant to have a different level is a `refines`, not a `same-as`. **How to fix:** Remove `level` from the reusing item so it takes the target's. If the level is meant to differ, write a `for: agent` rule with its own level and point its `refines` at the target, instead of reusing it with `same-as`. **Example that breaks it:** [`DSDS-10-same-as-level-matches.yaml`](/examples/invalid/DSDS-10-same-as-level-matches.yaml) ### DSDS-11 **A relative file path in `sourceFiles`, `source`, `rel: file` or `rel: source` must point at a file that exists.** *Checked by `dsds-validate`.* A relative path should point at a real file. That covers a component's `sourceFiles[].file`, a token's or theme's `source`, and the `href` of any `rel: file` or `rel: source` ref. A typo or an out-of-date path quietly breaks what those fields promise: that a tool can read the API, or the DTCG tokens, straight from the file named there. Anything after a `#` or `?` is ignored, so only the file itself has to exist. A `sourceFiles` path starts from the system's `metadata.sourceRoot` when the validator can see it (in the same document, or in a file validated in the same run), and from the document's own folder otherwise. A `sourceRoot` that's a URL isn't checked. Only paths inside the folder of the file being validated are checked, the same limit `DSDS-05`, `DSDS-08` and `DSDS-09` use. A path that leads outside it, or a URL (`https:`, `npm:` and so on), is skipped. **How to fix:** Fix the path so it's correct relative to this document's folder (or, for `sourceFiles`, the system's `sourceRoot`), or remove the reference if the file is gone. If the code lives in another repository, set `metadata.sourceRoot` on the system entry. **Note:** A warning, which becomes a failure with `--strict`, like `DSDS-05`, `DSDS-08` and `DSDS-09`. It's opt-in for a similar reason: it's the one rule that opens files the validator otherwise wouldn't read. **Example that breaks it:** [`DSDS-11-base-document-file-ref.yaml`](/examples/invalid/DSDS-11-base-document-file-ref.yaml), [`DSDS-11-file-ref-exists.yaml`](/examples/invalid/DSDS-11-file-ref-exists.yaml), [`DSDS-11-source-root.yaml`](/examples/invalid/DSDS-11-source-root.yaml) ### DSDS-12 **Retired: a guideline's `level` already says how strong it is.** *Retired in 0.22.0. No tool checks it.* Reported a lowercase "must" or "should" in a guideline's `statement`, and asked for it in capitals. A guideline's `level` already says how strong the rule is, so capitals added nothing a tool needs. And in plain English, like "when a button must stay in the tab order", capitalizing the word added a requirement nobody meant. Documenting Sanity UI raised 161 of these. The id is kept so it's never reused. **How to fix:** Nothing to fix. Set the rule's strength with `level`. ### DSDS-13 **A token's `description` shouldn't just repeat its id, its name or its value.** *A suggestion from `dsds-lint`. It never fails a build.* A token's `description` is optional and meant to be short. If an author writes one, it should add what the id can't, such as the token's role or when to use it. A description that only repeats the id or name, or the raw value the DTCG file already holds, looks like documentation but says nothing. **How to fix:** Say what the token is for or when to use it, or remove the description. **Note:** Ported from PR #33 by Cody Clark, first written as DSDS-011 for the model before 0.20.0 (`git show 3c805b7`). It uses the same method: after tidying the text, it flags an exact repeat or a bare value. It errs on the side of not flagging: a description with any role or usage information in it is never flagged. ### DSDS-14 **A `must` or `must-not` guideline has no `checkedBy`.** *A suggestion from `dsds-lint`. It never fails a build.* `checkedBy` says whether a guideline is checked automatically, with help, or by hand. Leaving it out means a tool can't tell whether the rule can be checked at all, not just that nobody has yet. Only `must` and `must-not` items are flagged: a `should` or `may` item that doesn't say how it's checked matters much less. An item that reuses another with `same-as` (DSDS-10) takes the target's `checkedBy`, like the rest of its content, so it's only flagged when neither one has it. A `same-as` target in another document isn't flagged: the lint can't see it, and making the pointer repeat its `checkedBy` would defeat the point of the pointer. **How to fix:** Add `checkedBy: manual`, `checkedBy: assisted`, or `checkedBy: automated` with a `checks` ref. **Note:** DSDS-03 already blocks a narrower case: `checkedBy: automated` with no `checks` to back it. This advisory rule covers the case DSDS-03 can't see: `checkedBy` left out entirely. ### DSDS-15 **A component has no `guidelines` section framed `when-to-use`.** *A suggestion from `dsds-lint`. It never fails a build.* "When do I use this?" is usually the first question a component's documentation has to answer, before how to use it well. This flags a component with no guidelines section that has `framing: when-to-use`. **How to fix:** Add a `guidelines` section with `framing: when-to-use`. **Note:** Adapted from the 0.16.0 branch's "entity-missing-use-cases" check, which looked for a separate `use-cases` block. 0.20.0 moved that content into a `guidelines` section, so this looks for the section instead. ### DSDS-16 **A token's `description` shouldn't just repeat its place on a scale.** *A suggestion from `dsds-lint`. It never fails a build.* A token's optional `description` should add what the id can't, such as the token's role or when to use it. One that only gives its place on a scale ("Shade 900", "Step 500", "Level 6 of the neutral scale") looks like documentation but says nothing. That number is already in the id, and in where the token sits among its `metadata.group`. This is the companion to DSDS-13: the scale step, rather than the id or the value. **How to fix:** Say what the token is for or when to use it, or remove the description. **Note:** Ported from PR #37 by Cody Clark, first proposed as DSDS-012 for the model before 0.20.0. It uses the same method: it flags a description that is only a scale word followed by a number, optionally followed by "of the \ scale" or "ramp". 0.20.0 has no separate token-group kind, so it checks `kind: token` only. It errs on the side of not flagging. The scale word has to come first and be singular, so forms like "Nine weights", "Twelve steps" and "Neutral 700" are left alone, and any word about role or usage stops a match. A description that also repeats the id or name is left to DSDS-13, so it's only reported once. ### DSDS-17 **Retired: `dsds-format` applies this order.** *Retired in 0.22.0. No tool checks it.* Reported that an entry's own fields weren't in STYLE_GUIDE.md's order. That order has one right answer, read from the schema files, so 0.22.0 replaced the report with `dsds-format`, which puts the fields in order. The id is kept so it's never reused. **How to fix:** Run `dsds-format` on the document. ### DSDS-18 **An entry's `sections` aren't grouped and ordered as STYLE_GUIDE.md asks.** *A suggestion from `dsds-lint`. It never fails a build.* STYLE_GUIDE.md §4 asks for sections of the same `kind` to sit together, with the kinds running from general to specific: guidelines, definitions, steps, then the generic `section`. Several `guidelines` sections are ordered by scope: `framing: when-to-use` first, then `how-to-use` or no framing. Sections with the same framing are then ordered by who they're for: `for: all`, then `human`, then `agent`. **How to fix:** Keep sections of one kind together, in the order guidelines, definitions, steps, section. Order guidelines sections by framing, then audience. **Note:** Framing comes first and audience second, so audience is only compared between sections with the same framing. A `for: agent` when-to-use section rightly comes before a `for: all` how-to-use one, so comparing audience across framings would flag something that's fine. Until 0.22.0 a section with `tags` was ranked as a third, narrowest group. That gave `tags` two jobs, classifying a section and placing it, so tagging a section could put it out of order. `tags` only classify now, and this rule ignores them. ### DSDS-19 **A `guidelines` section's items aren't ordered must, should, may, should-not, must-not.** *A suggestion from `dsds-lint`. It never fails a build.* STYLE_GUIDE.md §5 asks for the items in a guidelines section to be ordered by `level`, in exactly that order: strongest "do" to strongest "don't", with the two weaker levels in the middle. Items with the same level keep the order they're written in; only an item placed before a stronger level is flagged. An item with no `level` of its own is ranked at its `same-as` target's level when the target is in the same document, and left out of the check when it isn't. **How to fix:** Reorder the items: must, should, may, should-not, must-not. ### DSDS-20 **Retired: `dsds-format` applies this order.** *Retired in 0.22.0. No tool checks it.* Reported that a base document's own fields weren't in STYLE_GUIDE.md's order. That order has one right answer, read from the schema files, so 0.22.0 replaced the report with `dsds-format`, which puts the fields in order. The id is kept so it's never reused. **How to fix:** Run `dsds-format` on the document. ### DSDS-21 **A base document's `entries` aren't ordered by `kind`.** *A suggestion from `dsds-lint`. It never fails a build.* STYLE_GUIDE.md §1 asks for entries to be ordered by `kind`, in the order entries/entry.schema.yaml lists them: `system`, `token`, `theme`, `component`, then `entry`. A namespaced custom kind isn't on that list, and goes after all five. **How to fix:** Reorder `entries` by kind: `system`, `token`, `theme`, `component`, `entry`, then any custom kinds. **Note:** The order of entries of the same kind isn't checked on purpose. STYLE_GUIDE.md asks for "whatever order reads best" there, which no tool can work out, so only an entry placed before a kind that should come first is reported. ### DSDS-22 **Retired: `dsds-format` applies this order.** *Retired in 0.22.0. No tool checks it.* Reported that a nested object's fields weren't in the order its schema file lists them. That order has one right answer, read from the schema files, so 0.22.0 replaced the report with `dsds-format`, which puts the fields in order. The id is kept so it's never reused. **How to fix:** Run `dsds-format` on the document. ### DSDS-23 **Retired: `dsds-format` applies this order.** *Retired in 0.22.0. No tool checks it.* Reported that an entry's `combos` didn't keep each `subject` together, in `level` order. That grouping has one right answer, so 0.22.0 replaced the report with `dsds-format`, which groups them. The id is kept so it's never reused. **How to fix:** Run `dsds-format` on the document. ### DSDS-24 **A generic `entry` or `section` has a field no DSDS schema declares.** *Checked by `dsds-validate`.* `kind: entry` and `kind: section` are checked against the open base that the other kinds build on. So the schema accepts any extra field on them, and on a generic entry's `metadata`. That openness is what lets namespaced custom kinds add fields. But on the generic kinds it means a typo (`descripton`) or a field a release removed (`purpose`) passes without a word. This reports each one. Namespaced custom kinds aren't checked, since extra fields are what they're for. **How to fix:** Fix the spelling, or remove the field. Put vendor data under `$extensions` with a namespaced key. If you need real extra fields, use a namespaced custom kind (`acme.pattern`) instead of `kind: entry`. **Note:** A warning, which becomes a failure with `--strict`. Added in 0.22.0 instead of closing the two kinds, which would have broken any document that relies on them being open. A validator that only runs the JSON Schema won't report it, because the schema still accepts the field. The items of a generic section aren't checked, because the base doesn't say what shape they have. **Example that breaks it:** [`DSDS-24-undeclared-field.yaml`](/examples/invalid/DSDS-24-undeclared-field.yaml) ### DSDS-25 **A pointer must sit where its kind belongs.** *Checked by `dsds-validate`.* `same-as` reuses another item's content, so it goes on a section item and has to point at an item of the same section kind: a guideline reuses a guideline, a definition a definition, a step a step. `replaced-by` names what replaces something deprecated, so it goes on a deprecated entry, trait or trait value. A kind of pointer that has its own field goes in that field, not in `refs`: `extends` and `alternatives` on an entry, `specs` on a component, and `alternatives`, `evidence`, `checks` and `refines` on a guideline item. And a `rel: file` ref points at another DSDS document, which a consumer reads as part of the project. So one that points at a code or token file is reported as a warning, judged by its file extension or, for a `.json` file on disk, by its content. Processing model §3, §4, §5 and §7. **How to fix:** Point `same-as` at an item of the same section kind. Move `replaced-by` onto the deprecated entry, trait or trait value, or deprecate the thing it sits on. Move a pointer with its own field into that field, without its `rel`. Make a `rel: file` that points at a code or token file `rel: source`. **Note:** Added in 0.22.0, with the processing model. The `same-as` and `replaced-by` checks only run once the target is found, so they never repeat what DSDS-05 or DSDS-08 already report. **Example that breaks it:** [`DSDS-25-file-ref-not-a-document.yaml`](/examples/invalid/DSDS-25-file-ref-not-a-document.yaml), [`DSDS-25-ref-placement.yaml`](/examples/invalid/DSDS-25-ref-placement.yaml) {/* /dsds:rule-details */} ## Conformance suite `scripts/validate/validate.js` is the reference implementation, but DSDS doesn't need it to be the only one. A second validator, written independently, is one of the spec's [1.0 criteria](stability.html#criteria-for-declaring-10). The conformance suite is what that second validator tests itself against, without reading any of this repo's JavaScript. Each release publishes it at [`/v0.23.0/conformance-suite/manifest.json`](/v0.23.0/conformance-suite/manifest.json), with a copy of every example it lists (`/v0.23.0/conformance-suite/examples/invalid/`). `scripts/generate/generate-conformance-suite.mjs` builds it from the comment at the top of each example: the same `rejectedBy`, `expect` and `errorAt` described under [Conforming validator](#conforming-validator). It isn't kept by hand, so it can't drift from the examples. **How to test a validator against it**, in any language: 1. Fetch the manifest. For each item in `fixtures`, fetch the file it names, relative to the manifest. 2. Validate that file against the schema bundle for the manifest's `schemaVersion`. 3. Check that the document is **rejected**, and rejected for the stated reason, not just for any reason: - `rejectedBy: "schema"`: at least one error has to come from the schema itself, with no `DSDS-XX` rule id. If `errorAt` is set, one of those errors has to be at that location (`"/"` for the top of the document). - `rejectedBy: "semantic"`: every id in `expect` has to appear among the errors *or* warnings. A semantic rule can report either, depending on whether the validator runs in a strict mode. 4. A document that passes, or fails for a different reason than it states, means the validator **doesn't conform**. Rejecting it isn't enough: it has to be rejected for the right reason. `scripts/validate/conformance-test.js` already does exactly this for `scripts/validate/validate.js`, on every `npm run check`. The manifest lets something other than that script run the same test. ## Open conventions Some questions the schema leaves open on purpose. They aren't oversights: a fixed answer would suit some teams and not others. - **Where does a pointer on a guideline item go: `refs`, or a named field?** Each named field holds one kind of pointer and says what kind, so a pointer there has no `rel`: `uses`, `alternatives` (alternative-to), `evidence` (cites), `checks` (checked-by) and `refines`. Every other kind, such as `same-as` and `relates-to`, goes in `refs` with its `rel`. A kind that has its own field never goes in `refs` (`DSDS-25`). See [common/ref](schema.html#common-ref) for what each `rel` means. - **Where does an entry's main source file go?** In `refs`, with `rel: source`. A component's per-platform source files go in `sourceFiles` instead. See [common/ref](schema.html#common-ref). - **What does `tags[0]` mean?** By convention, the first tag is the entry's main category. See [metadata](schema.html#metadata-metadata). - **How does a token's `source` point at one token inside a shared DTCG file?** By convention, a token's `id` is also its path in the DTCG file: `color.action.primary` names the same token in both. That's why a token's `id` allows the slashes other DSDS ids don't. When a project's DTCG paths don't match its DSDS ids, add a JSON Pointer to the file path instead (`./tokens.dtcg.json#/color/action/primary`). It's ordinary URL syntax, so the schema doesn't need to change. - **What is a component's status when it ships on more than one platform?** Write one `metadata.status` for each platform, and work out the overall status from those. Don't write a separate overall status that could quietly disagree. See [Status across platforms](#status-across-platforms) below. ## Status across platforms A component rarely reaches the same stage everywhere at once. It might be stable on the web, in beta on iOS, and not started on Android. `metadata.status` takes either one status or a list of them, each of which can name its own `platform`. ```yaml metadata: status: - platform: react status: stable since: "1.0.0" - platform: ios status: beta since: "2.0.0" ``` **A consumer SHOULD work out a component's overall status from its per-platform statuses, instead of expecting one to be written separately.** There's no `overall` field, on purpose. A separate overall status is a second copy of the truth, and it goes stale as soon as one platform changes. Every consumer that needs an overall status can work it out the same way from what's already there. How to combine them is up to the consumer, because it depends on the question. Two approaches cover most cases: - **The least mature status wins.** This answers "can I rely on this everywhere?" A component that's `stable` on the web and `beta` on iOS is `beta` overall. - **Show each platform separately.** This answers "can I rely on this *here*?" A tool showing a React developer the React status shouldn't mark it down because iOS is behind. When a document uses per-platform statuses, a producer SHOULD list `metadata.platforms` on the system entry. Then a consumer can tell a platform that's deliberately unsupported from one that just has no status yet, and `DSDS-02` stops any other platform being used. A single status with no `platform` still means what it always has: the entry's status everywhere. ## Passing isn't the same as good A document with no errors and no warnings can still be poor documentation. The schema checks structure, not judgment. [`examples/anti-patterns/`](examples.html#anti-patterns) collects a few small documents that pass and are still worth avoiding: a definition that only repeats its own term, a `checkedBy: manual` claim too vague to check, and a guideline that talks about a "token-group" entry, which the spec doesn't have. A comment at the top of each file says what's wrong and why the schema can't catch it. They're left out of `npm run check` on purpose, and aren't meant to be copied. ## Index of every normative statement Every requirement sentence (with MUST, MUST NOT, SHOULD, SHOULD NOT or MAY) in the schema's own text, from both `description` and `$comment`. `scripts/generate/extract-normative.mjs` pulls them straight from `schema/**/*.schema.yaml` and rebuilds this list on every build (`npm run generate`, checked by `generate:check`). Nobody copies them by hand. Every statement below is in a `description`, which is where this spec keeps its rules. `$comment` holds the reasoning beside them. A requirement found in a `$comment` is marked *(in `$comment`)*, and fails `npm run check`. Each statement's id is based on where it lives (`/§.`). So if the schema moves under a citation, the id breaks visibly instead of quietly pointing at the wrong thing. {/* dsds:normative-index */} *Generated from the v0.23.0 schemas by `scripts/generate/extract-normative.mjs` — do not edit by hand. 19 statements: 14 MUST, 2 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` #### common/ref - **MUST NOT** — A conforming consumer works out the reverse direction itself, and MUST NOT require it to be written on the target as well. `common/ref§rel.1` - **MUST** — Where an object has a field named for a kind of pointer, that kind MUST go in the field, not in `refs` (DSDS-25). `common/ref§rel.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` - **MUST** — Every relative `sourceFiles` path in the project MUST be read from here when it's set (DSDS-11). `metadata/system-metadata§[allOf][1].sourceRoot.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` {/* /dsds:normative-index */} --- # Examples — DSDS 0.23.0 Every file below lives in the repo's `examples/` directory and is validated on every `npm run check` — nothing here is aspirational or out of date with the schema it demonstrates. `npm run check` additionally confirms any example built from hand-split fragments (`scripts/tools/compose.js`) still matches what those fragments say. For the example most directly relevant to a specific schema feature, see that feature's own description on the [Schema](schema.html) page or its mention on [Quick start](quickstart.html) — this page is the full index, not a curated tour. {/* dsds:examples-index */} ## anti-patterns/ Documents that validate cleanly and are still worth avoiding — the schema checks structure, not judgment. See each file's own leading comment. - [`anti-patterns/accessibility-says-follows-wcag.yaml`](/examples/anti-patterns/accessibility-says-follows-wcag.yaml) - [`anti-patterns/circular-definition.yaml`](/examples/anti-patterns/circular-definition.yaml) - [`anti-patterns/stale-terminology.yaml`](/examples/anti-patterns/stale-terminology.yaml) - [`anti-patterns/unfalsifiable-manual-check.yaml`](/examples/anti-patterns/unfalsifiable-manual-check.yaml) ## base/ Full base documents — a system with multiple entries, split across files via `rel: file`. - [`base/react-extension.dsds.yaml`](/examples/base/react-extension.dsds.yaml) - [`base/starter-kit-fragments/00-system.dsds.yaml`](/examples/base/starter-kit-fragments/00-system.dsds.yaml) - [`base/starter-kit-fragments/01-tokens-and-themes.dsds.yaml`](/examples/base/starter-kit-fragments/01-tokens-and-themes.dsds.yaml) - [`base/starter-kit-fragments/02-components.dsds.yaml`](/examples/base/starter-kit-fragments/02-components.dsds.yaml) - [`base/starter-kit.dsds.yaml`](/examples/base/starter-kit.dsds.yaml) - [`base/tokens/dark.tokens.json`](/examples/base/tokens/dark.tokens.json) - [`base/tokens/light.tokens.json`](/examples/base/tokens/light.tokens.json) ## entries/ Standalone entry files, one per kind, plus the source/manifest/story files a couple of them point at. - [`entries/button-group.yaml`](/examples/entries/button-group.yaml) - [`entries/button.yaml`](/examples/entries/button.yaml) - [`entries/chunks/confirmation-dialog.tsx`](/examples/entries/chunks/confirmation-dialog.tsx) - [`entries/color-action-primary-hover.yaml`](/examples/entries/color-action-primary-hover.yaml) - [`entries/color-action-primary.yaml`](/examples/entries/color-action-primary.yaml) - [`entries/color-surface-default.yaml`](/examples/entries/color-surface-default.yaml) - [`entries/color-surface-raised.yaml`](/examples/entries/color-surface-raised.yaml) - [`entries/confirmation-dialog.yaml`](/examples/entries/confirmation-dialog.yaml) - [`entries/dark.yaml`](/examples/entries/dark.yaml) - [`entries/dialog.yaml`](/examples/entries/dialog.yaml) - [`entries/empty-state.yaml`](/examples/entries/empty-state.yaml) - [`entries/error-state.yaml`](/examples/entries/error-state.yaml) - [`entries/getting-started.yaml`](/examples/entries/getting-started.yaml) - [`entries/icon-button.yaml`](/examples/entries/icon-button.yaml) - [`entries/icon.yaml`](/examples/entries/icon.yaml) - [`entries/link.yaml`](/examples/entries/link.yaml) - [`entries/manifests/button.cem.json`](/examples/entries/manifests/button.cem.json) - [`entries/space-4.yaml`](/examples/entries/space-4.yaml) - [`entries/spacing-scale.yaml`](/examples/entries/spacing-scale.yaml) - [`entries/src/Button.tsx`](/examples/entries/src/Button.tsx) - [`entries/stories/button.stories.tsx`](/examples/entries/stories/button.stories.tsx) - [`entries/tokens/dark.tokens.json`](/examples/entries/tokens/dark.tokens.json) - [`entries/tokens.dtcg.json`](/examples/entries/tokens.dtcg.json) ## interop/ Worked pairs showing a DSDS entry pointing at a real DTCG token file or CEM manifest, instead of restating it. - [`interop/color-action-primary.dsds.yaml`](/examples/interop/color-action-primary.dsds.yaml) - [`interop/color-action-primary.tokens.json`](/examples/interop/color-action-primary.tokens.json) - [`interop/my-element.custom-elements.json`](/examples/interop/my-element.custom-elements.json) - [`interop/my-element.dsds.yaml`](/examples/interop/my-element.dsds.yaml) - [`interop/my-element.js`](/examples/interop/my-element.js) - [`interop/nested-color.dsds.yaml`](/examples/interop/nested-color.dsds.yaml) - [`interop/nested-color.tokens.json`](/examples/interop/nested-color.tokens.json) ## invalid/ One broken example per semantic rule (`DSDS-XX-*.yaml`) plus plain schema fixtures (`schema-*.yaml`) — the negative-test corpus `scripts/validate/conformance-test.js` runs against. - [`invalid/DSDS-01-one-api-per-platform.yaml`](/examples/invalid/DSDS-01-one-api-per-platform.yaml) - [`invalid/DSDS-02-platform-vocabulary-status-list.yaml`](/examples/invalid/DSDS-02-platform-vocabulary-status-list.yaml) - [`invalid/DSDS-02-platform-vocabulary.yaml`](/examples/invalid/DSDS-02-platform-vocabulary.yaml) - [`invalid/DSDS-03-checked-by-needs-ref.yaml`](/examples/invalid/DSDS-03-checked-by-needs-ref.yaml) - [`invalid/DSDS-04-unique-entry-id.yaml`](/examples/invalid/DSDS-04-unique-entry-id.yaml) - [`invalid/DSDS-05-inline-ambiguous-address.yaml`](/examples/invalid/DSDS-05-inline-ambiguous-address.yaml) - [`invalid/DSDS-05-inline-unknown-address.yaml`](/examples/invalid/DSDS-05-inline-unknown-address.yaml) - [`invalid/DSDS-05-item-ref-resolves.yaml`](/examples/invalid/DSDS-05-item-ref-resolves.yaml) - [`invalid/DSDS-06-same-as-cycle.yaml`](/examples/invalid/DSDS-06-same-as-cycle.yaml) - [`invalid/DSDS-06-uses-cycle.yaml`](/examples/invalid/DSDS-06-uses-cycle.yaml) - [`invalid/DSDS-08-entry-ref-resolves.yaml`](/examples/invalid/DSDS-08-entry-ref-resolves.yaml) - [`invalid/DSDS-08-inline-malformed-target.yaml`](/examples/invalid/DSDS-08-inline-malformed-target.yaml) - [`invalid/DSDS-08-inline-unknown-entry.yaml`](/examples/invalid/DSDS-08-inline-unknown-entry.yaml) - [`invalid/DSDS-09-combo-target-resolves.yaml`](/examples/invalid/DSDS-09-combo-target-resolves.yaml) - [`invalid/DSDS-10-same-as-level-matches.yaml`](/examples/invalid/DSDS-10-same-as-level-matches.yaml) - [`invalid/DSDS-11-base-document-file-ref.yaml`](/examples/invalid/DSDS-11-base-document-file-ref.yaml) - [`invalid/DSDS-11-file-ref-exists.yaml`](/examples/invalid/DSDS-11-file-ref-exists.yaml) - [`invalid/DSDS-11-source-root.yaml`](/examples/invalid/DSDS-11-source-root.yaml) - [`invalid/DSDS-24-undeclared-field.yaml`](/examples/invalid/DSDS-24-undeclared-field.yaml) - [`invalid/DSDS-25-file-ref-not-a-document.yaml`](/examples/invalid/DSDS-25-file-ref-not-a-document.yaml) - [`invalid/DSDS-25-ref-placement.yaml`](/examples/invalid/DSDS-25-ref-placement.yaml) - [`invalid/schema-combo-target-pattern.yaml`](/examples/invalid/schema-combo-target-pattern.yaml) - [`invalid/schema-component-unknown-field.yaml`](/examples/invalid/schema-component-unknown-field.yaml) - [`invalid/schema-empty-entries-array.yaml`](/examples/invalid/schema-empty-entries-array.yaml) - [`invalid/schema-entry-id-pattern.yaml`](/examples/invalid/schema-entry-id-pattern.yaml) - [`invalid/schema-extensions-unnamespaced-key.yaml`](/examples/invalid/schema-extensions-unnamespaced-key.yaml) - [`invalid/schema-kind-enum-invalid.yaml`](/examples/invalid/schema-kind-enum-invalid.yaml) - [`invalid/schema-legacy-documentblocks-field.yaml`](/examples/invalid/schema-legacy-documentblocks-field.yaml) - [`invalid/schema-legacy-dsdsversion-field.yaml`](/examples/invalid/schema-legacy-dsdsversion-field.yaml) - [`invalid/schema-legacy-identifier-field.yaml`](/examples/invalid/schema-legacy-identifier-field.yaml) - [`invalid/schema-named-field-rel.yaml`](/examples/invalid/schema-named-field-rel.yaml) - [`invalid/schema-ref-mutually-exclusive-to-href.yaml`](/examples/invalid/schema-ref-mutually-exclusive-to-href.yaml) - [`invalid/schema-replaced-by-not-deprecated.yaml`](/examples/invalid/schema-replaced-by-not-deprecated.yaml) - [`invalid/schema-requirement-level-casing.yaml`](/examples/invalid/schema-requirement-level-casing.yaml) - [`invalid/schema-source-field-rel.yaml`](/examples/invalid/schema-source-field-rel.yaml) - [`invalid/schema-sourcefile-platform-pattern.yaml`](/examples/invalid/schema-sourcefile-platform-pattern.yaml) - [`invalid/schema-sourcefiles-empty-array.yaml`](/examples/invalid/schema-sourcefiles-empty-array.yaml) - [`invalid/schema-trait-missing-kind.yaml`](/examples/invalid/schema-trait-missing-kind.yaml) - [`invalid/schema-trait-value-deprecated-without-notice.yaml`](/examples/invalid/schema-trait-value-deprecated-without-notice.yaml) ## quickstart/ The Quick Start guide's own snippets, one per step, building up from a bare base document to a described, related entry. - [`quickstart/01-base-document.yaml`](/examples/quickstart/01-base-document.yaml) - [`quickstart/02-base-document-described.yaml`](/examples/quickstart/02-base-document-described.yaml) - [`quickstart/03-base-document-entries.yaml`](/examples/quickstart/03-base-document-entries.yaml) - [`quickstart/04-base-document-refs.yaml`](/examples/quickstart/04-base-document-refs.yaml) - [`quickstart/05-button-entry.yaml`](/examples/quickstart/05-button-entry.yaml) - [`quickstart/06-button-described.yaml`](/examples/quickstart/06-button-described.yaml) - [`quickstart/07-button-custom.yaml`](/examples/quickstart/07-button-custom.yaml) - [`quickstart/08-token-entry.yaml`](/examples/quickstart/08-token-entry.yaml) - [`quickstart/09-token-described.yaml`](/examples/quickstart/09-token-described.yaml) - [`quickstart/10-pattern-entry.yaml`](/examples/quickstart/10-pattern-entry.yaml) - [`quickstart/11-custom-entry.yaml`](/examples/quickstart/11-custom-entry.yaml) - [`quickstart/12-shared-sections.yaml`](/examples/quickstart/12-shared-sections.yaml) - [`quickstart/13-inline-links.yaml`](/examples/quickstart/13-inline-links.yaml) - [`quickstart/components/button.dsds.yaml`](/examples/quickstart/components/button.dsds.yaml) - [`quickstart/tokens.dtcg.json`](/examples/quickstart/tokens.dtcg.json) ## vectors/ - [`vectors/addresses/expected.json`](/examples/vectors/addresses/expected.json) - [`vectors/addresses/input.yaml`](/examples/vectors/addresses/input.yaml) - [`vectors/cross-file/button.yaml`](/examples/vectors/cross-file/button.yaml) - [`vectors/cross-file/expected.json`](/examples/vectors/cross-file/expected.json) - [`vectors/cross-file/index.yaml`](/examples/vectors/cross-file/index.yaml) - [`vectors/cycles/expected.json`](/examples/vectors/cycles/expected.json) - [`vectors/cycles/input.yaml`](/examples/vectors/cycles/input.yaml) - [`vectors/inline-links/expected.json`](/examples/vectors/inline-links/expected.json) - [`vectors/inline-links/input.yaml`](/examples/vectors/inline-links/input.yaml) - [`vectors/inverse/expected.json`](/examples/vectors/inverse/expected.json) - [`vectors/inverse/input.yaml`](/examples/vectors/inverse/input.yaml) - [`vectors/reuse/expected.json`](/examples/vectors/reuse/expected.json) - [`vectors/reuse/input.yaml`](/examples/vectors/reuse/input.yaml) - [`vectors/successors/expected.json`](/examples/vectors/successors/expected.json) - [`vectors/successors/input.yaml`](/examples/vectors/successors/input.yaml) *110 files across 7 categories, generated from the `examples/` directory by `scripts/generate/generate-examples-index.mjs` — do not edit by hand.* {/* /dsds:examples-index */} --- # Extending the schema — DSDS 0.23.0 DSDS ships with a fixed set of kinds, and a fixed set of fields on each. Sooner or later, every real project needs more than that — data a specific tool wants to store, a document type the spec has no name for, or fields that should be required instead of just optional. This page covers the three ways to get there, and when to reach for each one. | Mechanism | What it's for | Who else can read it | |---|---|---| | `$extensions` | Data a specific tool needs, attached to something the spec already models | Only the tool that owns the namespace — everyone else skips it, by design | | A custom kind | A kind of document the spec has no name for at all | Nobody, unless you also publish the schema that defines it | | A profile | Making an existing kind's *optional* fields required, for your own project | Everybody — the document is still fully standard DSDS | ## `$extensions` `$extensions` is a place to put vendor or tool-specific data, organized by namespace so different tools don't collide. It's available at the document, entry, and section level — and now on every section item too: a `guidelines`, `definitions`, or `steps` item, and a `freeform` entry. Every top-level key under `$extensions` MUST be a dotted namespace (`com.figma`, `com.acme`), matching the [Design Tokens Community Group's own `$extensions` convention](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/). That namespace is what lets a tool that doesn't recognize your data ignore it safely — it just skips any key it doesn't own. ### Why item-level `$extensions` matters A `guidelines` item only accepts a fixed set of fields — `statement`, `level`, `refs`, and a few others, nothing else. That's on purpose: any tool that understands the `guidelines` kind keeps understanding it, with no surprise fields to trip over. But until now, that also meant there was no way to attach a reason, or a documented failure case, to *one specific rule* — you'd either have to give up the well-known `guidelines` kind entirely (see [custom kinds](#custom-kinds) below), or wait for the spec itself to add a field that everyone else would then be stuck with too, whether they wanted it or not. `$extensions` on the item itself solves that without loosening the field for everyone: ```yaml sections: - kind: guidelines for: human items: - level: should statement: Limit each surface to one primary button. $extensions: com.acme: rationale: Multiple primary buttons compete for attention and force the user to guess which action is actually the recommended one. failureMode: A dialog ships with two primary-styled buttons (e.g. "Save" and "Save as draft"), and usability testing shows users default to the wrong one. ``` A tool that doesn't know about `com.acme` — the reference validator, another vendor's linter, a plain DSDS reader — skips it entirely. The document is still fully standard DSDS: remove the `$extensions` block and everything left over was already valid on its own. See it in the repo's own [`button.yaml`](/examples/entries/button.yaml) for a real, validated example. ## Custom kinds When the generic `entry` kind isn't specific enough — you want your own name for a whole category of document — use a custom kind instead of `entry`: ```yaml kind: acme.excerpt id: acme-excerpt-onboarding name: Onboarding excerpt description: A short, reusable pull-quote of onboarding copy, shared across three different surfaces. ``` A custom entry kind (`acme.excerpt` above) or a custom section kind (`acme.checklist`, say) is checked against the same **open** base every generic kind is — `entry.schema.yaml` for an entry, `section.schema.yaml` for a section. Open means genuinely open: no schema file exists for `acme.excerpt` in this repo, so nothing stops a misspelled field, or a value of the wrong type, from passing silently. If you want your custom kind to actually enforce anything — required fields, no unexpected properties — you have to write and ship that schema file yourself, in your own copy of `scripts/validate/validate.js`'s schema folder. Neither DSDS itself, nor any other tool, will ever recognize a `kind: acme.excerpt` document as anything more than a set of open fields. That's the tradeoff with a custom kind: total freedom over the fields, but total unfamiliarity to every other tool. It's the right choice for a document type that's genuinely yours and nobody else's. It's the wrong choice for making a kind the spec already defines stricter — for that, use a profile instead. Renaming `component` to `acme.component` to "add a required field" doesn't make the component schema stricter — it drops it entirely. `traits`, `combos`, and `sourceFiles` all stop being checked, because nothing at that new kind name says they still apply. See [Profiles](#profiles) below for the way to actually make a kind stricter instead of replacing it. ## Profiles A profile is a small, local schema file that turns some of a kind's *optional* fields into required ones, just for your own project. It never adds a new field — it can only make an existing rule stricter. That one property is what makes a profile safe to build on: **a profile may narrow. It must not extend.** A document that passes your profile is still a fully valid DSDS document to any tool that's never heard of your profile — nothing is added, nothing is different, just a stricter bar for your own project to clear. A profile is built with JSON Schema's `allOf`, which only ever *combines* rules — it can never take one away. There's no way to write a profile that makes a required field optional, because the base schema's own list of required fields is still one of the rules being checked. Try to loosen a requirement from the base schema, and the original requirement simply stays in force. This isn't a new idea — other standards use the same pattern to narrow a shared format for one group's needs (the EU's [DCAT-AP](https://www.w3.org/2016/12/staging-dcat-ap/) metadata standard is one example). DSDS didn't need to invent a mechanism for this: `allOf` plus `required` (and, for a rule that only applies sometimes, `if`/`then`) is already the standard way to write a profile in JSON Schema. What DSDS adds is a documented place to put one. ### The `profiles/` folder Save a file at `profiles/entries/.schema.yaml` or `profiles/sections/.schema.yaml`, and `scripts/validate/validate.js` picks it up automatically — no code change, nothing to register. `` can be a well-known kind (`component`, `guidelines`) or a custom one. A profile file needs its own `$id`, different from the schema it's narrowing — reusing the built-in schema's `$id` crashes the validator outright, since two schemas can't share the same id. It narrows the real schema by linking to it with `$ref`, inside `allOf`: ```yaml # profiles/entries/component.schema.yaml $schema: https://json-schema.org/draft/2020-12/schema $id: https://your-org.example/profiles/entries/component.schema.yaml title: Acme's stable-component profile description: Once a component's status is stable, it must have documentation sections and at least one source file. allOf: - $ref: https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml - if: required: [metadata] properties: metadata: required: [status] properties: status: required: [status] properties: status: {const: stable} then: required: [sections, sourceFiles] ``` This is the most useful form of profile: *if the status is stable, then require these fields.* It works on anything with a lifecycle field — a component, a token, a theme — and turns "is the documentation complete?" from a question someone has to remember to ask into something the build checks automatically. `profiles/` is never bundled into the published schema. `scripts/generate/bundle.js` only ever looks inside `schema/`, so a project's own profile — which reflects decisions specific to that project, not to the spec itself — can never end up in the published `dsds.bundled.yaml`. Only the command-line validator reads it, and only for the project that added it. `profiles/` is opt-in, per project — a profile reflects *your* completeness bar, not the spec's. This repo's own `npm run check` validates the spec's example files, which intentionally range from bare-minimum to fully filled out; requiring every one of them to be fully complete would defeat the point of having minimal examples at all. The snippet above is real and has been tested, it's just not saved in this repo's own `profiles/` folder — copy it into yours to see it work. ### What a profile can't do A profile can't add a field. If your organization needs a field the spec truly doesn't have anywhere — not "optional and currently empty," but genuinely missing — that's not a job for a profile. Use `$extensions` if it's your own tool's data, or raise it with the spec itself if it's something every DSDS user would benefit from. ## Things people often want to add Before reaching for `$extensions`, a custom kind, or a profile, check this list — a few common requests already have a home, no extension needed: - **A prop table.** Point a tool at the real source instead — a component's `sourceFiles` (or, for an already-generated manifest, `specs`). - **An anatomy diagram.** Anatomy is prose about parts, the same job `definitions` already does. Use a `definitions` section with `context: anatomy`. - **A `token-group` kind.** A group is a fact about its members, not a fourth kind. Set `metadata.group` on each token instead. - **Typed accessibility fields** (`wcagLevel`, ARIA facts). Treat accessibility guidance as ordinary guidance: a `guidelines` item with `evidence` pointing at the WCAG criterion, and `checks` pointing at the test that verifies it. - **Separate relationship/link/reference types.** One type already covers it — `common/ref`: `to`/`href` plus `rel`. - **A migration guide.** Each schema file's own `$comment` carries the reasoning; the [`CHANGELOG`](https://github.com/somerandomdude/design-system-documentation-schema/blob/main/CHANGELOG) has the field-by-field mapping. Still missing something after checking this list? That's what the rest of this page is for: - **A rationale or failure-mode field on a guideline item** — this is exactly what item-level [`$extensions`](#extensions) is for. - **Anything else genuinely absent** — a custom kind, or a profile, depending on whether you're naming a new kind of document or tightening an existing one. ## Choosing between the three - **Attaching data a specific tool needs, to something the spec already models?** `$extensions`. - **Documenting something the spec has no place for at all — and you're fine with no other tool understanding it?** A custom kind. - **Making optional fields mandatory for your own project, without leaving standard DSDS?** A profile. You can combine them. A custom kind can carry its own `$extensions`. A profile can require that an `$extensions` namespace be present, the same way it requires any other field. Pick the smallest tool that solves your actual problem — reaching for a custom kind just to make one field required gives up far more (the entire fixed structure of the kind you moved away from) than it gains. --- # Interoperability — DSDS 0.23.0 No open standard covers design-system documentation end to end. The formats that do exist each cover one layer well: DTCG owns token values, CEM owns a component's generated API, CSF owns a story, a test runner owns whether a rule actually holds. DSDS sits above them and documents **meaning and usage**. It is deliberately not a second copy of what those formats already own. If another format owns a fact, DSDS points at it rather than restating it. A pointer can go stale in exactly one way — the target moves — and `DSDS-11` catches that. A copy goes stale silently, every time the source changes. That is why a token entry has a `source` and no `value`, and why a component entry has `sourceFiles` and `specs` and no property table. ## The map Every integration below is a field that exists today, not a plan. `rel` values come from [`common/ref`](schema.html#common-ref); the whole list is open, so an unlisted format still has a home. {/* dsds:interop-map */} | Layer | Format it interoperates with | The DSDS field | |---|---|---| | Token values, types, aliases | [DTCG](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/) (W3C Design Tokens) | A token entry's `source`; a theme's `source` | | Component API contract, already generated | [Custom Elements Manifest](https://github.com/webcomponents/custom-elements-manifest) (CEM), or any standard contract document | A component's `specs` | | Component source, per platform | `.tsx`, `.vue`, `.swift`, framework typings — whatever a generator reads | A component's `sourceFiles` | | Stories and live demos | [Component Story Format](https://storybook.js.org/docs/api/csf) (CSF), Storybook, or an equivalent | A `refs`/`examples` entry with `rel: demo` | | Whether a guideline actually holds | A test or lint rule — vitest, axe-core, stylelint, ESLint | A guideline's `checks`, with `role: test`, `lint-rule` or `agent-test` | | Why a guideline exists | WCAG, ARIA APG, MDN, an internal RFC | A guideline's `evidence` | | Design artifacts | Figma or another design tool | A `refs` entry with `rel: design`, or `metadata.preview` | | Distribution | npm, or any package registry | A component's `imports[].package`, or `rel: source` | | Editor validation of the DSDS file itself | [JSON Schema](https://json-schema.org/) draft 2020-12 | The document's own `$schema` key | | Anything not listed | Any vendor or tool | `$extensions`, keyed by namespace | {/* /dsds:interop-map */} ## Tokens — DTCG A token entry documents what a token is *for*. Its value, type, and aliases stay in the DTCG file, and `source` points at them. ```yaml kind: token id: color.action.primary name: Primary action color description: The fill behind the single highest-priority action on a surface. tokenType: color source: ./color-action-primary.tokens.json ``` The `id` doubles as the path into the DTCG tree, which is why token ids allow the dot and slash separators other DSDS ids don't. When your DTCG paths don't line up with your ids, point `source`'s `href` at a JSON Pointer instead — `./tokens.dtcg.json#/color/action/primary` — ordinary URI syntax, no schema change. Nested groups, group-level `$type` inheritance, and alias references all work, because DSDS never parses the file: [`examples/interop/nested-color.*`](examples.html#interop) is a worked pair where the DTCG side uses `{color.palette.blue-500}` aliases and a Tokens Studio `$extensions` block, and the DSDS side just points at it. A theme is the same move one level up: `source` points at the DTCG file holding that theme's overrides, and DSDS records what the theme is for. ## Component APIs — CEM, source files, and contracts Two fields, one step apart in the same pipeline. A component can use either, both, or neither. - **`sourceFiles`** points at the real source a generator reads — one entry per platform. - **`specs`** points at the *already-generated* contract document, so a consumer doesn't have to re-derive it. ```yaml sourceFiles: - platform: react file: ./src/Button.tsx specs: - href: ./contracts/button.contract.json role: DS Contracts ``` A `sourceFiles` path starts from the document it's in. When the docs live apart from the code, such as in a separate repository, set `metadata.sourceRoot` on the system entry, and every relative `sourceFiles` path in the project starts there instead: ```yaml kind: system id: acme-ui name: Acme UI description: Acme's component library. metadata: sourceRoot: ../acme-ui # a path next to these docs, or a URL ``` DSDS does not parse or validate what `specs` points at, so **any** standard contract format works — CEM, [DS Contracts](https://github.com/southleft/ds-contracts-poc), a typedoc or react-docgen dump, something in-house. Name the format in `role` when the file extension doesn't make it obvious. CEM is a community project maintained at [webcomponents/custom-elements-manifest](https://github.com/webcomponents/custom-elements-manifest), not a W3C specification — worth stating because it sits next to DTCG on this page, and DTCG *is* a W3C Community Group report. Neither is a W3C Recommendation; both are widely enough adopted to point at. [`examples/interop/my-element.*`](examples.html#interop) is a full CEM pair: a real `custom-elements.json` alongside the DSDS entry that points at it, so you can see exactly which facts live on which side. ## Stories — CSF and Storybook A story is an example, and DSDS treats it as one. `rel: demo` covers a live demo of any kind — hosted Storybook docs, the CSF source they're built from, or an equivalent tool — and `role` and `note` say which is which: ```yaml refs: - href: https://storybook.org/ds/button rel: demo role: hosted docs - href: ./stories/button.stories.tsx rel: demo role: CSF story source note: Component Story Format (CSF3) - the source the hosted docs above are built from. ``` Point `examples[].ref` at a specific story when a guideline needs one, rather than pasting the code into the document. A CSF story is a named export, so name it with a fragment — `./stories/button.stories.tsx#Primary` — the same ordinary URI syntax the DTCG section above uses to point into a token tree, and the same reason it needs no schema change: ```yaml examples: - title: Loading state announces itself ref: href: ./stories/button.stories.tsx#Loading rel: demo ``` ## Tests and lint rules This is the integration that makes a guideline falsifiable rather than decorative. A guideline that claims automated verification MUST say what runs it — `DSDS-03` enforces exactly that. ```yaml - level: must statement: Every icon-only button MUST have an accessible name. checkedBy: automated checks: - href: ./tests/button.a11y.test.ts role: test note: vitest + axe-core ``` `checks` holds every way the rule gets checked, and needs no `rel`: the field says it. `role` says which kind of check each one is — `test` for the design system's own test suite, `lint-rule` for a static check (an ESLint or stylelint rule), `agent-test` for an eval that runs an AI agent against the guideline. A checklist step that verifies the rule goes here too, as a `to`. Nothing constrains the runner: DSDS names the check and points at the thing that proves it. ## Standards and evidence `evidence` points a guideline at the outside standard it comes from, so a reader can tell a house preference from a WCAG requirement: ```yaml - level: must statement: A focus indicator MUST remain visible for keyboard users. evidence: - href: https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html role: WCAG 2.2 SC 2.4.7 (AA) ``` A pointer in `evidence` needs no `rel` either: everything in it is something the rule cites. A link that's merely worth reading goes in `refs` with `rel: relates-to`. ## Design tools and packages ```yaml refs: - href: https://figma.com/file/…/Button rel: design - href: npm:@org/ds-react rel: source imports: - platform: react code: "import { Button } from '@acme/ui';" package: "@acme/ui" ``` Figma node ids, Tokens Studio metadata, and anything else tool-specific belong in `$extensions` under a vendor namespace — see [Extending the schema](extending.html). ## The DSDS file itself DSDS is JSON Schema draft 2020-12, so a DSDS document is a first-class citizen in any editor that resolves `$schema`: ```yaml $schema: https://designsystemdocspec.org/v0.23.0/dsds.bundled.yaml schemaVersion: "0.23.0" ``` The bundle ships as both YAML and JSON (`dsds.bundled.schema.json`) at that same versioned path, because a tool that only reads JSON schemas over HTTP is common enough to be worth serving directly. Documents themselves can be YAML or JSON. This site's own pages carry `schema.org` JSON-LD, and [`llms.txt`](/llms.txt) indexes every page for agents — that's interop for the *documentation*, separate from the spec. ## Anything not on this list `$extensions` is the escape hatch, namespaced so two tools can't collide: ```yaml $extensions: com.acme.designops: figmaNodeId: "1:234" ``` A conforming consumer MUST preserve `$extensions` it doesn't understand rather than dropping it, so data survives a round trip through a tool that has never heard of your namespace. It's available on the document, every entry, every section, and every section item. ## What DSDS won't do Worth stating plainly, because it's the boundary that keeps the format small: - **It won't restate a component's props, types, or defaults.** That's what `sourceFiles` and `specs` are for. Hand-typed API tables are the single most common source of documentation drift. - **It won't hold a token's value.** DTCG owns that. - **It won't parse or validate what a pointer points at.** DSDS checks that a relative target exists on disk (`DSDS-11`); reading inside it is the consuming tool's job. - **It won't invent a vocabulary another standard already has.** RFC 2119 for requirement levels, JSON Schema for structure, schema.org for page identity. Every worked pair lives in [`examples/interop/`](examples.html#interop), and each one is validated on every `npm run check` — so the examples on this page can't drift from what the schema actually accepts. --- # Design system doc spec 0.23.0 **Design system documentation is better when it's portable, structured, and connected. DSDS works to make all three possible.** **Draft Specification:** This is a draft. It can still change—and has done so. No standards body has endorsed it. We welcome feedback and contributions on GitHub. * **Latest version:** [GitHub repo](https://github.com/somerandomdude/design-system-documentation-schema) * **Feedback:** [GitHub Issues](https://github.com/somerandomdude/design-system-documentation-schema/issues) --- ## One entry, three readers Write a component's guidance once, in YAML. A docs site renders it for people, an agent reads it before writing code, and a linter checks it for gaps. Here's one entry: ```yaml kind: component id: button name: Button description: Triggers an action on the current screen. sections: - kind: guidelines for: all framing: when-to-use items: - level: should statement: Use for an action that happens in place, like saving a form. - level: should-not statement: Avoid for navigation. A link tells people they're going somewhere. - kind: guidelines for: human items: - level: must statement: Label the button with the action it performs, like "Save draft". - level: should statement: Keep to one primary button per screen. - kind: guidelines for: agent items: - level: must-not statement: Do not build a button from a div with a click handler. checkedBy: assisted ``` ### What a docs site shows The sections written for people (`for: human`) and everything (`for: all`): > **Button** > > Triggers an action on the current screen. > > **When to use** > > - **Should:** Use for an action that happens in place, like saving a form. > - **Should not:** Avoid for navigation. A link tells people they're going somewhere. > > **How to use** > > - **Must:** Label the button with the action it performs, like "Save draft". > - **Should:** Keep to one primary button per screen. ### What an agent reads Agents can be given their own specific documentation with a `for: agent` section to guide agents with things only they need: ```yaml - kind: guidelines for: agent items: - level: must-not statement: Do not build a button from a div with a click handler. checkedBy: assisted ``` ### What the linter reports `npm run lint` checks the entry and points at a gap: ``` button.yaml ⚠ [DSDS-14 guideline-missing-checkedby] /sections/1/items/0/checkedBy: guideline in "button" is a hard requirement (level: must) with no checkedBy, and no same-as target that declares one — declare 'automated', 'assisted', or 'manual' so a tool can tell whether this rule is verifiable at all. 1 file(s) linted: 0 clean, 1 warning(s). ``` The labeling rule is a `must`, but nothing says how anyone would check it. Add `checkedBy: manual` and the warning clears. The linter never fails a build. It points out what a reviewer would otherwise have to catch. --- ## Connected documentation A design system is a set of connected pieces. A dialog is built from a button, a button uses color tokens, and many components follow the same rules. DSDS lets things connect, so those links are data rather than copy/paste-waste. ```yaml kind: component id: dialog name: Dialog description: A window over the page that asks for a decision before the person can go on. sections: - kind: guidelines for: all items: - refs: - to: shared-a11y#focus-visible rel: same-as uses: - to: button - to: color.surface.raised alternatives: - to: popover refs: - href: https://www.figma.com/design/abc123/Dialog rel: design - href: https://storybook.example.com/?path=/story/dialog rel: demo ``` Because the links are data, a docs site can show what's related, an agent can check what a change would affect, and a rule shared by many components is written once and reused instead of copied. → **[common/ref](schema.html#common-ref)** lists every kind of link, what it means, and where it goes. --- ## Interoperability No open standard covers design-system documentation end to end. The formats that do exist each cover one layer well — [DTCG](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/) owns token values, [CEM](https://github.com/webcomponents/custom-elements-manifest) owns a component's generated API, [CSF](https://storybook.js.org/docs/api/csf) owns a story, a test runner owns whether a rule actually holds. DSDS sits above them and documents meaning and usage. **If another format owns a fact, DSDS points at it rather than restating it** — which is why a token entry has a `source` and no `value`, and a component entry has `sourceFiles` and `specs` and no property table. A pointer can go stale in exactly one way, and `DSDS-11` catches it; a copy goes stale silently. → **[Interoperability](interoperability.html)** covers every format DSDS is built to sit alongside, the field that points at each, and a worked example for all of them. --- ## For humans and agents A DSDS document has two readers: people and AI agents. Both are served by the same file. Every section carries a `for` field naming its audience: - **`for: human` or `for: all`**: Everything a person needs—definitions, guidelines, steps, freeform narrative. Agents read these too. - **`for: agent`**: Agent-only notes—for the times when basic documentation can't get through those thick titanium skulls. This allows you to write documentation specifically for agent misunderstandings, hallucinations, or agent-specific tasks. Write for people by default and use `for: agent` when all else fails. ## How a document is built A document is a [**base**](schema.html#base) — `schemaVersion`, `name`, and a list of [**entries**](schema.html#entries-entry): a [system](schema.html#entries-system), any number of [components](schema.html#entries-component), [tokens](schema.html#entries-token), [themes](schema.html#entries-theme), or the generic `entry` for anything else. The same file feeds your docs site, gives your agents context, and shows up everywhere your design system does. Design systems have different documentation needs. This schema can be as simple or as detailed as needed. Every entry's structured docs live in one `sections` array, and each section has a `kind`. The spec defines three kinds — `guidelines`, `definitions`, `steps`, plus the generic `section`. Any section can also carry `freeform`: headed, nestable prose alongside its own `items`. A component also carries `sourceFiles`, `specs`, `traits`, `combos`, and `imports` as fields of its own, not sections. Any entry kind can use any section kind — nothing restricts which goes with which. --- ## Principles 1. **Documentation only.** This schema is focused on the how, when, and why of a design system. That's it. If a better source of truth exists elsewhere, the schema links rather than restating. 2. **A consistent schema pattern.** Each part of the schema follows the same structure, so writing one part teaches you the rest. 3. **No learning cliffs.** It's designed to have a low barrier of entry. But it can grow with you. DSDS' `$extensions` let you add on to the schema when you actually need it. 4. **Everything can link to everything else.** A design system is a network of connections; the documentation reflects that. 5. **There's always a way out.** The schema has opinions, but always a way to step outside them. --- ## Next steps New to DSDS? Start with the [Quick Start Guide](quickstart.html) — document structure, entry kinds, the section system, and examples you can copy. For the full schema reference, see [Schema](schema.html). For what it means to conform to the spec and every rule the validator enforces, see [Conformance](conformance.html). For the JSON Schema source, see `schema/`; for working examples, see [Examples](examples.html). --- ## Contributors - [PJ Onori](https://pjonori.com): Current maintainer. - [Cody Clark](https://github.com/codysue): the CEM interop example, the nested/aliased DTCG interop example, and the two token-description lint rules (`DSDS-13` and `DSDS-16`). - [Suleiman Ali Shakir](https://iamsuleiman.com/): the README validate command and lockfile, and a stale version string fix. - [Mykhaylo Ryechkin](https://github.com/mryechkin): the agent skills (`.agents/skills/`) and `scripts/generate/sync-skill-versions.js`, and keeping them current with the schema. - [Mark Toadvine](https://github.com/marktoadvine): the `shared-a11y` WCAG 2.2 accessibility guidelines in the starter kit. - [Chris Strahl](https://github.com/chrisstrahl): real API identifier shapes on a trait `id` (`apiId`), the widened `combo` target, and the `DSDS-14` same-as false positive fix. - [isabelthedesigner](https://www.isabelthedesigner.com): the anti-pattern for bare WCAG conformance claims. - [Afyia Smith](https://afyiasmith.co/): the `owner`/`reviewed` and `origin` metadata schemas. --- # Processing model — DSDS 0.23.0 The schema says what a document can contain. This page says what a tool works out when it reads one. For example: which item a `#` address points to, where a reused rule gets its text, and what replaces something deprecated. This page sets requirements. MUST, MUST NOT, SHOULD, SHOULD NOT and MAY mean what [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) says, and only when written in capitals. The requirements apply to a **conforming consumer**: anything that reads a DSDS document and does something with it. [Conformance](conformance.html) covers the other kinds of tool. Each MUST ends with what checks it, in parentheses, as the [specification's §1.2](specification.html#12-what-enforces-each-requirement) describes. That's a validator rule, a test vector, or `consumer` when no check on a document can see it. `npm run check` fails if a MUST names nothing. `scripts/resolve/resolve.js` is the reference implementation. The [test vectors](#11-test-vectors) hold the answers it gives, and every implementation has to give the same ones. ## 1. Terms - A **project** is all the documents a consumer reads together (§3). - An **address** is the part of a ref's `to` after the `#`. In `shared-a11y#focus-visible`, the address is `focus-visible`. - An **item** is one member of a section's `items`, or one `freeform` entry. - A **chain** is a path a consumer follows from one thing to the next, through `uses`, `extends`, `same-as` or `replaced-by` (§9). - An **inline link** is a link inside a Markdown field whose URL starts with `dsds:`, like `[focus visible](dsds:shared-a11y#focus-visible)` (§2.4). ## 2. Addresses ### 2.1 Entries A consumer MUST treat `entries` and `shared` as one set of ids, so no two entries in a project can share an id (DSDS-04). ### 2.2 Items A `to` written as `entryId#address` points at one thing inside an entry. A consumer MUST work out each thing's address with these rules (vector: addresses): 1. **An item with an `id`** uses that `id`. 2. **An item without an `id`** uses an id made from its own text: a guideline's `statement`, a definition's `term`, or a step's or freeform entry's `title`. To make it, lowercase the text, turn each run of characters other than `a`–`z` and `0`–`9` into one dash, and remove any dash at either end. "Name it!" becomes `name-it`. This works for freeform entries at any depth. 3. **An item with neither**, because it only reuses another item with `same-as` (§4), uses the address of the item it reuses. That's the part of its `same-as` target after the `#`. 4. **Anything else inside an item that has its own `id`**, such as an example, uses that `id`. 5. **A component's trait** uses its `id`. **One value of an enum trait** uses `traitId.valueId`, as in `button#variant.primary`. Combos use the same form. It can't be misread, because a trait id can't contain a dot. ### 2.3 When two things share an address Two things on one entry can end up with the same address. A consumer MUST pick the one whose address has the strongest source, in this order (vector: addresses; DSDS-05): 1. an `id` written in the document (rules 1, 4 and 5); 2. an id made from the text (rule 2); 3. an address taken from a reused item (rule 3). If two things tie for the strongest source, the address is ambiguous. A consumer MUST NOT choose between them, because it's an error (DSDS-05). The fix is to give one of the two its own `id`. So when an item and another item reusing it sit on the same entry, their shared address points at the item that states the rule. ### 2.4 Inline links An inline link names its target the same way a `to` does, so it's found with the same rules. - A consumer MUST find inline links with a CommonMark parser, not by searching the text (vector: inline-links). A `dsds:` URL in a code span or a fenced code block isn't a link. A reference-style link (`[text][x]`, with `[x]: dsds:button` elsewhere in the field) is. - A consumer MUST treat the scheme as case-insensitive, so `DSDS:button` is an inline link too (vector: inline-links). The target is everything after the colon, after the parser has decoded escapes such as `%23` for `#`. - A consumer MUST resolve the target with the rules in §2.1 to §2.3, and report it the way §3 reports a `to` that can't be found (DSDS-08, DSDS-05; vector: inline-links). A target that isn't shaped like a `to` is a problem too. - For `[text](dsds:target)`, the text to show is the author's. For ``, a consumer MUST show the first of these the target has (vector: inline-links): 1. an entry's `name`; 2. a definition's `term`; 3. a step's or freeform entry's `title`; 4. a trait's or trait value's `name`; 5. otherwise, the target itself, without `dsds:`. A reusing item (§4) uses its effective content, so a reused definition shows the `term` of the item it reuses. A guideline has no short name, so an autolink to one shows its target. Write `[text](dsds:…)` for a guideline instead. - An inline link isn't a relationship. A consumer MUST NOT count it in §5: it isn't a `uses`, and it doesn't appear when relationships are read backwards (vector: inline-links). A consumer MAY offer a separate "mentioned in" list built from inline links. ## 3. Projects A consumer looks up refs across a whole project. - A project MUST include every document a consumer can reach by following `rel: file` refs from the documents it was given, then from those, and so on (DSDS-08; vector: cross-file). - A consumer MUST NOT follow a `rel: file` ref out of the folder that holds the document, or its subfolders (consumer). A document is data from whoever wrote it, and could point anywhere. See [Security](security.html). - `rel: file` is only for DSDS documents. A code file or a DTCG token file is `rel: source`, and a consumer doesn't read it as part of the project (`DSDS-25`). - When the project's system entry sets `metadata.sourceRoot`, a consumer MUST read each relative `sourceFiles` path starting from there (`DSDS-11`). Without it, the path starts from the component's own document. A relative `sourceRoot` starts from the document that holds the system entry. If `sourceRoot` points outside that document's folder, the consumer still decides whether to open it. - A consumer given several documents at once SHOULD treat them as one project. That's how a standalone entry file can point at the files next to it, since it has no field to list them. - A ref to something not in the project is an error when the project is known to be complete: a base document with no `rel: file` refs, read on its own. Otherwise the target may be in a file the consumer didn't read, so it's a warning (`DSDS-05`, `DSDS-08`). ## 4. Reused content An item with a `same-as` ref reuses the content of the item it points at. - A consumer MUST show a reusing item with its **effective content**: each content field the item states itself, plus every other content field from the item it reuses (vector: reuse). The content fields are: - for a guideline: `level`, `statement` and `checkedBy`; - for a definition: `term`, `definition` and `usage`; - for a step: `title` and `description`. - If the reused item itself reuses another, a consumer MUST follow the chain to the item that states the content (vector: reuse). It SHOULD say which item that is. - A reusing item MUST point at an item of the same section kind (`DSDS-25`). A guideline can't reuse a definition. - When both items state a `level`, the two MUST match (`DSDS-10`). An item that means to state a different level is a `refines`, not a `same-as`. - When the target of a `same-as` ref can't be found, a consumer SHOULD show the reusing item's `level` and the link, rather than nothing (see [sections/guidelines](schema.html#sections-guidelines)). `refines` works differently. A `for: agent` item uses `refines` to point at the human rule it states more precisely for an agent. A consumer reading for an agent SHOULD apply both rules. The refinement doesn't replace the rule it refines. ## 5. Relationships and their reverse A relationship is written once, on the thing it starts from. A conforming consumer works out the reverse direction itself, and MUST NOT require it to be written on the other end too (vector: inverse). A pointer in `refs` says what kind it is with `rel`. A pointer in a field named for one kind of pointer has no `rel`, and a consumer MUST read it as the kind that field stands for (vector: inverse). When an object has a field for a kind of pointer, that kind MUST go in the field, not in `refs` (`DSDS-25`). Eight of the fourteen kinds have their own field, and `refs` holds the other six. `source` also has fields that each hold one: a token's or theme's `source`, and the `file` in each of a component's `sourceFiles`. Every `rel`, where it's written, and what a consumer does with it: | `rel` | Where it's written | What it means | Read backwards | What a consumer does | |---|---|---|---|---| | `uses` | `uses`, on an entry or a section item | This is built from the target, needs it, or follows it. A dialog uses its button. A button uses a color token, and the pattern it follows. | used by | Follows `uses` backwards to answer "what is affected if this changes?" Follows it as a chain (§9). | | `same-as` | `refs`, on an item | This item reuses another item's content instead of repeating it. | reused by | Shows the reusing item with its effective content (§4). | | `replaced-by` | `replacedBy`, in the deprecated `status` of an entry, trait or trait value | The target replaces this. | replaces | Follows it to the successor (§7). | | `file` | `refs` | Another DSDS document in the same project. A code or token file is `source` instead. | — | Reads the target as part of the project (§3). | | `source` | `refs`; a token's or theme's `source`; a component's `sourceFiles[].file` | Where this is built: a repository, a source file, a DTCG token file or a package. | — | MAY fetch it. Reads a relative `sourceFiles` path from the system's `sourceRoot` (§3). | | `design` | `refs` | The design file this comes from. | — | MAY link to it. | | `demo` | `refs`, or an example's `ref` | A live demo, such as a Storybook story. | — | MAY link to it or embed it. | | `relates-to` | `refs` | Worth reading alongside this, when no other kind fits. On a guideline item with no `statement` and an `href`, it's where the rule is tracked. | the same both ways | MAY show it as "see also". For an item with no statement, SHOULD show the level and the link (§4). | | `extends` | an entry's `extends` | This inherits from the target. | extended by | Never merges the target in (§6). Follows it as a chain (§9). | | `alternative-to` | `alternatives`, on an entry or a guideline item | Use the target instead, in the situation the entry or rule describes. | the same both ways | Offers the target when this one doesn't fit. | | `cites` | a guideline's `evidence` | The outside standard the rule is based on. | cited by | SHOULD show it, so a reader can tell a requirement from a house preference. | | `checked-by` | a guideline's `checks` | What checks the rule: a test, lint rule or agent eval (an `href`, with `role` saying which), or a checklist step (a `to`). | checks | Runs it or links to it. `checkedBy: automated` needs one with an `href` (`DSDS-03`). | | `contract` | a component's `specs` | A machine-readable API contract that was already generated. | — | Reads the component's props, slots and events from it, with whatever parser its format needs. | | `refines` | a guideline's `refines`, on a `for: agent` item | This states a human rule more precisely, for an agent. | refined by | When reading for an agent, applies both rules (§4). | A "—" means the pointer leads outside the project, so nothing in the project reads it backwards. Custom namespaced `rel` values are covered in §8. Only pointers are relationships. An inline link in prose (§2.4) isn't one. When a consumer shows an entry's relationships, it SHOULD include the ones written on other entries that point at it. A pointer inside an item belongs to that item. So a step that `uses` `button` is a relationship from the step, not from its whole entry. ## 6. `extends` `extends` says one entry inherits from another: a dark theme from a light one, or an icon button from a button. A consumer MUST NOT copy the extended entry's fields or sections into the entry that extends it (consumer). The extending entry is complete as written. A consumer that reads it on its own reads all of it. A consumer MAY show the extended entry's content beside it, as long as it's clearly labelled as coming from that entry. Why not merge them? An entry's meaning would then depend on another document the consumer may not have. And every consumer would need the same rules for merging lists, fields and overrides, or they'd disagree. What a theme changes lives in its DTCG `source`, which has its own rules for that. ## 7. Successors A successor is what replaces something deprecated. `replaced-by` points from the deprecated thing to its successor. - The successor is named in `replacedBy`. It sits in the `status` that marks the entry, trait or trait value as deprecated, next to its `deprecationNotice`, where a reader learns the thing is going away. The schema only accepts `replacedBy` in a deprecated status. In one platform's status, it names that platform's successor. - To find what to use instead, a consumer MUST follow `replaced-by` (vector: successors). If the successor is deprecated too, with its own `replaced-by`, a consumer MUST follow that as well and use where it ends (vector: successors). - When there's more than one successor, a consumer MUST NOT pick one on its own (vector: successors). The `deprecationNotice` says when to choose which. A consumer that can't apply it MUST leave the choice to whoever it's working for (consumer). For example, an agent replacing `variant="secondary-green"` across a codebase asks, or leaves a note, instead of guessing. ## 8. Unknown values - When a consumer writes a document back out, it MUST keep any `$extensions` content it doesn't understand (consumer). - A consumer MAY ignore a custom namespaced `rel` (such as `acme.themes`) or a custom kind it doesn't understand. It MUST NOT report the document as invalid because of one (consumer). - A field the schema doesn't declare, on a generic `entry` or `section`, is most likely a typo or a leftover. A validator reports it as a warning (`DSDS-24`). ## 9. Chains and loops A consumer follows four kinds of pointer from one thing to the next. Each one answers a question: - `uses`: what is affected if this changes? - `extends`: what does this inherit from? - `same-as`: where is this content stated? - `replaced-by`: what replaces this? A chain MUST NOT lead back to where it started, because then its question has no answer (DSDS-06; vector: cycles). A loop is an error, and a consumer that finds one MUST stop following it instead of going round forever (vector: cycles). `uses` and `extends` link entries. `same-as` and `replaced-by` link items, traits, trait values and entries. Each kind is checked on its own: a `uses` ref and an `extends` ref pointing at each other don't make a loop. ## 10. Problems What `scripts/resolve/resolve.js` reports, and the rule that catches each one in a document: | Problem | What it means | Rule | |---|---|---| | `duplicate-entry` | Two entries in the project have the same id. | `DSDS-04` | | `unresolved-entry` | A ref points at an entry the project doesn't have. | `DSDS-08`, `DSDS-05` | | `unresolved-item` | A ref points at an address the entry doesn't have. | `DSDS-05` | | `ambiguous-address` | An address points at two things, and neither one wins (§2.3). | `DSDS-05` | | `level-mismatch` | A reusing item states a different `level` from the item it reuses. | `DSDS-10` | | `same-as-kind` | A `same-as` ref points at a different kind of item. | `DSDS-25` | | `replaced-by-misplaced` | A `replaced-by` pointer is on something that can't be deprecated. | the schema, `DSDS-25` | | `replaced-by-not-deprecated` | A `replaced-by` pointer is on something that isn't deprecated. | the schema | | `cycle` | A chain leads back to where it started. | `DSDS-06` | | `malformed-link` | An inline link's target isn't shaped like a `to`. | `DSDS-08` | | `inline-links-skipped` | A Markdown field mentions `dsds:`, but the consumer had no Markdown parser to read its links. | — | `unresolved-entry`, `unresolved-item` and `ambiguous-address` cover inline links too. For a problem in an inline link, `at` is the item or entry whose field holds the link. ## 11. Test vectors Each folder in [`examples/vectors/`](/examples/vectors/) holds one or more documents and an `expected.json`: what a conforming consumer should work out from them. They cover addresses (§2), inline links (§2.4), a project split across files (§3), reused content (§4), reverse relationships (§5), successors (§7) and loops (§9). They're published with the conformance suite at [`/v0.23.0/conformance-suite/manifest.json`](/v0.23.0/conformance-suite/manifest.json), next to the broken examples that validators are tested against. The manifest's `consumerContract` explains how to compare results. In short, everything has to match except each problem's `message`, which is text for people to read. The result has `inlineLinks` only when the project has inline links. Each one says where it is (`from`, `pointer`, `offset`), its `form` and `target`, what it `resolves` to (missing when it doesn't resolve), and its `text`: the author's for `[text](dsds:…)`, the §2.4 text for ``, and none for a reference definition, whose text is on the link that uses it. In this repo, `node scripts/resolve/resolve.js ` prints what any project resolves to, and `npm run check` checks every vector. --- # DSDS quick start guide ## What is DSDS? DSDS (Design System Doc Spec) is a YAML format for documenting design systems. It helps document the how, when and why of a design system in a machine-readable format. This quick start guide is here to help you write your first DSDS document. If a dummy like PJ can write one, you can as well. The schema itself is written in [JSON Schema](https://json-schema.org/), but the docs are written in YAML. All the validators that help you catch errors (like `npm run check`) require that format. ### What you get - **Structured data:** Every section has a defined structure to reduce ambiguity - **Machine-readable:** Tools can parse, generate, validate, and transform it - **Portable:** Say buh-bye to vendor lock-in - **Extensible:** Add your own content and metadata without breaking compatibility - **Validatable:** The schema catches errors before they reach consumers @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@ @@ @@ @@ @@ @@@@@@@@@@@ @@@@@@@@@@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@@@@@@@@@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@@@@@@@@@@ @@@@@@@@@@ @@ @@ @@ @@ @@@@@@@@@@@ @@@@@@@@@@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@@@@@@@@@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@@@@@@@@@@ @@@@@@@@@@ @@ @@ @@ @@ @@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ## Document structure A DSDS document starts with a **base**: `schemaVersion`, a `name`, and a list of **entries**. ### The bare minimum This is valid YAML, but it's not a valid DSDS document yet. `entries` still need to be added: ### Adding a system entry DSDS uses entries to describe pieces of the system—even the system itself. So the best entry to add first is a system. This entry allows you to document all system-level information (version, organization, url, license, platforms). It also gives you the freedom to document multiple systems in one document (if that's your kind of thing): ### Adding more entries Once your system is documented, it's time to add other stuff—like a button. DSDS has a specific entry for components (`kind: component`—duh). Entries have no hierarchy, so your button sits at the same level as a system entry. Entries need _very little_ information to be valid. This'll give you the freedom to add placeholder content to fill out later. This is what your document would look like once a system and component are added. Every entry kind shares one open base — `id`, `kind`, `name`, `description`, plus a handful of optional fields — defined at [entries/entry](schema.html#entries-entry). `kind` then picks a specialization: [`component`](schema.html#entries-component), [`token`](schema.html#entries-token), [`theme`](schema.html#entries-theme), [`system`](schema.html#entries-system), the open `entry` kind for anything else, or a custom kind of your own (like `acme.icon-library`). See [How the schema is organized](schema.html#how-the-schema-is-organized) for the full picture. ### Splitting across files with `refs` It's cool to add all your content into one DSDS YAML file. But that's going to get big really fast. Most of you will want to break content up by entry—and the good news is that's easy. DSDS has a `refs` property which allows you to point to other files. This lets the base DSDS document be a list of links to all of your DSDS YAML files. It also gives you the freedom to organize your documents however you want. ### Metadata `metadata` holds optional information about an entry—things like who owns it, when it last changed, how it's tagged, its status, and more. All metadata is optional, so add as little or as much as is needed. A few of the more common ones: * `owner`: An RFC 5322 mailbox for the responsible team * `tags`: Keywords for search and grouping. The schema treats the first tag as a category * `since`: The version this entry was introduced * `group`: A way to tie related entries together, like `color.action` on a family of tokens. * `status`: Defines where an entry is in the system lifecycle (ex: beta, available, deprecated). See [common/metadata](schema.html#metadata-metadata) for the complete field list. metadata: status: {status: stable} tags: [action, CTA, interactive] owner: pj@pizzapartysupertime.com ## The section system Structured docs live in the `sections` array on each entry. Each section has a `kind` field naming its type — `guidelines`, `definitions`, `steps`, or the generic `section`. Any entry kind can use any section kind; nothing restricts which section kinds go with which entry kind. Every section also carries a `for` field (`human`, `agent`, or `all`) naming its audience — see [For humans and agents on the Overview page](index.html#for-humans-and-agents). ### Guidelines explain when and how to use an entry `guidelines` take two different shapes based on the `framing` property: * `when-to-use`: Describes when and when not to use the entry * `how-to-use` (default): Explains how to use the entry correctly A fully-defined guideline acts as both guidance and a reference to test against. But it can also be as simple as a `statement` with a `level`. That's all you need to make a guideline. You can optionally add `alternatives` to point towards a better option for use cases, `evidence` to back up a statement, or a `checkedBy`/`checks` verification pair for testing purposes. The `level` field's values are lowercase kebab-case, like every DSDS vocabulary: `must`, `should`, `may`, `should-not`, `must-not`. Agents treat `must`/`must-not` items as hard limits. ### Definitions help you define terms or things in the system A `definitions` section pairs a `term` with a `definition`. And that structure can be used for all sorts of things (component anatomy, naming conventions, a basic list of component API options, etc.). It's a super flexible section that can explicitly describe system concepts. sections: - kind: definitions for: all title: Anatomy context: anatomy items: - term: Container definition: The interactive root element. Receives background, border, radius, and padding. - term: Label definition: The visible text of the button. ### Steps let you break down a process, guide, or checklist A `steps` section is an ordered procedure or an unordered checklist (`ordered: false`). When a step verifies a guideline, the guideline lists the step in its own `checks`, next to any test or lint rule that checks it. Everything that checks a rule is then in one place. A self-check checklist, and the rule it checks: sections: - kind: guidelines for: all items: - id: aria-label-required level: must statement: Give an icon-only button an aria-label. checkedBy: manual checks: - to: button#icon-only-buttons-have-an-aria-label - kind: steps for: agent ordered: false title: Self-check before shipping items: - title: Icon-only buttons have an aria-label ### Freeform lets you do your own thing Every section can add a `freeform` property which allows you to add nestable unstructured writing. It's DSDS' way to allow you to add extra color to any section in your docs: sections: - kind: section for: all title: Overview freeform: - title: About body: Button is the primary interactive primitive in this design system. ## A component's own top-level fields A component is an opinionated kind of entry. And with that, it comes with specific properties to document a component. Here are the special properties it contains: - **`sourceFiles`**: Points to the component's source file. This pointer can be used for tools to extract the component's API. One entry per platform. - **`imports`**: A code sample showing how to import the component. One entry per platform. - **`traits`**: Describes every way the component can vary. A trait can be a `traitType`: `variant` (like `size: sm | md | lg`), or `state` (like `hover` or `loading`). - **`combos`**: Helps describe acceptable and unacceptable pairing between traits, tokens, or entries (ex: "loading and disabled must not both be set"). Here's `sourceFiles`, `traits`, and `combos` together: sourceFiles: - platform: react file: ./src/Button.tsx traits: - id: tone traitType: variant kind: enum name: Tone values: - id: default description: Neutral. General-purpose actions. - id: critical description: Destructive or irreversible actions only. - id: loading traitType: state kind: boolean name: Loading description: An async operation triggered by the button is in progress. - id: hover traitType: state kind: boolean name: Hover description: Background darkens slightly when the pointer is over the button. combos: - subject: loading level: should-not items: [disabled] note: Loading already makes the button non-interactive. ## Do your own thang ### `$extensions` `$extensions` allow you to structure your own data at the document, entry, or section level. Keys MUST use a namespace (e.g. `com.figma`) so a tool integration never collides with a future field the spec adds. Here, linking a component to its Figma source: ### Custom kinds When the generic `entry` kind isn't specific enough, use a custom kind instead. It's checked against the same open `entry.schema.yaml` base the generic kind is: See [Extending the schema](extending.html) for a third option too — profiles, for making an existing kind's optional fields required on your own project — and for more on when to reach for each of the three. ## Minimal examples These are close to the smallest valid entry for each kind. Copy one, fill in your content, and add `sections` as your docs grow. ### Component A minimal standalone component entry: ### Token A token needs `id`, `kind`, `name`, `description`, and (usually) `tokenType`. Use `source` to point back at the DTCG file that holds the real value: A described token adds `metadata.group` (the recommended way to group related tokens — there's no separate token-group kind) and a guideline: ### A pattern, using the generic entry kind A pattern, documented as a generic entry: ### Sharing content A lot of design system documentation covers the same topics—and you shouldn't have to rewrite it. Declare a rule, term, or step once and then point to it everywhere it applies with `entryId#itemId` and `rel: same-as`. The borrowing item can act as a pure pointer with no duplicated `statement` or `level`. The target can be an item in the base document's `shared` array, or an item in any entry. Use `shared` when the content doesn't fit in any single entry: The button's accessibility section uses `tags: [accessibility]` to identify its topic for conforming consumers, while its `same-as` pointer supplies the shared rule. Keep unrelated guidance in a separate section. The tag identifies documentation; it does not mean the component has passed an accessibility audit. Use `checks` and `checkedBy` on the rule to describe how it is verified. ### Linking from prose When a description or a rule mentions another part of the system, link to it with a `dsds:` link instead of writing its id in backticks. The part after `dsds:` is written exactly like a `to`. The validator checks it, so a mention of something that doesn't exist gets caught. - `` shows the target's own name, "Order Confirmation", and stays right if it's renamed. - `[names what's being ordered](dsds:order-confirmation#name-the-order)` shows your own words. Use this form for a guideline, which has no short name. A link only mentions something. If the entry depends on what it mentions, say so with `uses` as well, as the button does here. Tools read pointers, not prose, to work out what a change affects. ## Validate your document ### Using the bundled schema Add `$schema` to get editor autocompletion and inline validation: $schema: https://designsystemdocspec.org/v0.23.0/dsds.bundled.yaml kind: component id: my-component name: My Component description: What this component is and does. ### Using the CLI # Clone the repo git clone https://github.com/somerandomdude/design-system-documentation-schema.git cd design-system-documentation-schema npm install # Validate the built-in examples and test corpus npm run check # Validate your own file node scripts/validate/validate.js my-system.dsds.yaml ## Next steps You've seen the basics. Here's where to go deeper. | Resource | Description | |----------|-------------| | [Full Spec](schema.html) | Complete schema reference for every field and constraint | | [Schema files](https://github.com/somerandomdude/design-system-documentation-schema/tree/main/schema) | The raw `.schema.yaml` files — use for editor autocompletion | | [Example files](https://github.com/somerandomdude/design-system-documentation-schema/tree/main/examples) | Complete, valid example documents for every entry and section kind | | [GitHub Discussions](https://github.com/somerandomdude/design-system-documentation-schema/discussions) | Ask questions, share ideas, propose changes | 1. Copy the [minimal component example](#component) above 2. Replace it with your own design system's first component 3. Add `sections` as your docs grow 4. Validate with `npm run check` to catch schema errors early --- # Security considerations — DSDS 0.23.0 DSDS lets a document talk to an agent directly, in directive language. A `for: agent` section can carry what [AGENTS.md](AGENTS.md) calls "hard MUST/MUST NOT rules." And [Conformance](conformance.html) says a conforming consumer must treat a `must-not` guideline as "a hard stop for an agent writing code, not a suggestion." Nothing in that mechanism says who has to have written the document. This page says who. ## A DSDS document is only data A DSDS document is content to be read, like any other file an agent opens. That includes one describing a design system your organization didn't build and doesn't control. It is never a command channel. Nothing in this spec ranks a `for: agent` section, a `must`/`must-not` guideline, or a `steps` checklist above the instructions the operator gave the agent for the task at hand. This matters because the language is built to *sound* authoritative. That is the feature, not a flaw: a rule that reads like a suggestion won't stop an agent from working around it. But phrasing doesn't move authority from the author to the document. The author is a third party you may or may not trust. ## The `for: agent` rule applies only to documents you trust [Conformance](conformance.html#conforming-consumer) tells a consumer to take MUST/SHOULD guidance as seriously as the spec means it. That says how to read a document you have already decided to rely on. A linter obeys the config file you point it at, in the same way. It does not mean every DSDS document on the internet gets to constrain your agent. Trust is a separate decision, and it comes first. You already make it for any dependency or config file. Make it here too, whether the docs came from a vendor, a dependency, or a URL some user supplied. In a document you haven't vetted, a `must-not` guideline is evidence of how its authors intend the system to be used. Weigh it. It does not carry the force of your own security policy. ## Following a pointer is your decision [AGENTS.md](AGENTS.md#self-checking-your-work) tells an agent to follow a guideline's `checks` ref "to the real test." A `refs` entry can point anywhere, using `href`. DSDS never requires you to fetch any of them. No conformance rule says a validator or agent MUST retrieve a `checks` or `href` target. The rules only say that a reference, once resolved, has to resolve correctly (`DSDS-05`, `DSDS-08`, `DSDS-09`). So treat these pointers like links in any other untrusted input. A relative path inside your own repo is not the same risk as an outside URL. And "the document told me to fetch it" is not permission to fetch it. ## Inline links stay inside the project A Markdown field can mention another entry with a `dsds:` link, like `` ([Specification §6.5](specification.html#65-inline-links)). The target is always something in the project. A consumer looks it up there and never fetches it over the network. When a consumer turns a document into HTML, it must not output a `dsds:` URL as an `href`. A browser doesn't know the scheme, and an unknown scheme in a link is a common way to sneak behaviour past a sanitizer. Link to the published page for the target instead, or show the link's text with no link. Content an agent loads by following an inline link is data, like content it reaches through `refs`. A mention in a trusted document doesn't make the mentioned content more trustworthy than the document that holds it. ## `$extensions` is opaque, never executable A conforming consumer MUST keep `$extensions` data it doesn't understand ([Conformance](conformance.html#conforming-consumer)). That way the data survives tools that don't know its namespace. Keeping data is not trusting it. Validation only proves a document is well formed. It says nothing about whether the contents are safe to run, or to paste into a shell command, a prompt, or a query. Treat every `$extensions` value as untrusted data, whatever namespace it claims. ## In practice If you're building an agent harness, validator, or renderer: - Check `schemaVersion` and structure. Treat the content as read-only data for the task, never as instructions that rank with the operator's own. - Apply the obedience rule ([Conformance](conformance.html#conforming-consumer)'s MUST bullet) only to sources you have already decided to trust. - Apply your own network and tool-use policy before fetching an `href` or `checks` target. DSDS never requires it. - Never `eval`, run, or paste unchecked `$extensions` content into a privileged context. None of this needs new machinery. It is the handling untrusted input already gets. It is written down because a format that invites an agent to read hard directives should say plainly that those directives don't outrank the operator. Most such formats never say it. --- # Specification — DSDS 0.23.0 This page defines DSDS 0.23.0. The [schema](schema.html) gives the exact shape of every field. This page says what those shapes mean together, and what a document, a validator and a consumer have to do. If the two ever seem to disagree, the schema is right and this page has a bug. ## 1. Conventions ### 1.1 Requirement words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY mean what [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) says, and only when written in capitals. ### 1.2 What enforces each requirement Every MUST and MUST NOT on this page ends with what checks it, in parentheses: - **`(schema: entries/entry)`**: the JSON Schema file `schema/entries/entry.schema.yaml`. Any JSON Schema validator checks it. - **`(DSDS-04)`**: a rule in the [rule catalog](conformance.html#rule-details). A conforming validator checks it, on top of the schema. - **`(vector: addresses)`**: a [consumer test vector](processing.html#11-test-vectors) whose expected answer depends on it. - **`(suite)`**: the [conformance suite](conformance.html#conformance-suite), which tests a validator. - **`(consumer)`**: a requirement on what a consumer does with a document. No check on the document itself can see it. `npm run check` fails if a requirement on this page or in the [processing model](processing.html) has nothing in parentheses. It also fails if one names a schema file, rule or vector that doesn't exist, or if a validator rule isn't named anywhere. ### 1.3 Terms This page uses the words in the [terms table](conformance.html#the-words-this-spec-uses): - a **document** is one file; - an **entry** is one documented thing; - a **section** is one member of an entry's `sections`; - an **item** is one member of a section's `items`; - a **field** is a named slot on an object. ### 1.4 Conformance classes The [Conformance page](conformance.html#conformance-classes) defines four kinds of conforming thing: document, producer, consumer and validator. A **conforming document** meets every requirement in §2 to §7. A **conforming validator** meets §8. A **conforming consumer** meets §9 and the [processing model](processing.html). ## 2. Documents ### 2.1 Format A document is one YAML 1.2 or JSON file, and its top level MUST be a single mapping (schema: base, entries/entry). By convention, YAML files end in `.dsds.yaml` and JSON files in `.dsds.json`. ### 2.2 Base documents A document with a `schemaVersion` field is a base document. It MUST have `schemaVersion`, `name` and an `entries` list that isn't empty (schema: base). It MUST NOT have any field that `base.schema.yaml` doesn't declare (schema: base). `schemaVersion` SHOULD name the DSDS version the document was written for. A base document's `shared` list holds content that no single entry owns, such as an accessibility rule many components follow. A shared entry MUST have an `id`, a `name` and a `description` (schema: shared). Entries and shared entries draw ids from the same pool, so one id MUST NOT name two of them in a document (DSDS-04). ### 2.3 Entry documents A document with no `schemaVersion` is a single entry, written at the top level (schema: entries/entry). §4 applies to it in the same way as to an entry in `entries`. ### 2.4 Projects A large system is usually split across several files. A base document lists the others in `refs`, with `rel: file`. A `rel: file` ref MUST point at another DSDS document (DSDS-25). A code file or a DTCG token file is `rel: source` instead. A relative path MUST point at a file that exists (DSDS-11). That applies to the path in a `rel: file` or `rel: source` ref, in a component's `sourceFiles`, and in a token's or theme's `source`. The code a system describes often lives somewhere other than its documents, such as in a separate repository. A system entry can say where with `metadata.sourceRoot`: a path relative to its own document, or a URL. When a project's system sets it, every relative `sourceFiles` path in the project MUST be read starting from there (DSDS-11). The [processing model, §3](processing.html#3-projects) says how a consumer gathers a project's files. ### 2.5 Markdown fields A field whose schema is [common/markdown](schema.html#common-markdown) holds CommonMark. GFM tables and fenced code blocks are allowed too. These fields are: - an entry's and a shared entry's `description`; - a section's `description`, and a freeform entry's `body`; - a guideline's `statement`, a definition's `definition` and `usage`, and a step's `description`; - a trait's and a trait value's `description`; - an example's `description`. Other text fields, such as `name`, `title`, `term` and `deprecationNotice`, are plain text. A Markdown field can link to something in the project (§6.5). ## 3. Identifiers ### 3.1 Entry ids An entry's `id` MUST be lowercase words joined by dashes, with dots allowed between parts, like `button` or `color.action.primary` (schema: entries/entry, common/id). A token's `id` MAY also use a design tool's own capitals and slashes, like `Color/Action/Primary` (schema: entries/token). A lowercase UUID fits this shape, so an entry's or an item's `id` MAY be one, like `3f9a1c2e-8b4d-4e2a-9c71-5d0e6f8a2b13`. It suits something that tools outside the project link to and that gets renamed: `name` holds the readable label, and the `id` never has to change. A project can mix readable ids and UUIDs. Whatever its form, an `id` SHOULD NOT change once a document using it is published, since every pointer and inline link to it would break. Change `name` instead. If an entry's `id` has to change, keep the old entry as a deprecated stub whose `replacedBy` names the new one, so consumers can follow it (processing model §7). A validator sees one version of a document, so it can't check this; a project that needs the guarantee checks it in its own CI, by comparing ids with the version before. ### 3.2 Item ids and addresses A section item doesn't need an `id`. When it has none, a consumer MUST give it one made from its own text (vector: addresses). If an address points at two things on one entry, and neither one wins, that's an error (DSDS-05). The [processing model, §2](processing.html#2-addresses) has the full rules. ### 3.3 Trait ids A trait's `id`, and an enum value's, copy a real API name in whatever case the API uses, like `isDisabled`. They MUST NOT contain a dot, because `traitId.valueId` is how one value is addressed (schema: entries/component, common/id). ### 3.4 Namespaced values Some values can be custom: an entry kind, a section kind, a section's `context`, a `rel`, and every key in `$extensions`. A custom one MUST be namespaced: lowercase parts with at least one dot, like `acme.icon-library` (schema: common/id, common/extensions, entries/entry, sections/section, common/ref). The dot means a custom value can never clash with a built-in one. ## 4. Entries ### 4.1 Every entry Every entry MUST have `kind`, `id`, `name` and `description` (schema: entries/entry). `description` says what the entry is and why it exists, starting with what it is. `kind` MUST be `system`, `token`, `theme`, `component` or `entry`, or a namespaced custom kind (schema: entries/entry). Use `entry` for content like patterns, foundations, guides, or anything else that has no kind of its own. A `system`, `token`, `theme` or `component` entry MUST NOT have any field its schema file doesn't declare (schema: entries/component, entries/token, entries/theme, entries/system). A generic `entry` and a custom kind accept extra fields. On a generic `entry`, a validator warns about a field no DSDS schema declares (DSDS-24). What fields a custom kind has is up to whoever made it. ### 4.2 Metadata Every date in `metadata` MUST be written as `YYYY-MM-DD` (schema: metadata/metadata). `metadata.status` is one status object, or a list with one for each platform. A status MUST have a `status` word (schema: metadata/entry-metadata). When the word is `deprecated`, the status MUST also have a `deprecationNotice` that says what to use instead (schema: metadata/entry-metadata). It can name the replacement in `replacedBy`, which MUST NOT appear in a status that isn't `deprecated` (schema: metadata/entry-metadata). ### 4.3 Components A component's `sourceFiles` points at its real source code, with one entry for each platform. Each entry MUST have a `platform` and a `file` (schema: entries/component). Two entries MUST NOT name the same platform (DSDS-01). Each of a component's `traits` MUST say whether it's a `variant` or a `state` with `traitType`, and whether it's a `boolean` or an `enum` with `kind` (schema: entries/component). Each trait MUST also have an `id` and a `description` (schema: entries/component). An `enum` trait MUST list its `values`, each with an `id` and a `description` (schema: entries/component). A trait or a value MAY have its own `status`, with the same shape as an entry's. Whether the caller sets a trait as a prop is a fact about the component's API, so DSDS doesn't record it. A consumer reads it from `sourceFiles` or `specs`. ### 4.4 Combos A component's or token's `combos` are rules about what can be paired with what. Each MUST have a `subject`, an `items` list that isn't empty, and a `level` (schema: common/combo). `must` and `should` allow the pairing. `should-not` and `must-not` forbid it. Each subject or item MUST name one of three things (schema: common/combo; DSDS-09): - a trait on the same entry, like `size`, or one of its values, like `size.small`; - a token, in braces, like `{color.action.primary}`; - an entry. ### 4.5 Tokens, themes and systems A token's `tokenType` names its DTCG type, and MUST be a camelCase word like `color` or `fontFamily` (schema: entries/token). A token's value lives in the DTCG file its `source` points at, never in the DSDS document. A theme's `colorScheme` MUST be `light` or `dark` (schema: entries/theme). A system's `metadata.platforms` lists the platforms it ships on. Once that list exists, every `platform` in the document MUST be one of them (DSDS-02). That covers a status, a `sourceFiles` entry and an `imports` entry. ## 5. Sections ### 5.1 Every section Every section MUST have a `kind` and a `for` (schema: sections/section). It MUST also have `items`, `freeform` content, or both (schema: sections/section). `kind` is `guidelines`, `definitions`, `steps`, `section`, or a namespaced custom kind. `for` says who the section is written for: `all`, `human` or `agent`. A tool showing documentation to people SHOULD NOT show a `for: agent` section. An agent SHOULD read every section, not only the `for: agent` ones. A `guidelines`, `definitions` or `steps` section MUST NOT have any field that its schema file doesn't declare, on the section or on its items (schema: sections/guidelines, sections/definitions, sections/steps). A generic `section` is checked the same way as a generic entry (DSDS-24). Each freeform entry MUST have a `title` (schema: sections/section). ### 5.2 Guidelines A guideline item states one rule. It MUST have a `statement`, with two exceptions (schema: sections/guidelines): - its `refs` reuse another item with `same-as`; - its `refs` link to where the rule is tracked, with a `relates-to` `href`. It MUST have a `level` unless it has `refs` (schema: sections/guidelines). When an item that reuses another states its own `level`, the two MUST match (DSDS-10). `checkedBy` says how the rule is checked: `automated`, `assisted` or `manual`. An `automated` rule MUST list at least one test, lint rule or agent eval in `checks` that runs it (DSDS-03). ### 5.3 Definitions and steps A definitions item MUST have a `term` and a `definition`, unless it has `refs` (schema: sections/definitions). A steps item MUST have a `title`, unless it has `refs` (schema: sections/steps). Use `context` on a definitions section to say what it's for: `anatomy`, `terms`, `keyboard`, `events`, or a namespaced value. ## 6. Pointers ### 6.1 Shape A pointer has either `to`, which names something in the project, or `href`, which names something outside it. A pointer MUST NOT have both (schema: common/ref). A bare string is short for `{href: }`. A `to` names an entry by its id, or one thing inside it as `entryId#address`. It MUST be shaped like an id (schema: common/ref). It MUST point at something that exists in the project (DSDS-08, DSDS-05). ### 6.2 Where each kind of pointer goes There are fourteen kinds of pointer, and each one is a `rel` value. Eight have a field of their own. A pointer in one of those fields MUST NOT have a `rel`, because the field already says what kind it is (schema: common/ref, entries/entry, sections/guidelines, metadata/entry-metadata): | Field | On | Kind | |---|---|---| | `uses` | entry, section item | `uses` | | `extends` | entry | `extends` | | `alternatives` | entry, guideline item | `alternative-to` | | `specs` | component | `contract` | | `evidence` | guideline item | `cites` | | `checks` | guideline item | `checked-by` | | `refines` | guideline item | `refines` | | `replacedBy` | deprecated `status` | `replaced-by` | The other six go in `refs`: `same-as`, `file`, `source`, `design`, `demo` and `relates-to`. A pointer in `refs` MUST have a `rel` (schema: common/ref). A `rel` MUST be one of the fourteen, or namespaced (schema: common/ref). When an object has a field for a kind of pointer, that kind MUST go in the field, not in `refs` (DSDS-25). `source` belongs to both groups. Three fields each hold one source, with no `rel`: a token's `source` and a theme's `source` (their DTCG file), and the `file` in each of a component's `sourceFiles` (schema: entries/token, entries/theme, entries/component). Any other source, such as the package that ships the component, goes in `refs` as `rel: source`. For what each kind means, which end it's written on, and what a consumer does with it, see the [table in the processing model, §5](processing.html#5-relationships-and-their-reverse). ### 6.3 Placement A `same-as` pointer MUST sit on a section item, and MUST point at an item of the same section kind (DSDS-25). A successor named in `replacedBy` MUST sit in the status of a deprecated entry, trait or trait value (schema: metadata/entry-metadata, entries/component). ### 6.4 Direction and chains A relationship is written once, on the thing it starts from. A consumer MUST NOT require it to be written on the other end too (vector: inverse). Following `uses`, `extends`, `same-as` or `replaced-by` from one thing to the next MUST NOT lead back to where it started (DSDS-06). ### 6.5 Inline links A Markdown field (§2.5) can mention something in the project with a CommonMark link whose scheme is `dsds:`. What follows `dsds:` is a target, written exactly as a `to` would be: ```yaml statement: >- Keep the focus ring visible on every interactive element. See [focus visible](dsds:shared-a11y#focus-visible) for the full rule, or use for the main action. ``` `[text](dsds:target)` shows your own text. `` shows text the consumer takes from the target, so it stays right when the target is renamed. A reference-style link whose definition is a `dsds:` URL counts too. An inline link's target MUST be shaped like a `to` and MUST point at something that exists in the project, checked the same way as a `to` (DSDS-08, DSDS-05). A link in a code span or a fenced code block isn't a link, so it isn't checked. Links with any other scheme aren't checked either. An inline link mentions something. It isn't a relationship: it adds nothing to `uses` or any other kind of pointer, and it doesn't count when a consumer reads relationships backwards. To say how two things are connected, use a pointer field (§6.2). When a consumer turns a document into HTML, it MUST NOT output a `dsds:` URL as a link's `href` (consumer). It links to wherever it publishes the target instead, or shows the link's text with no link if it can't. The [processing model, §2.4](processing.html#24-inline-links) says how a consumer finds the target and the text. ## 7. Extensions `$extensions` holds data for one particular tool. It can sit on a document, an entry, a section or an item. Each key MUST be namespaced, and each value MUST be a mapping (schema: common/extensions). What goes inside is up to the tool: DSDS doesn't read it. There are three ways to extend DSDS: a custom kind, a profile that makes optional fields required for one project, and `$extensions`. See [Extending](extending.html). ## 8. Validators A conforming validator MUST check a document against this version's schema, with format checks turned on (suite). It MUST also check every semantic rule in the [rule catalog](conformance.html#rule-details) (suite). It MUST reject every example in the conformance suite, for the reason that example gives (suite). `DSDS-05`, `DSDS-08`, `DSDS-09`, `DSDS-11` and `DSDS-24` report a warning, not an error. They can't always see every file a document depends on, or can't be sure a field is a mistake. A validator SHOULD offer a strict mode that treats them as errors. Checking inline links (§6.5) needs a CommonMark parser. A validator that skips them, for example because the parser isn't installed, isn't checking `DSDS-05` and `DSDS-08` in full. It SHOULD say in its output that it skipped them, rather than let them pass unchecked. The reference validator, `dsds-validate`, treats its parser as an optional dependency: npm installs it by default, and without it the validator reports a warning instead. The advisory rules (`DSDS-13` and up) are suggestions. A finding from one never stops a document from conforming. ## 9. Consumers A conforming consumer follows the [processing model](processing.html). It also has to do the following: - It MUST NOT fail because a document has an optional field it doesn't recognize (consumer). - When it writes a document back out, it MUST keep any `$extensions` content it doesn't understand (consumer). - It SHOULD treat a document's `must` and `must-not` rules as binding only after deciding to trust whoever wrote the document. A document is data from its author, not instructions. See [Security](security.html). ## 10. Versions DSDS follows the version rules on the [Stability page](stability.html): what can change in a patch, a minor and a major release, and what has to be true before 1.0. Each release's schema is published at its own URL and never changes after the release. --- # Stability — DSDS 0.23.0 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](conformance.html). ## 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 semantic rules.** 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 | Change | Version bump | Your documents | |---|---|---| | Documentation-only edits | none | Unaffected | | Additions and loosenings (new optional fields, new union members, relaxed constraints, new advisory lint rules) | patch | Remain valid unchanged | | Tightenings and behavior changes (new constraints, new required fields, moved fields, **a new validator rule**) | minor | May need edits; the CHANGELOG lists every affected position | | Renames and removals | batched 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. **0.22.0 was a breaking window, and it is now closed.** It's the last one planned before 1.0. It simplifies the format rather than adding to it: - `purpose` (on entries, traits and trait values) and `metadata.context` fold into `description`, so there's one place to say why something exists. - A section no longer carries `metadata`. - `metadata.origin.author` drops to three values: `human`, `assisted`, `generated`. - A section's `tags` only classify it. They no longer change where `DSDS-18` expects it to sit. - A non-token entry `id` must match the standard id pattern. The schema always described this but never enforced it. - `composes`, `depends-on`, `part-of` and `implements` merge into one `uses` field. `part-of` pointed the other way, so it becomes `uses` written on the other entry. `DSDS-07` is retired, and `DSDS-06` checks `uses` cycles. - `replaces` becomes `replacedBy`, inside the deprecated thing's `status`, next to its `deprecationNotice`. - A single trait value can be deprecated, with its own `status` naming `button#variant.primary` in `replacedBy`. - `dsds-format` puts fields in the style guide's order, replacing the four lint rules that reported it (`DSDS-17`, `DSDS-20`, `DSDS-22`, `DSDS-23`, now retired). - The pointer fields stay, but each means one `rel`, so a pointer in it has no `rel` of its own. Entry `related` becomes `alternatives`; guideline `related` becomes `refines` on the agent rule; a checklist step is listed in the guideline's `checks` instead of pointing at it. - `rel` goes from 23 values to 14. `test`, `lint-rule` and `agent-test` become `checked-by`, with the kind of check in `role`; `external-link` becomes `cites` or `relates-to`; `storybook` becomes `demo`; `package` becomes `source`; `see-also`, `pairs-with` and `excludes` become `relates-to`. - `rel: file` is only for DSDS documents. A code file or DTCG token file is `rel: source`. A token's or theme's `source` and a component's `sourceFiles[].file` take no `rel`. `scripts/tools/migrate-to-0.22.js` covers all of it in one pass. From here, the plan 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](https://github.com/somerandomdude/design-system-documentation-schema/blob/main/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.23.0`, 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. ## Migrating a 0.21.x document `scripts/tools/migrate-to-0.22.js [--dry-run]` brings a 0.21 document to 0.22.0. It folds `purpose` and `metadata.context` into `description`, moves a section's `metadata` to that section's `$extensions["com.dsds.migration"]`, maps `origin.author` to `assisted` or `generated`, renames and merges `rel` values, drops the `rel` from pointers in named fields, turns a `rel: file` that points at a code or token file into `rel: source`, and moves a checklist step's `checks` onto the guideline it verifies. Like the 0.21 script, it edits the file in place and keeps comments. It reports every fold so you can reread the description it produced. Don't run it on the output of `migrate-to-0.20.js`, which already produces the 0.22.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 (`same-as`, `relates-to`, `demo`, …), 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 semantic rules (see the [rule catalog](conformance.html#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.23.0` is that version today — treat `/v0.23.0/` 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. --- # DSDS authoring style guide — DSDS 0.23.0 How to order things inside a DSDS document, so every document reads the same way no matter who wrote it. None of this changes whether a document is valid. The schema accepts any order. Most of this guide you never have to apply by hand: `dsds-format` puts every field in its place and groups combos. The orders that are a reading choice — entries, sections, guideline items — are only advice, and `dsds-lint` points out where a document departs from them. See [Tooling](#tooling) for which is which. ## The schema is the style guide **Write in the order the schema lists them.** The order of fields and options in the schema reflects this guide's recommendation for how to organize a document. When in doubt, copy how the schema does it. ## The patterns behind these rules Everything below is one of a few ideas, applied to a different part of a document. Learn these and you can usually work out the specific rule. **Field order comes from the schema. List order comes from this guide.** Each schema follows the style guide for field order. Which entry, section, or item comes first in a list is something a schema can't express, so this guide describes it. **Broad to specific.** The widest thing leads: `guidelines` before `steps`, `when-to-use` before `how-to-use`, `for: all` before `for: agent`. **Nothing comes before the thing it's built on.** Tokens before the themes that override them, themes before the components that use them. Read top to bottom and you meet every idea before anything that depends on it. **Shared fields before specific ones.** The fields every kind has come first, then the fields one kind adds. A `metadata` block works the same way. **Catch-alls go last.** `$extensions` after every real field, the plain `section` kind after the three specific ones, custom kinds after the five known ones. Anything that exists because nothing else fit goes at the end. **Where no rule decides, keep the order you wrote.** Don't alphabetize. Items sharing a `level`, entries sharing a kind — leave them in whatever order reads best. --- ## 1. Base documents and `shared` entries A base document is the outermost object. Its fields go in the order seen in [`base.schema.yaml`](/v0.23.0/base.schema.yaml). `shared` entries follow [`shared.schema.yaml`](/v0.23.0/shared.schema.yaml). `dsds-format` applies both, falling back to `shared.schema.yaml` for an object with no `kind`. ### Entry order, inside `entries[]` When a base document holds several entries, order the entries themselves by `kind` (the order [`entries/entry.schema.yaml`](/v0.23.0/entries/entry.schema.yaml) lists its `kind` values). This order intends to reflect the "food chain" of systems' elements. 1. **`system`:** Always first since it contextualizes everything contained within. 2. **`token`:** Defines the attributes that themes and components are created from. 3. **`theme`:** Built from tokens, so only makes sense to come after. 4. **`component`:** Built from tokens and themes. 5. **`entry`:** Always last since it can cover a broad set of topics. Onboarding and overview content that wants to be read first belongs in the `system` entry's own sections — putting an `entry` ahead of the tokens and components is what `DSDS-21` reports. This follows the same idea as [§4](#4-sections): Broadest first, and follow the dependency chain. This _theoretically_ means that all prerequisite information is provided up front for an entry. Within one kind, use whatever order reads best. Avoid arbitrary ordering, such as alphabetical when another order is better for comprehension. For example, a scale reads better as `compact`, `regular`, `loose` than as `compact`, `loose`, `regular`. ```yaml entries: - kind: system # id: acme-design-system — what the document is about - kind: token # id: color.action.primary - kind: theme # id: light — overrides the tokens above - kind: theme # id: dark - kind: component # id: button - kind: entry # id: form-layout — a pattern built from components ``` --- ## 2. Entries Follow the high-level order: 1. **Shared entry fields _except_ `$extensions`** defined in [`entries/entry.schema.yaml`](/v0.23.0/entries/entry.schema.yaml). 2. **Then that kind's own fields**, defined in `entries/.schema.yaml`. A custom kind has no file of its own, so should follow the high-level guidance. 3. **Then `$extensions`.** All information that adds to the schema goes last. `dsds-format` puts an entry's fields in this order. A field the schema doesn't declare stays right after the field it followed. ### A trait's own fields A component's `traits` lead with `traitType`, then `kind`, then the rest of the order [`entries/component.schema.yaml`](/v0.23.0/entries/component.schema.yaml) declares. `traitType` goes first because it answers the question a reader has: is this a variant the caller configures, or a state the component can be in? `kind` follows, saying whether the value is a `boolean` toggle or an `enum` with named `values`. The two are independent — `hover` is a boolean state, `size` an enum variant. `dsds-format` applies this, along with the field order of each value inside an `enum` trait. It reads both orders out of `entries/component.schema.yaml` at runtime, so this guide holds no second copy of either. --- ## 3. `metadata` A `metadata` block joins two lists the same way an entry does: the fields every entry's metadata shares, from [`metadata/metadata.schema.yaml`](/v0.23.0/metadata/metadata.schema.yaml), then the ones for that kind of entry, from [`metadata/entry-metadata.schema.yaml`](/v0.23.0/metadata/entry-metadata.schema.yaml) or [`metadata/system-metadata.schema.yaml`](/v0.23.0/metadata/system-metadata.schema.yaml), then `$extensions` last. All `metadata` types work the same way. --- ## 4. Sections ### A section's own fields A section leads with the fields that say what it is: `kind`, then `for`, then the one field its kind adds — `framing` for `guidelines`, `ordered` for `steps`. The rest follow in the order [`sections/section.schema.yaml`](/v0.23.0/sections/section.schema.yaml) lists them. That's the same reason the identifying field leads everywhere. On an entry or a section that field is `kind`. On a component trait it's `traitType`, because there `kind` only says whether the value is a boolean or an enum, not what sort of trait you're reading. Either way the field tells a reader what they're looking at before they read any of it, so it sits at the top where the reader already is. ### Keep each kind together If an entry has more than one section, **put all the sections of the same kind side by side** — every `guidelines` section in a row, then every `definitions` section, then every `steps` section, then any plain `section`. Don't mix them up: `guidelines`, `definitions`, `guidelines` is wrong, even if that's the order you thought of them in. ### Kind order: broadest first Order section kinds like this: 1. **`guidelines`** — the broadest question. Should I use this at all, and how do I use it properly? 2. **`definitions`** — reference material: parts, terms, a glossary. Narrower than guidelines, because it's about this entry's own parts and words. Wider than a set of steps. 3. **`steps`** — one procedure or checklist. The narrowest of the three, because it's one exact sequence rather than general advice. 4. **`section`** (the plain kind) — prose that didn't fit the other three. Last, because it's the fallback. That's the order [`sections/section.schema.yaml`](/v0.23.0/sections/section.schema.yaml) lists its `kind` values in. A namespaced custom kind like `acme.custom-section` isn't in that list, so it sorts after all four. A section with no `kind` at all is read as `section`, the default that file declares. ### Within `guidelines`: how broad, then who it's for Guidelines sorts content by: **how broad first, who it's for second.** `for: agent`, `framing: when-to-use` still comes before a `for: all` `framing: how-to-use`, because breadth is settled first. Audience only sorts within one breadth tier — the two below. Guidelines should lead with when to use, then describe how to use it. That's the order [`sections/guidelines.schema.yaml`](/v0.23.0/sections/guidelines.schema.yaml) lists its `framing` values in: 1. **`framing: when-to-use`:** Does it make sense to use? 2. **`framing: how-to-use`:** If so, how do you use it? Leaving `framing` out means this — it's the default that file declares. A section's `tags` say what it's about — the accessibility rules, say, or the mobile-only ones. They don't change where it sits. Within a tier, put narrow sections after general ones if that reads better; no rule checks it. Sections in the same breadth tier are then ordered by how broad the audience is. The order [`sections/section.schema.yaml`](/v0.23.0/sections/section.schema.yaml) lists its `for` values in: 1. **`for: all`** — also what you get by leaving `for` out, since it's the default that file declares. So an omitted `for` sorts first, not last. 2. **`for: human`** 3. **`for: agent`** ```yaml sections: - kind: guidelines for: all framing: when-to-use items: [...] - kind: guidelines for: all items: [...] # framing: how-to-use (default) - kind: guidelines for: all title: Accessibility tags: [accessibility] # says what it's about; doesn't move it items: # same tier and audience as the section - level: must # above, so it can go here, after the statement: Give it a name. # general rules checkedBy: assisted tags: [accessibility] - level: must statement: Keep focus visible. checkedBy: assisted tags: [accessibility] - kind: guidelines for: agent items: [...] # same framing as above, narrower # audience — so audience breaks the tie - kind: definitions context: anatomy items: [...] - kind: steps title: Migration items: [...] ``` --- ## 5. Section items ### An item's own fields Order an item's fields the way its own schema file lists them. | Shape | Its schema file | |---|---| | A `guidelines` item | [`sections/guidelines.schema.yaml`](/v0.23.0/sections/guidelines.schema.yaml) | | A `definitions` item | [`sections/definitions.schema.yaml`](/v0.23.0/sections/definitions.schema.yaml) | | A `steps` item | [`sections/steps.schema.yaml`](/v0.23.0/sections/steps.schema.yaml) | ### Guideline item order, by `level` Order items by `level`, in the order [`common/requirement-level.schema.yaml`](/v0.23.0/common/requirement-level.schema.yaml) lists its values in: ``` must should may should-not must-not ``` Order items at the same level in whatever way aids comprehension. Ideally, group subject matter of the same level together. Don't alphabetize items that share a level — that's an arbitrary order dressed up as a rule. ```yaml items: - level: must statement: Use semantic tokens instead of raw values. - level: should statement: Prefer the `primary` variant for the page's one main action. - level: may statement: Add a leading icon when it disambiguates the action. - level: should-not statement: Should not pair `loading` with `disabled` in the same interaction. - level: must-not statement: Never stack two primary-variant buttons in the same view. ``` --- ## 6. Refs and combos ### Combos: by subject, then level Combos group by: **subject first, level second.** Keep every combo for one subject together, so a reader checking one trait or token finds all of its rules in one place. Within a subject, order them by `level`, as [§5](#5-section-items) does for guideline items. Which subject goes first is up to you — put the one that matters most where a reader will look first. Don't alphabetize, same as everywhere else. A combo reads as one sentence: this subject, at this strength, with these things. That's the order [`common/combo.schema.yaml`](/v0.23.0/common/combo.schema.yaml) lists its fields in: 1. **`subject`:** What is the rule about? A trait, a token, or an entry. 2. **`level`:** Does it permit or forbid and how strictly? 3. **`items`:** What does the rule apply to? 4. **`note`:** Why does it hold? ### Refs: what it points at, then what kind of pointer A ref answers what it points at before what kind of pointer it is. That's the order [`common/ref.schema.yaml`](/v0.23.0/common/ref.schema.yaml) lists its fields in: 1. **`to` or `href`:** What does this point at? `to` for something in this document, `href` for anything outside it. Exactly one of the two. 2. **`rel`:** What kind of pointer is it? Left out in a field named for one kind of pointer (`uses`, `extends`, `alternatives`, `evidence`, `checks`, `refines`, `replacedBy`, `specs`), where the field says it. 3. **`role`:** What does the thing being pointed at do here? 4. **`note`:** Anything else worth saying about the connection. ### Inline links: a pointer for a relationship, a link for a mention When prose mentions another part of the system, link to it instead of writing its id in backticks. A validator checks a `dsds:` link; it can't check `` `button#variant.primary` ``. - **``** for a named thing: an entry, a definition, a step, or a trait or value with a `name`. The text comes from the target, so it stays right after a rename. - **`[keep the focus ring visible](dsds:shared-a11y#focus-visible)`** for a guideline, which has no short name, and anywhere the sentence needs its own words. - **Give a link's target its own `id`, and don't change it.** An item without one is addressed by a slug of its text, so rewording the rule breaks every link to it. Rename by changing `name`, never `id`. For something that gets renamed often, or that other tools link to, a lowercase UUID makes a good `id`; `` still shows its `name`. - **A link isn't a relationship.** If the entry depends on the thing it mentions, say so in `uses` too. A tool answering "what does changing this affect?" reads pointers, not prose. This is about reading order, not field order, so `dsds-format` doesn't apply it. --- ## 7. A complete example ```yaml kind: component id: badge name: Badge description: A small status indicator, attached to another element. Draws attention to a count, state, or category without interrupting the layout it's attached to. metadata: tags: [status, indicator, count] since: 1.2.0 status: {status: stable} sections: - kind: guidelines for: all framing: when-to-use items: - level: should statement: Use to surface a count or state on another element (a notification count, an unread indicator). - level: should-not statement: Do not use as a replacement for a full status message a user needs to act on. - kind: guidelines for: all items: - level: must statement: Keep label text to a single word or number. checkedBy: manual - level: must statement: Pair with an accessible label when the badge conveys meaning color alone cannot. checkedBy: assisted - level: should statement: Prefer the `dot` variant when the exact count isn't meaningful to the user. - kind: guidelines for: all title: Accessibility items: - level: must statement: Expose the badge's content to assistive technology even when visually decorative. checkedBy: assisted - kind: definitions for: all context: anatomy items: - term: Dot definition: The minimal variant with no visible label, just a colored indicator. alternatives: - to: tag refs: - href: https://github.com/org/ds/react/badge rel: source sourceFiles: - platform: react file: ./src/Badge.tsx specs: - href: ./contracts/badge.contract.json traits: - traitType: variant kind: enum id: variant description: Which visual form the badge takes. values: - id: count description: Shows a numeric value. - id: dot description: Shows no value, just presence. combos: - subject: variant.dot level: must-not items: [variant.count] note: A badge is either a dot or a count, never both at once. ``` --- ## Tooling Two tools cover this guide, split by whether the order is a fact or a choice. - **`dsds-format` applies the orders that have one right answer.** Every field order comes from a schema file, and combos are an unordered set of rules. It moves lines and nothing else: text, comments and wrapping stay as they are, a comment moves with the field below it, and the result has to hold exactly the same data as before. `dsds-format --check` changes nothing and exits 1 if a file needs formatting, for CI. In this repo, `npm run format` runs it and `npm run check` runs the check. - **`dsds-lint` reports the orders that are a reading choice** — entries, sections, guideline items. An author can have a reason to break them, so they stay advice: three warning-only rules in `schema/conformance-rules.yaml`, run with `npm run lint`. Until 0.22.0, four more lint rules (`DSDS-17`, `DSDS-20`, `DSDS-22`, `DSDS-23`) reported the field and combo orders. `dsds-format` replaced them. ### What's covered Every rule in this guide is covered except one, and that one can't be. | Rule | Where | Covered by | |---|---|---| | A base document's own fields | [§1](#1-base-documents-and-shared-entries) | `dsds-format` | | A `shared` entry's own fields | [§1](#1-base-documents-and-shared-entries) | `dsds-format` | | Entry order inside `entries[]` | [§1](#1-base-documents-and-shared-entries) | `DSDS-21` | | An entry's own fields | [§2](#2-entries) | `dsds-format` | | A trait's own fields | [§2](#2-entries) | `dsds-format` | | A trait value's own fields | [§2](#2-entries) | `dsds-format` | | A `metadata` block's fields | [§3](#3-metadata) | `dsds-format` | | A section's own fields | [§4](#4-sections) | `dsds-format` | | Same-kind sections kept together | [§4](#4-sections) | `DSDS-18` | | Section kind order | [§4](#4-sections) | `DSDS-18` | | Breadth, then audience, among `guidelines` | [§4](#4-sections) | `DSDS-18` | | An item's own fields | [§5](#5-section-items) | `dsds-format` | | Guideline items by `level` | [§5](#5-section-items) | `DSDS-19` | | A combo's own fields | [§6](#6-refs-and-combos) | `dsds-format` | | Combos for one subject kept together, then by level | [§6](#6-refs-and-combos) | `dsds-format` | | A ref's own fields | [§6](#6-refs-and-combos) | `dsds-format` | | Order within one entry kind, or one `level` | [§1](#1-base-documents-and-shared-entries), [§5](#5-section-items) | can't be — it's a judgment call | **Every sort reads a field, and classification never sorts.** `DSDS-18` used to put a section declaring `tags` in its own, narrowest tier. That gave `tags` two jobs, so tagging a section to classify it could also make it out of order. Since 0.22.0 `tags` only classifies, and `DSDS-18` sorts on `framing` and `for` alone. **Field order has one source: the schema files.** The tables on the [Schema page](schema.html) are generated from each schema file's own order, `dsds-format` reads that same order when it runs, and [§2](#2-entries) tells you to follow it. So you can read the authoring order off a property table, and the other way round. --- ## Bundled schema (v0.23.0) ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/dsds.bundled.yaml", "title": "Design System Doc Spec (DSDS) v0.23.0 — Bundled", "description": "Every schema file under schema/, collected into one document. Generated by scripts/generate/bundle.js - do not hand-edit.", "$ref": "#/$defs/base", "$defs": { "base": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/base.schema.yaml", "title": "Base", "description": "A DSDS document. Acts as the container for all content. Can be optionally extended or split across other DSDS documents.", "$comment": "Base only has one document structure. Unlike an entry or a section, nothing here varies by kind.", "type": "object", "required": [ "schemaVersion", "name", "entries" ], "properties": { "schemaVersion": { "type": "string", "description": "The version of this spec the document follows.", "$comment": "The one authoritative version marker. Distinct from `metadata.version`, which is the design system's own release version.", "example": "0.23.0" }, "$schema": { "type": "string", "format": "uri-reference", "description": "An optional hint for editor tools, like autocomplete or validate-on-save.", "$comment": "Usually a URL to the exact versioned schema file. Not authoritative: `schemaVersion` is the real source of truth for which version this document targets. The published bundle is YAML - most YAML-aware editors (ex: VS Code's YAML extension) resolve a YAML `$schema` target the same way they'd resolve a JSON one; a tool that only ever reads JSON schemas won't.", "example": "https://designsystemdocspec.org/v0.23.0/dsds.bundled.yaml" }, "name": { "type": "string", "description": "The name of the design system this document describes.", "example": "Acme Design System" }, "entries": { "type": "array", "minItems": 1, "description": "Every entry this document owns, listed directly.", "$comment": "A single-entry file just has one item here. System-wide facts (version, organization, url, license, platforms) live on this list's own `kind: system` entry. Reusable content that is not a standalong artifact belongs in `shared`.", "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/entry.schema.yaml#/$defs/dispatch" } }, "shared": { "type": "array", "minItems": 1, "description": "Reusable content that is not itself a design system artifact, for example an accessibility rule or a guideline that applies broadly.", "$comment": "Enables multiple artifacts to reference share content from one source. The content is agnostic to entry `kind`. A shared entry is never meant to be its own page.", "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/shared.schema.yaml" } }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "The same pointer type entries use.", "$comment": "Enables pointing at a parent document (rel: extends) and splitting a system across several files (rel: file, pointing at a sibling document that owns other entries in this same system).", "example": [ { "href": "./starter-kit.dsds.yaml", "rel": "file", "role": "core" } ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool-specific document-level data, keyed by namespace.", "$comment": "Same structure as an entry's own $extensions." } }, "additionalProperties": false }, "common/combo": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/combo.schema.yaml", "title": "Combo", "type": "object", "description": "Defines what an element can or cannot be paired with. Relates to traits, tokens, and entries.", "$comment": "A combo is a pairing rule, It lives on the entry the rule is about (`subject`), naming what else (`items`) can or cannot be used with it, and how strict the rule is (`level`). `subject` and each of `items` do not need to be the same type.", "required": [ "subject", "items", "level" ], "properties": { "subject": { "$ref": "#/$defs/target", "description": "The subject of what elements can or cannot be paired with. Can be a trait, token, or entry id.", "example": "size.large" }, "level": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/requirement-level.schema.yaml", "description": "Whether the elements in `items` can or cannot be paired with the subject—and how strict the rule is.", "$comment": "`must`/`should` make `items` allowed. `should-not`/`must-not` make `items` denied. `must`/`must-not` make the rule explicit. `should`/`should-not` make it a recommendation." }, "items": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/target" }, "description": "The elements paired with the subject.", "$comment": "What `level` says (permitted or forbidden) applies to every entry here.", "example": [ "{color.surface.default}", "{color.surface.raised}" ] }, "note": { "type": "string", "description": "Why this rule holds.", "$comment": "Provides rationale to the rule defined in `subject`/`items`/`level`.", "example": "Contrast is verified only against these surfaces; on any other background the label ratio is unproven." } }, "additionalProperties": false, "$defs": { "target": { "description": "An entry id, a trait target on this entry, or a token reference wrapped in braces.", "anyOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml" }, { "$ref": "#/$defs/traitTarget" }, { "$ref": "#/$defs/tokenRef" } ], "example": "{color.action.primary}" }, "traitTarget": { "type": "string", "description": "A boolean trait's id, or `traitId.valueId` for one enum value.", "$comment": "Segments use the same shape as common/id.schema.yaml's `apiId`, since a trait id mirrors a real prop or attribute name. Exactly one optional dot: the addressing form is never deeper than trait-then-value, which is why `apiId` itself forbids a dot.", "pattern": "^[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*(\\.[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*)?$", "example": "size.large" }, "tokenRef": { "type": "string", "description": "A token reference, wrapped in braces.", "$comment": "Wraps common/id.schema.yaml's `tokenId` shape, so a token id this schema accepts can always be referenced here. The two drifted before: `tokenId` allowed slashes and this pattern did not.", "pattern": "^\\{[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*([./][A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*)*\\}$", "example": "{color.action.primary}" } } }, "common/example": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/example.schema.yaml", "title": "Example", "type": "object", "description": "A single example, illustrating something in context.", "$comment": "At least one of `title`, `description`, `showcase`, or `ref` must be given.", "properties": { "title": { "type": "string", "description": "A short heading for the example.", "example": "Loading state has an accessible announcement" }, "description": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "Describes the example in detail.", "example": "Renders with aria-live=\"polite\" so the loading state is announced." }, "showcase": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/showcase.schema.yaml", "description": "A visual sample of this example.", "$comment": "This property is used only when a visual example exists. Sometimes the example is only code or a file reference. Use `ref` in those cases." }, "ref": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml", "description": "Where this example lives in code.", "$comment": "Use `rel: source` for a source file, or `rel: demo` for a live story or demo." } }, "anyOf": [ { "required": [ "title" ] }, { "required": [ "description" ] }, { "required": [ "showcase" ] }, { "required": [ "ref" ] } ], "$defs": { "list": { "type": "array", "minItems": 1, "description": "One or more examples, in order.", "$comment": "Every consumer expects an array of examples. Pulled out here so call sites don't have to redefine it.", "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/example.schema.yaml" } } } }, "common/extensions": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "title": "Extensions", "type": "object", "description": "Escape hatch for tool data, or for an outside id that doesn't fit this schema's own id pattern. Each top-level key MUST be a dotted namespace, like `com.acme`, matching the Design Tokens Community Group's own `$extensions` convention.", "$comment": "The namespace requirement is what lets a consumer skip data it doesn't recognize instead of guessing at it. By convention, an outside id goes under `$extensions..displayName`. Worth noting: nothing enforces that specific key.\nRecommended: add a `context` key under each namespace explaining what the extension is for and how a tool should use it.", "propertyNames": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml#/$defs/namespaced" }, "additionalProperties": { "type": "object", "additionalProperties": true }, "example": { "com.figma": { "context": "Links this entry to its source Figma component for design-file lookups.", "displayName": "Button/Primary", "nodeId": "12:4045" } } }, "common/id": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "title": "Id", "type": "string", "description": "Lowercase, dash-separated segments, optionally chained with dots. A section item's own `id` is optional, but a conforming consumer MUST be able to address every item. When an item has no `id`, consumers MUST derive one from the item's own text so content has a consistent id. The `$comment` below gives the exact derivation.", "$comment": "An id from an outside tool that doesn't fit this format goes under `$extensions..displayName` instead.\nA lowercase UUID fits this format, so it can be the id itself (`3f9a1c2e-8b4d-4e2a-9c71-5d0e6f8a2b13`), for an entry or an item that other tools link to and that gets renamed. Put the readable label in `name`, and link to it with `` in prose, which shows the name. An uppercase UUID doesn't fit: generate it in lowercase. Readable ids and UUIDs can be mixed in one project.\nA published id should never change, since every pointer and inline link to it would break, along with any link from outside the project. Change `name` instead. If the id really has to change, keep the old entry as a deprecated stub whose `replacedBy` names the new one.\nAn agent citing \"why did you do that\" needs something to cite, and same-as/refines/checks all point at an item by id. To derive one: lowercase the item's own primary text field (a definitions item's `term`, a guidelines item's `statement`, a steps item's or freeform entry's `title`), replace each run of characters outside `[a-z0-9]` with a single dash, then trim any leading or trailing dash.", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*(\\.[a-z0-9]+(-[a-z0-9]+)*)*$", "example": "color.action.primary", "$defs": { "apiId": { "type": "string", "description": "A single identifier copied verbatim from a real API, in whatever case that API uses.", "$comment": "For an id that names something that already exists rather than being derived from prose: a prop, an attribute, an enum value. The base id format assumes derivation (a guideline's `statement`, a definition's `term`) via the lowercase-and-dash algorithm above, so a slug shape fits there. A trait id is the opposite - it mirrors a React or Vue prop, or a Web Component attribute, and rewriting `isDisabled` as `is-disabled` gives a name no tool can call. Accepts camelCase, PascalCase, snake_case and kebab-case; rejects spaces, an empty string, and a leading or trailing dash. A leading digit is allowed, since a real enum value can be a bare number (``'s `level` trait uses `1`-`6`, matching the real attribute value). A dot is deliberately forbidden, and that - not the slash - is why this can't just be `tokenId`: a `combo` addresses an enum value as `traitId.valueId`, so a dot inside a trait id would make `size.large` ambiguous between a trait named `size.large` and value `large` of trait `size`.", "pattern": "^[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*$", "example": "isDisabled" }, "tokenId": { "type": "string", "description": "The same as `apiId`, but segments can also be chained with a dot or a slash.", "$comment": "Only for token entries. Their id often comes from a design tool that names paths that way, and that tool's casing comes with it - a Figma variable named `Color/Action/Primary` is the real name, so it validates as written.", "pattern": "^[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*([./][A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*)*$", "example": "color/action/primary" }, "namespaced": { "type": "string", "description": "A dotted, namespaced custom value. The open extension point alongside a fixed set of well-known values.", "$comment": "Same lowercase-dash-per-segment format as the base id, but requires at least one dot. This is so a custom value can't collide with existing ids.", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*(\\.[a-z0-9]+(-[a-z0-9]+)*)+$", "example": "acme.icon-library" } } }, "common/markdown": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "title": "Markdown", "type": "string", "description": "Prose written in CommonMark, with GFM tables and fenced code blocks allowed. It can link to something in the project with a `dsds:` link, whose target is any string a `to` field accepts.", "$comment": "Two forms, both standard CommonMark, so any Markdown parser finds them: `[focus visible](dsds:shared-a11y#focus-visible)` with your own text, and ``, whose text a consumer fills in from the target (processing model, section 2.4). The target is checked like a `to`: DSDS-08 for the entry, DSDS-05 for the part after `#`. An inline link is not a relationship. It doesn't count as `uses` or `relates-to`, and doesn't appear when a consumer reads relationships backwards. Use a pointer field for a relationship, and an inline link to mention something in a sentence.\nA JSON Schema can't see inside a string, so nothing here checks the links: a validator does. For a real code sample, prefer pointing a `ref` at a file instead of embedding one here.", "example": "Use for the single primary action on a surface. For navigation, use ." }, "common/ref": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml", "title": "Ref", "description": "A pointer from one thing to another, with `rel` saying what kind of pointer it is.", "$comment": "Every link in DSDS is this one shape: \"A points at B\". What kind of link it is (built from, replaced by, cited, and so on) is the `rel`.\nB is either `to` or `href`, never both. `to` points at something in this project: an entry id, or `entryId#itemId` for one item inside an entry. `href` points outside it: a file, a URL or a package. Which one you use doesn't depend on `rel`.\nThere are two places a pointer can be written. In `refs`, each pointer says its kind with `rel`. In a field named for one kind of pointer (`uses`, `extends`, `alternatives`, `evidence`, `checks`, `refines`, `replacedBy`, a component's `specs` and `sourceFiles[].file`, a token's or theme's `source`), the field name already says the kind, so the pointer has no `rel`. See `$defs/target`.\nOnly add a new `rel` value when no existing one fits. Otherwise widen that value's description.\nA plain string means `{href: }`. A plain string is never read as an id, so use the object form to point at something inside the project, or when an outside link needs a `role` or `note`.", "oneOf": [ { "type": "string", "format": "uri-reference", "description": "A plain outside link, short for `{href: }`.", "example": "https://storybook.org/ds/button" }, { "type": "object", "required": [ "rel" ], "oneOf": [ { "required": [ "to" ] }, { "required": [ "href" ] } ], "properties": { "to": { "$ref": "#/$defs/to" }, "href": { "$ref": "#/$defs/href" }, "rel": { "$ref": "#/$defs/rel" }, "role": { "$ref": "#/$defs/role" }, "note": { "$ref": "#/$defs/note" } }, "additionalProperties": false } ], "$defs": { "to": { "type": "string", "pattern": "^[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*([./][A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*)*(#[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*(\\.[A-Za-z0-9_$]+(-[A-Za-z0-9_$]+)*)*)?$", "description": "What this points at, inside the project.", "$comment": "An id on its own points at a whole entry. Add `#` and an address to point at one thing inside it: an item (`shared-a11y#focus-visible`), a trait (`button#isDisabled`), or one value of a trait (`button#variant.primary`, the same form a combo uses). The part after `#` can use a trait id's casing. The part before it can use a token id's looser form, with slashes and capitals, because `to` can point at a token.\nThe pattern only checks that the value is shaped like an id. It catches a value with a space in it, but not a display name like `to: Button`, because a token id can have capitals. Whether the target exists is checked separately: DSDS-08 for an entry, DSDS-05 for an address. How an address finds its target is in the processing model, section 2.", "example": "shared-a11y#focus-visible" }, "href": { "type": "string", "format": "uri-reference", "description": "What this points at, outside the project. A file, a URL or a package.", "$comment": "A URI. It can be a relative path (`./tests/button.a11y.test.ts`) or a package name with a scheme (`npm:@org/ds-react`).", "example": "./tests/button.a11y.test.ts" }, "role": { "type": "string", "description": "What the target does here.", "$comment": "In a guideline's `checks`, say what kind of check it is (`test`, `lint-rule` or `agent-test`) so a reader can tell them apart.", "example": "hosted docs" }, "note": { "type": "string", "description": "Anything else worth saying about this link.", "example": "Component Story Format (CSF3) - the source the hosted docs above are built from." }, "rel": { "oneOf": [ { "type": "string", "enum": [ "same-as", "file", "source", "design", "demo", "relates-to", "uses", "extends", "alternative-to", "replaced-by", "cites", "checked-by", "contract", "refines" ] }, { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml#/$defs/namespaced" } ], "description": "What kind of pointer this is, or a namespaced custom value. A relationship is written once, on the thing it starts from. A conforming consumer works out the reverse direction itself, and MUST NOT require it to be written on the target as well. Where an object has a field named for a kind of pointer, that kind MUST go in the field, not in `refs` (DSDS-25).", "$comment": "Fourteen values. The first six are written in `refs`, with `rel`. The other eight each have a field of their own, and a pointer in that field doesn't repeat the `rel`. Each entry below says what the value means, where it's written, and how it reads from the other end. The processing model, section 5, has the same list with what a consumer does with each.\nWritten in `refs`:\n`same-as`: this item reuses another item's content instead of repeating it. Goes on a section item, pointing at an item of the same kind (`shared-a11y#focus-visible`). The item can then leave out its own text. Reads \"reused by\" from the other end. Not the same as `refines`, which states a stricter rule for an agent.\n`file`: another DSDS document in the same project. Goes on a base document or an entry. A consumer reads the target as part of the project. DSDS documents only: a code file or a DTCG token file is `source`.\n`source`: where this is built. A repository, a source file, a DTCG token file or a package (`npm:@acme/ui`). Covers what `package` meant before 0.22.0, and what `file` meant for a code or token file. Three fields mean `source` without saying so: a token's or theme's `source`, and the `file` in a component's `sourceFiles`, which tools read the API from.\n`design`: the design file this comes from, such as a Figma component.\n`demo`: a live demo of this, such as a Storybook story. Covers what `storybook` meant before 0.22.0.\n`relates-to`: worth reading alongside this, when no other kind fits. Reads the same from both ends. Covers what `see-also`, `pairs-with` and `excludes` meant (a rule about combining two things is a combo, which has a `level`). A guideline item with no `statement` and a `relates-to` link with an `href` means \"this rule is tracked over there\", which `external-link` used to say.\nWritten in a field of its own:\n`uses`: the `uses` field on an entry or a section item. This is built from the target, needs it, or follows it. A dialog uses its button. A button uses a color token, and the pattern it follows. If the target changes, this may be affected. Reads \"used by\". Covers what `composes`, `depends-on`, `part-of` and `implements` meant before 0.22.0. Not for \"worth reading too\": that's `relates-to`.\n`replaced-by`: the `replacedBy` field in a deprecated `status`, on an entry, a trait or a trait value, next to its `deprecationNotice`. The target replaces this. Several successors means several pointers, and the notice says when to choose which. Reads \"replaces\".\n`extends`: an entry's `extends` field. This inherits from the target, like a dark theme from a light one. Reads \"extended by\". A consumer never merges the target's content into this entry (processing model, section 6).\n`alternative-to`: the `alternatives` field on an entry or a guideline item. Use the target instead, in the situation the entry or rule describes. Reads the same from both ends.\n`cites`: a guideline's `evidence` field. The outside standard the rule is based on, such as a WCAG criterion. Covers what `external-link` meant in `evidence`. For a link that's only related, use `relates-to`.\n`checked-by`: a guideline's `checks` field. What checks the rule: a test, a lint rule or an agent eval (an `href`, with `role` saying which), or a checklist step (a `to`). Reads \"checks\". Covers what `test`, `lint-rule` and `agent-test` meant, and replaces a step's own `checks` field, which pointed the other way. An agent eval usually gives a pass rate rather than a pass or fail, so it more often backs `checkedBy: assisted` than `automated`.\n`contract`: a component's `specs` field. A machine-readable API contract that already exists, such as a Custom Elements Manifest. DSDS points at it and doesn't read it.\n`refines`: a guideline's `refines` field, on a `for: agent` item. This states the target, a rule written for people, more strictly for an agent. An agent follows both. Reads \"refined by\".\nA namespaced custom value (`acme.supersedes`) is allowed too. A consumer that doesn't understand it can ignore it.", "example": "relates-to" }, "list": { "type": "array", "minItems": 1, "description": "One or more pointers, in order, each with its own `rel`.", "$comment": "The general `refs` list.", "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml" } }, "target": { "description": "A pointer in a field named for one kind of pointer. It has no `rel`, because the field name says the kind. A plain string means `{href: }`.", "oneOf": [ { "type": "string", "format": "uri-reference", "description": "A plain outside link, short for `{href: }`.", "example": "https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html" }, { "type": "object", "oneOf": [ { "required": [ "to" ] }, { "required": [ "href" ] } ], "properties": { "to": { "$ref": "#/$defs/to" }, "href": { "$ref": "#/$defs/href" }, "role": { "$ref": "#/$defs/role" }, "note": { "$ref": "#/$defs/note" } }, "additionalProperties": false } ] }, "targets": { "type": "array", "minItems": 1, "description": "One or more pointers of the kind the field is named for, in order.", "items": { "$ref": "#/$defs/target" } } } }, "common/requirement-level": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/requirement-level.schema.yaml", "title": "RequirementLevel", "type": "string", "description": "How strict a rule is, from must to must-not.", "$comment": "The values come directly from [RFC 2119](https://www.rfc-editor.org/info/rfc2119/).", "enum": [ "must", "should", "may", "should-not", "must-not" ] }, "common/showcase": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/showcase.schema.yaml", "title": "Showcase", "description": "A visual sample of something. Can be a media file (image or video) or a link to a live page.", "$comment": "For code, point at a file or story with a `ref` instead.", "required": [ "kind", "url" ], "properties": { "kind": { "type": "string", "enum": [ "image", "video", "sound", "html", "file", "code", "other" ], "description": "What kind of media this showcase is." }, "url": { "type": "string", "format": "uri-reference", "description": "Where the showcase lives.", "example": "https://cdn.example.com/screenshots/button-primary.png" }, "alt": { "type": "string", "description": "Alt text describing the image or video, for accessibility", "example": "Primary button in its default state, showing the filled blue background and white label." }, "note": { "type": "string", "description": "Provides additional detail about the showcase if needed.", "example": "Captured from the Storybook build, light theme." } }, "additionalProperties": false }, "common/since": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/common/since.schema.yaml", "title": "Since", "type": "string", "description": "The version something was introduced in.", "$comment": "This value isn't pattern-validated since design system's own version scheme can vary.", "example": "1.4.0" }, "entries/component": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml", "title": "ComponentEntry", "description": "A reusable UI element, like a button or a dialog.", "$comment": "`traits`, `combos`, `imports`, `sourceFiles`, and `specs` live directly here rather than in a section, since they're facts about the component as a build artifact, not documentation content.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/entry.schema.yaml" }, { "type": "object", "properties": { "kind": { "const": "component", "description": "Marks this entry as a component." }, "metadata": { "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/entry-metadata.schema.yaml" } ], "unevaluatedProperties": false }, "sourceFiles": { "type": "array", "minItems": 1, "description": "One entry per platform's source file.", "$comment": "Defining the source file in the schema enables a tool to extract the component's full API (properties, events, slots, CSS hooks) straight from the code. This avoids drift between docs and code. Points at raw source (e.g. `Button.tsx`) - a project whose tooling already generates a manifest *from* that source (a Custom Elements Manifest, or similar) points `specs` at the generated document itself instead of repeating the extraction here.", "items": { "type": "object", "required": [ "platform", "file" ], "properties": { "platform": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "The platform or framework this interface applies to. When the document declares a `metadata.platforms` list, this MUST be one of its entries.", "example": "react" }, "file": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/target", "description": "Path to the component's source file (rel: source). No `rel`: this field says it.", "$comment": "A bare string is a plain file path (shorthand for href). Use the object form when role or note matters. A relative path starts from the system's `metadata.sourceRoot` when it has one, and from this document's folder when it doesn't.", "example": "./src/Button.tsx" } }, "additionalProperties": false } }, "imports": { "type": "array", "minItems": 1, "description": "One entry per platform.", "$comment": "List the primary platform first.", "items": { "type": "object", "required": [ "platform", "code" ], "properties": { "platform": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "The platform this applies to. When the document declares a `metadata.platforms` list, this MUST be one of its entries.", "example": "react" }, "code": { "type": "string", "description": "The import statement itself, written out.", "example": "import { Button } from '@acme/ui';" }, "package": { "type": "string", "description": "The package to install, exactly as named in the package manager.", "example": "@acme/ui" } } } }, "specs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "Machine-readable API contract(s) for this component (props, slots, events, etc.) in a standard, tool-readable format (contract). No `rel`: this field says it.", "$comment": "`specs` is different from `sourceFiles`. This points at an already-extracted contract document. It could be a a Custom Elements Manifest (https://github.com/webcomponents/custom-elements-manifest, a DS Contracts document (https://github.com/southleft/ds-contracts-poc), or your own format. The property accepts entry per platform or format, if more than one exists; use `role` to name the format when it isn't obvious from the file extension.", "example": [ { "href": "./contracts/button.contract.json", "role": "DS Contracts" } ] }, "traits": { "type": "array", "description": "The component's variants and states. Every item says which one it is with `traitType`, and carries a `kind` of `boolean` or `enum` for the form it takes - an enum has its own `values` list, a boolean is a plain toggle.", "$comment": "Two independent axes, deliberately. `kind` is the `anyOf` discriminator, because a boolean and an enum genuinely carry different fields - compare `sections/steps.schema.yaml`'s and `sections/guidelines.schema.yaml`'s own items, which stay one flexible form with no `kind` tag for exactly that reason. `traitType` is the taxonomy a reader wants, and both kinds can be either type: `hover` is a boolean state, `size` an enum variant.", "minItems": 1, "items": { "anyOf": [ { "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml#/$defs/traitValue" }, { "type": "object", "required": [ "traitType", "kind" ], "properties": { "traitType": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml#/$defs/traitType" }, "kind": { "const": "boolean", "description": "Marks this trait as a yes/no toggle.", "$comment": "A boolean can be either `traitType` - an `outlined` variant or a `hover` state are both plain toggles. `kind` is only about the form the value takes." }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Pointers to resources for this trait: a design tool entry, a source file, or a verifying test." } } } ], "unevaluatedProperties": false }, { "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml#/$defs/traitValue" }, { "type": "object", "required": [ "traitType", "kind", "values" ], "properties": { "traitType": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml#/$defs/traitType" }, "kind": { "const": "enum", "description": "Marks this trait as an enumerated configuration dimension." }, "id": { "description": "The machine-readable name for the dimension, for example 'size'." }, "name": { "description": "The human-readable name of the dimension, for example 'Size'." }, "description": { "description": "What this dimension of variation controls." }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Pointers to resources for this dimension as a whole: a design tool entry, a source file, or a verifying test." }, "values": { "type": "array", "minItems": 1, "description": "The possible values, in order. The first value is implied as the default.", "$comment": "Each value is a `traitValue`.", "example": [ { "id": "small", "description": "Compact size for dense layouts." }, { "id": "medium", "description": "The default size for most surfaces." } ], "items": { "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml#/$defs/traitValue" } ], "properties": { "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Pointers to resources for this specific value: a design tool entry, a source file, or a verifying test." } }, "unevaluatedProperties": false } } } } ], "unevaluatedProperties": false } ] } }, "combos": { "type": "array", "minItems": 1, "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/combo.schema.yaml" }, "description": "Define which of this component's own boolean traits or enum values can or cannot be paired with each other.", "example": [ { "subject": "loading", "level": "must-not", "items": [ "disabled" ], "note": "A control can't be simultaneously loading and disabled." } ] } } } ], "unevaluatedProperties": false, "$defs": { "traitType": { "type": "string", "enum": [ "variant", "state" ], "description": "Whether this trait is a variant the caller configures, or a state the component can be in.", "$comment": "`variant` is a dimension of configuration, like `size` or the `variant` trait itself. `state` is a condition the component is in, like `hover`, `loading` or `disabled`. Note that a state is not always something the component sets on its own - `disabled` and `loading` are states the caller turns on. Required.\nWhether a trait is a prop the caller passes, or something the component does on its own (`hover`), is a fact about the component's API, so DSDS doesn't restate it: follow `sourceFiles` or `specs` to the code or contract. `setBy` carried it until 0.21.0, and isn't coming back." }, "traitValue": { "type": "object", "required": [ "id", "description" ], "description": "The shared properties of a boolean trait or one enum value - what it is, and what it's for.", "$comment": "The shared properties of boolean and enum traits. Contains all the details to describe a trait's value, what it is, and what it's for.", "properties": { "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml#/$defs/apiId", "description": "The machine-readable id. Mirrors the real prop, attribute or value name, so it accepts camelCase, PascalCase, snake_case and kebab-case.", "example": "isDisabled" }, "name": { "type": "string", "description": "The human-readable name. Uses `id` when left out.", "example": "Loading" }, "description": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "What this is, why it exists, how it looks or behaves, and any constraints.", "example": "Shows a spinner in place of the label and blocks interaction while active, so an action can't be submitted twice." }, "examples": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/example.schema.yaml#/$defs/list", "description": "Examples showing this in context." }, "since": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/since.schema.yaml", "description": "The version this was introduced." }, "status": { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/entry-metadata.schema.yaml#/$defs/status", "description": "This trait's or value's own lifecycle status, when it differs from the component's.", "$comment": "The same shape as an entry's `metadata.status`, so `status: deprecated` needs a `deprecationNotice`, and `replacedBy`, next to it, names the successor (`button#variant.primary`).", "example": { "status": "deprecated", "deprecationNotice": "Renamed to `primary`.", "replacedBy": [ { "to": "button#variant.primary" } ] } } } } } }, "entries/entry": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/entries/entry.schema.yaml", "title": "Entry", "type": "object", "description": "The structure every entry kind shares: `id`, `kind`, `name`, `description` (required), plus `metadata`, `sections`, `uses`, `extends`, `alternatives`, `refs`, `$extensions` (optional). This schema doubles as a general-use entry that isn't explicitly defined in the schema (ex: pattern, foundation, guideline). See schema/entries/ for each kind's own closing file.", "$comment": "Shared fields for every entry kind. Kind-specific fields, like a token's `tokenType`, live in that kind's own entries/.schema.yaml file instead.\n`kind` is one of 5 well-known values (`system`, `component`, `token`, `theme`, `entry`) or a namespaced custom value like \"acme.icon-library\". Use `entry` for anything that doesn't need fields beyond this base. Only add a dedicated file once a kind needs a field none of the others do.\nReusable content that isn't a design-system artifact (like a shared accessibility rule) doesn't belong here as a kind. Use the base document's `shared` list instead.", "required": [ "id", "kind", "name", "description" ], "if": { "required": [ "kind" ], "properties": { "kind": { "const": "token" } } }, "else": { "properties": { "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml" } } }, "properties": { "kind": { "description": "What kind of thing this entry is, as a real design-system artifact: one of the 5 well-known kinds, or a namespaced custom kind.", "$comment": "Reusable content that is not an artifact at all (like a rule other entries point at instead of restating) belongs in the base document's `shared` list instead.", "oneOf": [ { "type": "string", "enum": [ "system", "token", "theme", "component", "entry" ], "default": "entry" }, { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml#/$defs/namespaced", "description": "A namespaced custom kind.", "example": "acme.icon-library" } ] }, "id": { "type": "string", "description": "This entry's unique id in the design system graph.", "$comment": "Every ref that points at it uses this. A token entry's id uses a looser pattern (see entries/token.schema.yaml). Every other kind, including a namespaced custom kind, uses common/id.schema.yaml's standard pattern - enforced by the `if`/`else` at the top of this file, not here.", "example": "button" }, "name": { "type": "string", "description": "The human-readable display name.", "$comment": "Kept separate from `id`, which is machine-readable.", "example": "Button" }, "description": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "What this entry is, and why it exists. Lead with what it is, in one sentence.", "$comment": "The one place an entry says why it exists. 0.22.0 folded the separate `purpose` field and `metadata.context` into it, since all three answered the same question. Keep it short: the deeper explanation belongs in `sections`. Markdown since 0.23.0, so it can link to another entry with a `dsds:` link. A consumer that shows it on one line, such as in a table or a search result, shows each link as its text.", "example": "An interactive element that triggers an action. Gives users one consistent way to act across the product." }, "metadata": { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/entry-metadata.schema.yaml" }, "sections": { "type": "array", "minItems": 1, "description": "Every documentation section for this entry.", "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/section.schema.yaml#/$defs/dispatch" } }, "uses": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "Entries this one is built from, needs, or follows (uses). No `rel`: this field says it.", "$comment": "A dialog uses its button; a button uses a color token, and the pattern it follows. Read from the target, it's \"used by\": what's affected if that target changes. A consumer follows it as a chain (processing model section 9).", "example": [ { "to": "color.action.primary" } ] }, "extends": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "The entry this one inherits from (extends). No `rel`: this field says it.", "$comment": "A consumer never merges the extended entry's content into this one (processing model section 6).", "example": [ { "to": "base-dialog" } ] }, "alternatives": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "Entries to use instead of this one, in some situation (alternative-to). No `rel`: this field says it.", "$comment": "Say when in a `note`, or in a `when-to-use` guideline that points at the alternative.", "example": [ { "to": "link" } ] }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Every other pointer from this entry, to entries and outside resources, each with its own `rel`.", "example": [ { "href": "https://github.com/org/ds/react/button", "rel": "source" } ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml" } }, "$defs": { "dispatch": { "description": "Routes an entry to its own kind-specific schema by `kind` (`system`, `component`, `token`, `theme`), falling back to this open base for the generic `entry` kind or a namespaced custom kind.", "$comment": "Routes an entry to its own entries/.schema.yaml by `kind`, falling back to this file (the open base) for the generic `entry` kind or a namespaced custom kind with no dedicated file. Used anywhere an entry is embedded (base.schema.yaml's own `entries`) instead of a bare $ref to this file, so the bundled schema enforces the same per-kind rules scripts/validate/validate.js does in JS.", "if": { "required": [ "kind" ], "properties": { "kind": { "const": "component" } } }, "then": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/component.schema.yaml" }, "else": { "if": { "required": [ "kind" ], "properties": { "kind": { "const": "token" } } }, "then": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/token.schema.yaml" }, "else": { "if": { "required": [ "kind" ], "properties": { "kind": { "const": "theme" } } }, "then": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/theme.schema.yaml" }, "else": { "if": { "required": [ "kind" ], "properties": { "kind": { "const": "system" } } }, "then": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/system.schema.yaml" }, "else": { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/entry.schema.yaml" } } } } } } }, "entries/system": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/entries/system.schema.yaml", "title": "SystemEntry", "description": "A DSDS design system. System-level information and guidance for the design system as a whole.", "$comment": "Does not contain entries. A system points at its own components/tokens/etc with `refs` instead. It can reference other systems the same way to state a relationship (sibling, child, parent).", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/entry.schema.yaml" }, { "type": "object", "properties": { "kind": { "const": "system", "description": "Marks this entry as a design system." }, "metadata": { "description": "Facts about the design system as a whole.", "$comment": "Uses the same structure as every other entry's `metadata` (entry-metadata.schema.yaml), plus fields that only make sense for a whole system: `version`, `organization`, `url`, `license`, `platforms` (system-metadata.schema.yaml).", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/entry-metadata.schema.yaml" }, { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/system-metadata.schema.yaml" } ], "unevaluatedProperties": false } } } ], "unevaluatedProperties": false }, "entries/theme": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/entries/theme.schema.yaml", "title": "ThemeEntry", "description": "A defined system theme.", "$comment": "This entry doesn't list which tokens the theme overrides. `source` points to the DTCG JSON file which acts as the source of truth.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/entry.schema.yaml" }, { "type": "object", "properties": { "kind": { "const": "theme", "description": "Marks this entry as a theme." }, "metadata": { "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/entry-metadata.schema.yaml" } ], "unevaluatedProperties": false }, "colorScheme": { "type": "string", "enum": [ "light", "dark" ], "default": "light", "description": "Which native color-scheme setting this theme matches." }, "source": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/target", "description": "The theme's DTCG source file (rel: source). No `rel`: this field says it.", "$comment": "A bare string is a plain file path (shorthand for href). Use the object form when role or note matters.", "example": "tokens/dark.tokens.json" } } } ], "unevaluatedProperties": false }, "entries/token": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/entries/token.schema.yaml", "title": "TokenEntry", "$comment": "Never stores the token's real value or type. `source` points to the DTCG JSON file which acts as the source of truth. This entry describes what the token is, why is exists, and how to use it.", "description": "A single design token, from the Design Tokens Community Group (DTCG) format.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/entries/entry.schema.yaml" }, { "type": "object", "properties": { "kind": { "const": "token", "description": "Marks this entry as a token." }, "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml#/$defs/tokenId", "description": "This token's unique id.", "$comment": "Looser than the standard id pattern. Allows slash separators too, to fit however a token layer or DTCG source already names things.", "example": "color.action.primary" }, "metadata": { "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/entry-metadata.schema.yaml" } ], "unevaluatedProperties": false }, "tokenType": { "type": "string", "pattern": "^[a-z][a-zA-Z0-9]*$", "description": "The token's type, from DTCG.", "$comment": "Optional - a token can inherit its type from its `metadata.group` instead of stating its own. Left open as a pattern instead of a fixed list. Well-known values as of this writing: color, dimension, fontFamily, fontWeight, duration, cubicBezier, number, shadow.", "example": "color" }, "source": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/target", "description": "The token's DTCG source file (rel: source). No `rel`: this field says it.", "$comment": "A bare string is a plain file path (shorthand for href). Use the object form when role or note matters. To point at one token in a shared file, add a JSON Pointer: `./tokens.dtcg.json#/color/action/primary`.", "example": "./tokens.dtcg.json" }, "combos": { "type": "array", "minItems": 1, "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/combo.schema.yaml" }, "description": "Rules about which other tokens this one must or must never be paired with.", "example": [ { "subject": "{color.action.primary}", "level": "must", "items": [ "{color.surface.default}", "{color.surface.raised}" ] } ] } } } ], "unevaluatedProperties": false }, "metadata/entry-metadata": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/metadata/entry-metadata.schema.yaml", "title": "EntryMetadata", "description": "Information about a single entry, on top of the fields every metadata object shares.", "$comment": "Adds fields that only make sense per-entry: `status`, `since`, `group`, `aliases`, `preview`. A system's own metadata adds a different set instead - see metadata/system-metadata.schema.yaml.\nDeliberately left open (no `unevaluatedProperties`) - a system entry needs this schema's fields AND system-metadata.schema.yaml's own fields to both apply to the same `metadata` object at once, so only the combined usage (entries/system.schema.yaml's own `metadata` property) can correctly close the union. Every other kind file (component/token/theme) closes its own `metadata` property locally instead, since only this schema applies there.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/metadata.schema.yaml" }, { "type": "object", "properties": { "preview": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/showcase.schema.yaml", "description": "A visual sample of this entry, either a media file or a link.", "$comment": "For a code sample, use a section's own examples instead, which can point at a real file or story." }, "since": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/since.schema.yaml", "description": "The version this entry was first introduced." }, "status": { "$ref": "#/$defs/status" }, "group": { "type": "string", "description": "A group name this entry belongs to, for example \"color.action\" on a set of related tokens, or \"action\" on a family of related components.", "$comment": "Any entry can be grouped. Components, tokens, themes, etc. This property is used for simple organization and categorization.", "example": "color.action" }, "aliases": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "description": "Other names this entry is also known or searched by, like a past name or a common misspelling.", "example": [ "btn" ] } } } ], "$defs": { "status": { "description": "A lifecycle status, optionally scoped to one platform - or a list of them, one per platform, when an entry has reached different maturity on each.", "$comment": "There is deliberately no `overall` field. Entry's overall status should be based on the aggregate of all platform status entries. How to aggregate is the consumer's call. A bare object with no `platform` is the entry's overall status. A component's trait and trait value use this same shape.", "oneOf": [ { "$ref": "#/$defs/statusEntry" }, { "type": "array", "minItems": 1, "description": "One status per platform.", "items": { "$ref": "#/$defs/statusEntry" } } ] }, "statusEntry": { "description": "One lifecycle status, optionally scoped to a single platform.", "$comment": "Factored out of `status` so the single-object and one-per-platform list forms enforce exactly the same rules, including the deprecated-needs-a-notice conditional below.", "allOf": [ { "type": "object", "required": [ "status" ], "properties": { "platform": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "Which platform this status applies to.", "$comment": "This field isn't necessary if only one platform exists. It is required in practice for the list form to mean anything - two entries with no `platform` say the same thing twice.", "example": "react" }, "status": { "$ref": "#/$defs/statusValue" }, "since": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/since.schema.yaml", "description": "The version this platform (or the system overall) reached this status." }, "deprecationNotice": { "type": "string", "description": "What to use instead, and why.", "$comment": "Required when `status` is deprecated.", "example": "Use `icon-button` instead - this variant never got contrast-tested and is being removed in 2.0." }, "replacedBy": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "What replaces this, when `status` is deprecated (replaced-by). No `rel`: this field says it.", "$comment": "Sits next to `deprecationNotice`, where a reader finds out the thing is going away. Point at an entry (`icon-button`), or one trait value (`button#variant.primary`). Several successors means several pointers, and `deprecationNotice` says when to choose which. In one platform's status, it applies on that platform. Read from the target, it's \"replaces\".", "example": [ { "to": "button#variant.primary" } ] }, "note": { "type": "string", "description": "Free-text context.", "example": "Still supported for legacy integrations only." } }, "additionalProperties": false }, { "if": { "required": [ "status" ], "properties": { "status": { "const": "deprecated" } } }, "then": { "required": [ "deprecationNotice" ] } }, { "if": { "required": [ "replacedBy" ] }, "then": { "properties": { "status": { "const": "deprecated" } } } } ] }, "statusValue": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "examples": [ "experimental", "stable", "deprecated", "beta", "planned" ], "description": "A lowercase, dash-separated lifecycle word, for example \"stable\" or \"deprecated\".", "$comment": "This enum isn't closed. Any term is allowed." } } }, "metadata/metadata": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/metadata/metadata.schema.yaml", "title": "Metadata", "type": "object", "description": "Optional information about an element.", "$comment": "The shared foundation every entry's metadata builds on. Fields only meaningful for one kind live in a per-kind extension instead - entry-metadata.schema.yaml (every entry) or system-metadata.schema.yaml (a `kind: system` entry only).", "$defs": { "isoDate": { "type": "string", "format": "date", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "An ISO 8601 date (YYYY-MM-DD).", "example": "2026-06-02" }, "note": { "type": "string", "description": "A plain-text note. MUST NOT contain markup.", "example": "Reviewed against the latest Figma file; no changes needed." } }, "properties": { "origin": { "type": "object", "description": "How this entry's documentation came to exist, and who or what wrote it.", "$comment": "`method` and `author` both default to the common case (authored, by a human). `method` says how the documentation came to exist; `author` says how much of it a person wrote. A one-off exception, like \"the accessibility section was reviewed by a person, unlike the rest,\" belongs as a `note`.", "properties": { "method": { "type": "string", "enum": [ "authored", "generated", "extracted", "reconstructed" ], "default": "authored", "description": "How this documentation as a whole came to exist." }, "author": { "type": "string", "enum": [ "human", "assisted", "generated" ], "default": "human", "description": "How much of the content a person wrote.", "$comment": "`human`: a person wrote it. `assisted`: a person wrote it with help from a tool or model, and checked the result. `generated`: a tool or model wrote it, and no person has rewritten it. Whether that tool was an AI model or a script is a `method` question (`extracted` is a script reading a source; `generated` is anything else) and can be spelled out in `note`." }, "note": { "type": "string", "description": "Free-text context about the origin, including any exceptions to `method` or `author`.", "example": "The accessibility section was reviewed by a person, unlike the rest." } } }, "owner": { "type": "string", "description": "The owning team, role, or group.", "$comment": "Follows mailbox standard (RFC 5322).", "example": "ds@pizzapartysupertime.com" }, "updated": { "type": "object", "description": "A conforming consumer SHOULD treat `updated.date` as the cache key for this item's documentation specifically. `updated.date` is distinct from `metadata.version`/`since` which tracks the design system's own release. Always use `updated.date` as the source of truth for when to re-fetch or re-index.", "$comment": "A bare date, or a date with a change note.", "properties": { "date": { "$ref": "#/$defs/isoDate", "description": "The date of the change." }, "note": { "type": "string", "description": "A plain-text note on what changed.", "example": "Added the loading trait and its guideline." } } }, "reviewed": { "type": "array", "minItems": 1, "description": "Independent reviews confirming this item's documentation, each recording who confirmed it and when.", "$comment": "A list of review events so human sign-offs and automated checks can both be recorded independently.", "items": { "type": "object", "properties": { "date": { "$ref": "#/$defs/isoDate", "description": "The date of the review." }, "by": { "type": "string", "description": "Who or what performed the review.", "$comment": "Follows the actor convention: `human:` for a person, `/` for an agent or tool, `process:` for an automated process. Adapted from the Open Knowledge Format (OKF) spec.", "example": "human:ahormati" }, "note": { "type": "string", "description": "A plain-text note on the review outcome.", "example": "Copy and contrast ratios re-checked; no changes needed." } } } }, "tags": { "type": "array", "description": "Keywords for grouping, search, and filtering. By convention, the first tag, if any, is the main category.", "$comment": "The first tag can be used as a category.", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "example": [ "actions", "button", "cta", "form-control" ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool data scoped to just this entry's metadata, keyed by namespace." } } }, "metadata/system-metadata": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/metadata/system-metadata.schema.yaml", "title": "SystemMetadata", "description": "Information about the design system as a whole, on top of the fields every metadata object shares.", "$comment": "Adds fields specifically for the system. See metadata/entry-metadata.schema.yaml for the per-entry equivalent.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/metadata.schema.yaml" }, { "type": "object", "properties": { "organization": { "type": "string", "description": "The team or company that owns the design system.", "example": "Acme Inc." }, "url": { "type": "string", "format": "uri", "description": "The main home page or repository for the design system.", "example": "https://github.com/acme/design-system" }, "version": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/since.schema.yaml", "description": "The current version of the design system." }, "platforms": { "type": "array", "description": "The platforms this system ships on, for example \"react\" or \"web-component\". When this list is present, every other `platform` value used anywhere in the document MUST match one of these.", "minItems": 1, "uniqueItems": true, "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml" }, "example": [ "react", "web-component", "ios" ] }, "license": { "type": "string", "description": "The license this design system is published under.", "example": "MIT" }, "sourceRoot": { "type": "string", "format": "uri-reference", "description": "Where relative `sourceFiles` paths start from, when the code lives somewhere other than these documents. A path is relative to the document that holds this system entry; a URL is used as is. Every relative `sourceFiles` path in the project MUST be read from here when it's set (DSDS-11).", "$comment": "For docs kept apart from the code they describe, such as a docs repo next to a code repo. Applies to `sourceFiles` only: a token's `source` and a `rel: source` ref still start from their own document. Without it, a `sourceFiles` path starts from its own document's folder.", "example": "../ui" } } } ] }, "sections/definitions": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/sections/definitions.schema.yaml", "title": "DefinitionsSection", "description": "Term definitions. Can describe content labels within components or patterns, define naming conventions, act as a glossary, or be a simple way to outline component props/APIs.", "$comment": "A `definitions` section on an entry defines that entry's own terms. Distinct from `metadata.tags` or any other classification label.\n`definitions` does several genuinely different jobs - a component's anatomy, a glossary, a prop list stand-in, keyboard interactions, event names - told apart only by `title` until `context` existed. `context` (inherited from section.schema.yaml - see its own $comment) makes the job machine-readable: a renderer can find \"the anatomy table\" by checking `context: anatomy` instead of matching on a human-written heading string, which drifts and gets localized. `title` still carries the human-facing heading; `context` is what a tool reads.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/section.schema.yaml" }, { "type": "object", "properties": { "kind": { "const": "definitions", "description": "Marks this section as term-definition documentation." }, "items": { "type": "array", "minItems": 1, "description": "One entry per term.", "items": { "type": "object", "if": { "not": { "required": [ "refs" ] } }, "then": { "required": [ "term", "definition" ] }, "properties": { "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "An optional stable id, so a ref or a same-as pointer can address this term directly. `id` is optional, but a conforming consumer MUST derive one from this item's own text.", "$comment": "The derivation algorithm is in common/id.schema.yaml's own description." }, "term": { "type": "string", "description": "The subject of the definition.", "example": "OK" }, "definition": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "What the term means.", "example": "To confirm an action." }, "usage": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "When and how to use it, with formatting notes.", "example": "Use as the confirming action's label in a destructive-action dialog." }, "aliases": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "description": "Related terms often confused with this one.", "example": "Confirm" } }, "uses": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "Entries this item relates to, like the components a step involves (uses).", "example": [ { "to": "button" } ] }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Pointers from this term, including `rel: same-as` to borrow a term declared once, in `base.shared` or in any entry.", "example": [ { "to": "shared-terms#ok", "rel": "same-as" } ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool data scoped to just this one term, keyed by namespace." } }, "additionalProperties": false } } } } ], "unevaluatedProperties": false }, "sections/guidelines": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/sections/guidelines.schema.yaml", "title": "GuidelinesSection", "description": "Rules for an entry. When and how to use it. Each item pairs a rule with why it exists. 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. An author who expects a person to read the guideline without following the link SHOULD add a `statement` instead.", "$comment": "The same structure covers both a how-to-build rule and a when-to-use judgment. Only `framing` is different (`framing: when-to-use` for a judgment). Named `framing`, not `context`, to stay clear of section.schema.yaml's own `context` field (what job this section is doing) - this one is narrower, just how-to-use vs. when-to-use.\n`statement` is required unless the item points somewhere else instead: a `same-as` ref for a statement already declared once elsewhere (a shared item, or an item in another entry), or a `relates-to` ref with an `href` for a rule tracked in another system, like a ticket. Both go in `refs`.\n`alternatives`, `evidence`, `checks` and `refines` are each named for one kind of pointer, so a pointer in them has no `rel` - the field says it (`alternative-to`, `cites`, `checked-by`, `refines`). A tool asking \"what backs up this guideline\" reads `evidence`. `refs` is the catch-all for every other kind, each with its own `rel`.\n`checkedBy` says what type of verification is used, not what does the checking. `checks` points at the actual test, lint rule, agent eval, or checklist step. `example` always matches `level`, so there's no need for a separate pass/fail flag.\nAn item with no `statement` and a `relates-to` link (just `level` plus the `refs` entry) is intentional, not a gap: it says \"this rule exists, at this strictness, tracked in another system\" without restating text that lives there. It's unreadable on its own - a reader will have to follow the link to access the rule.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/section.schema.yaml" }, { "type": "object", "properties": { "kind": { "const": "guidelines", "description": "Marks this section as guidelines." }, "framing": { "type": "string", "enum": [ "when-to-use", "how-to-use" ], "default": "how-to-use", "description": "Which kind of guidance this section holds.", "$comment": "when-to-use is a fit judgment: whether, or when, this entry is the right choice at all. how-to-use is an implementation rule: how to apply or use it correctly, once chosen." }, "items": { "type": "array", "minItems": 1, "description": "The rules, in order.", "items": { "type": "object", "allOf": [ { "anyOf": [ { "required": [ "statement" ] }, { "required": [ "refs" ], "properties": { "refs": { "contains": { "anyOf": [ { "type": "object", "required": [ "rel", "to" ], "properties": { "rel": { "const": "same-as" } } }, { "type": "object", "required": [ "rel", "href" ], "properties": { "rel": { "const": "relates-to" } } } ] } } } } ] }, { "if": { "not": { "required": [ "refs" ] } }, "then": { "required": [ "level" ] } } ], "properties": { "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "A stable id for this guideline, so another item, like an agent-only refinement or a checklist entry, can point at it. `id` is optional, but a conforming consumer MUST derive one from this item's own text.", "$comment": "The derivation algorithm is in common/id.schema.yaml's own description." }, "level": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/requirement-level.schema.yaml" }, "statement": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "An individual, concrete usage rule.", "$comment": "When the section's framing is when-to-use, this states a user situation. Leave it out only when `refs` carries a same-as pointer, or a relates-to link, instead (see the file comment above).", "example": "Limit each surface to one primary button." }, "example": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/example.schema.yaml", "description": "An example illustrating this rule. It MUST reflect `level`. A `must-not` item's example MUST show what not to do.", "$comment": "Matching `level` is why there's no separate pass/fail flag on an example." }, "alternatives": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "What to use instead, when this is a fit judgment (alternative-to). No `rel`: this field says it.", "example": [ { "to": "link" } ] }, "evidence": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "The outside standard this rule follows, like a WCAG criterion or MDN (cites). No `rel`: this field says it.", "example": [ { "href": "https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html", "role": "WCAG 2.2 SC 2.4.7 (AA)" } ] }, "checks": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "What verifies this rule (checked-by): a test, lint rule or agent eval (an `href`, with `role` saying which), or a checklist step (a `to`). No `rel`: this field says it.", "example": [ { "href": "./tests/button.a11y.test.ts", "role": "test" } ] }, "checkedBy": { "type": "string", "enum": [ "automated", "assisted", "manual" ], "description": "How this rule is verified.", "$comment": "`automated`: a tool checks this on its own. Point `checks` at the real test, lint rule or agent eval (ex: a promptfoo config) doing the checking (DSDS-03).\n`assisted`: a tool flags likely cases, but a person makes the call.\n`manual`: a person checks this by hand, no tool involved. Leave this out if you don't know yet how it'll be checked." }, "tags": { "type": "array", "description": "Freeform keywords that relate guidelines across categories.", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "example": [ "accessibility", "focus" ] }, "refines": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "On a `for: agent` item: the human-facing rule this one sharpens for an agent (refines). No `rel`: this field says it.", "$comment": "Written on the agent rule, pointing at the human one, so the human-facing rule never has to know it's been refined. A consumer reading for an agent applies both.", "example": [ { "to": "button#loading-announcement" } ] }, "uses": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "Entries this item is about or needs, like the components a step involves (uses). No `rel`: this field says it.", "example": [ { "to": "button" } ] }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Every other pointer from this guideline, each with its own `rel`.", "$comment": "Includes rel: same-as, and rel: relates-to with an href, which point at where the rule is really stated instead of here (see `statement`'s own $comment).", "example": [ { "to": "shared-a11y#focus-visible", "rel": "same-as" } ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool data scoped to just this one guideline, keyed by namespace.", "$comment": "A custom `kind` (ex: \"acme.rule\") can add its own typed fields directly. A well-known kind like `guidelines` can't — its items are closed so every tool that understands `guidelines` keeps understanding it — so this is the one way to attach org-specific data (a rationale, a failure mode, an internal ticket) to a single item without forking the kind." } }, "additionalProperties": false } } } } ], "unevaluatedProperties": false }, "sections/section": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/sections/section.schema.yaml", "title": "Section", "type": "object", "description": "A logical documentation section. Supplies the fields every section kind shares, in declaration order — `kind`, `for`, `title`, `description`, `context`, `tags`, `items`, `freeform`, `$extensions` — and is tagged with who it is for (human, agent, or all). Content always lives in `items`, never in a field named after the kind. Each sections/.schema.yaml file adds its own `kind` value (`definitions`, `guidelines`, `steps`, or the generic `section`) and its own structure for `items` on top of this shared base.", "$comment": "`freeform` lives here on the base so every kind can carry nested, headed content alongside its own structured `items`. A `guidelines` section can pair its rules with background reading, a `steps` section can pair its checklist with extra context, and so on. A section should use either `items` or `freeform`.A section that's only `freeform` content (this used to be its own `kind: freeform`) picks whichever kind fits it best. If nothing fits, use the generic `kind: section` instead.\nThis file doesn't close its own list of properties. Each sections/.schema.yaml file adds its own extra fields on top (steps' `ordered`, definitions' term/definition items), and closes the combined structure itself, at the item level.", "required": [ "kind", "for" ], "anyOf": [ { "required": [ "items" ] }, { "required": [ "freeform" ] } ], "properties": { "kind": { "description": "What kind of content section this is.", "$comment": "One of the 3 specific content patterns, the generic `section`, or a namespaced custom kind, like \"acme.custom-section\".", "oneOf": [ { "type": "string", "enum": [ "guidelines", "definitions", "steps", "section" ], "default": "section" }, { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml#/$defs/namespaced", "description": "A namespaced custom kind.", "example": "acme.custom-section" } ] }, "for": { "type": "string", "enum": [ "all", "human", "agent" ], "default": "all", "description": "Who or what this section is written for." }, "title": { "type": "string", "description": "An optional heading for the section.", "example": "When to use" }, "description": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "An optional one-line intro for the section.", "example": "Rules for using this component correctly." }, "context": { "description": "What job this section is doing.", "$comment": "Open, the same way `entry.kind`/`section.kind` are: one of the well-known values below, or a namespaced custom one (ex: \"acme.slots\"). Optional - a section with no stated `context` is just its kind's default reading. Lives on the shared base, not a single kind's file, since more than one kind can use it: a `definitions` section's anatomy table vs. glossary vs. prop list is the original case, but any section kind can be doing a distinguishable job its `title` alone doesn't make machine-readable.", "oneOf": [ { "type": "string", "enum": [ "anatomy", "terms", "keyboard", "events" ] }, { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml#/$defs/namespaced", "description": "A namespaced custom value.", "example": "acme.slots" } ], "example": "anatomy" }, "tags": { "type": "array", "description": "What this section is about. A section tagged `accessibility` holds the accessibility rules for its entry, and nothing else.", "$comment": "Section-level, as opposed to the same-named field on a `guidelines` item. An item's tags relate one rule across categories; these say what the whole section covers. The first tag is the primary one, matching `metadata.tags`'s own convention.", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "example": [ "accessibility" ] }, "items": { "type": "array", "description": "The one universal list for this section kind's own structured content.", "$comment": "Never under a kind-specific key like parts, steps, or properties.", "items": { "type": "object" } }, "freeform": { "type": "array", "minItems": 1, "description": "Nestable written content that can include headings. Available on every section kind regardless of `items`' own structure.", "$comment": "See this file's own $comment above for why this lives on the base instead of its own section kind.", "items": { "$ref": "#/$defs/freeformEntry" }, "example": [ { "title": "Install", "body": "Add the package and its peer dependencies." } ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool data scoped to just this one section, keyed by namespace.", "$comment": "A tool that only cares about one section, like an api section, can stash its data here instead of using the whole entry's escape hatch." } }, "$defs": { "dispatch": { "description": "Routes a section to its own kind-specific schema by `kind` (`definitions`, `guidelines`, `steps`), falling back to this open base for the generic `section` kind or a namespaced custom kind.", "$comment": "Routes a section to its own sections/.schema.yaml by `kind`, falling back to this file (the open base) for the generic `section` kind or a namespaced custom kind with no dedicated file. Used anywhere a section is embedded (an entry's or a shared item's own `sections`) instead of a bare $ref to this file, so the bundled schema enforces the same per-kind rules scripts/validate/validate.js does in JS.", "if": { "required": [ "kind" ], "properties": { "kind": { "const": "definitions" } } }, "then": { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/definitions.schema.yaml" }, "else": { "if": { "required": [ "kind" ], "properties": { "kind": { "const": "guidelines" } } }, "then": { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/guidelines.schema.yaml" }, "else": { "if": { "required": [ "kind" ], "properties": { "kind": { "const": "steps" } } }, "then": { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/steps.schema.yaml" }, "else": { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/section.schema.yaml" } } } }, "freeformEntry": { "type": "object", "required": [ "title" ], "description": "One nestable, headed block of prose - a freeform section's own building block.", "properties": { "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "A stable id for linking to this entry directly, unique within the section. `id` is optional, but conforming consumer MUST derive one from this item's own text.", "$comment": "The derivation algorithm is in common/id.schema.yaml's own description.", "example": "install" }, "title": { "type": "string", "description": "The entry's heading, for example 'Installation'.", "example": "Installation" }, "body": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "The entry's content.", "$comment": "Can be left out when the entry only groups sub-entries.", "example": "Add the package and its peer dependencies." }, "examples": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/example.schema.yaml#/$defs/list" }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "\"See also\" pointers for this entry. To point at another entry, use the entry's own top-level `refs` instead.", "example": [ { "to": "button", "rel": "relates-to" } ] }, "items": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/freeformEntry" }, "description": "Sub-entries nested beneath this one, to any depth." }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool data scoped to just this one freeform entry, keyed by namespace." } }, "additionalProperties": false } } }, "sections/steps": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/sections/steps.schema.yaml", "title": "StepsSection", "description": "A series of actions/steps/tasks, like a tutorial, a migration, a pattern's interaction flow, or checklist of things to verify.", "$comment": "Useful whenever documenting a multi-phase process of job to be done.", "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/section.schema.yaml" }, { "type": "object", "properties": { "kind": { "const": "steps", "description": "Marks this section as a stepped process or checklist." }, "ordered": { "type": "boolean", "default": true, "description": "Whether entries must be done in order. Set to false for an unordered checklist." }, "items": { "type": "array", "description": "The steps or checklist entries, in order.", "minItems": 1, "items": { "type": "object", "if": { "not": { "required": [ "refs" ] } }, "then": { "required": [ "title" ] }, "properties": { "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "An optional stable id, so a ref or a same-as pointer can address this step directly. `id` is optional, but a conforming consumer MUST derive one from this item's own text.", "$comment": "The derivation algorithm is in common/id.schema.yaml's own description." }, "optional": { "type": "boolean", "default": false, "description": "Whether this entry can be skipped without breaking the procedure or checklist." }, "title": { "type": "string", "description": "A short heading for this entry, for example 'Install the package' or 'Focus ring is visible'.", "example": "Install the package" }, "description": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "What to do, what happens, and what success looks like.", "example": "Run `npm install @org/ds-react`." }, "examples": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/example.schema.yaml#/$defs/list", "description": "A screenshot, recording, or live URL. For code, point at a file or story through the example's `ref` instead." }, "uses": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/targets", "description": "Entries this item is about or needs, like the components a step involves (uses). No `rel`: this field says it.", "example": [ { "to": "button" } ] }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Every other pointer from this step, each with its own `rel`. To say a step verifies a guideline, point the guideline's `checks` at the step instead.", "example": [ { "href": "./install.sh", "rel": "file" } ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool data scoped to just this one step, keyed by namespace." } }, "additionalProperties": false } } } } ], "unevaluatedProperties": false }, "shared": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://designsystemdocspec.org/v0.23.0/shared.schema.yaml", "title": "Shared", "description": "Reusable content other entries point at instead of restating. Not a design-system artifact in its own right.", "$comment": "Declares its own structure rather than extending `entry.schema.yaml` via `allOf`, even though the fields look similar. A shared item deliberately has no `kind`, `extends`, or `related`.", "type": "object", "required": [ "id", "name", "description" ], "properties": { "id": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/id.schema.yaml", "description": "This entry's unique id.", "$comment": "A ref addresses it by this id, either directly or as the entryId half of an entryId#itemId address.", "example": "shared-a11y" }, "name": { "type": "string", "description": "The human-readable display name.", "$comment": "Kept separate from `id`, which is machine-readable.", "example": "Shared accessibility rules" }, "description": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/markdown.schema.yaml", "description": "A one-line statement of what this shared entry is for.", "example": "Accessibility rules that apply broadly across components." }, "metadata": { "allOf": [ { "$ref": "https://designsystemdocspec.org/v0.23.0/metadata/metadata.schema.yaml" } ], "unevaluatedProperties": false }, "sections": { "type": "array", "minItems": 1, "description": "The reusable content itself, in the same section structure an entry uses.", "items": { "$ref": "https://designsystemdocspec.org/v0.23.0/sections/section.schema.yaml#/$defs/dispatch" } }, "refs": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/ref.schema.yaml#/$defs/list", "description": "Pointers from this shared entry to entries and outside resources.", "example": [ { "href": "https://www.w3.org/WAI/WCAG21/quickref/", "rel": "relates-to" } ] }, "$extensions": { "$ref": "https://designsystemdocspec.org/v0.23.0/common/extensions.schema.yaml", "description": "Escape hatch for tool data or an outside id, the same structure as an entry's own $extensions." } }, "additionalProperties": false } } } ```