diff --git a/README.md b/README.md index a317a72..6a45eb0 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Think of it as OpenAPI for design systems. --- -> **Status: v0.3 draft available.** +> **Status: v0.4 draft available.** > -> The current draft is [`spec/dspack-v0.3.md`](./spec/dspack-v0.3.md) (written as a delta over [v0.2](./spec/dspack-v0.2.md)), with a matching [JSON Schema](./schema/dspack.v0.3.schema.json), a companion [surface schema](./schema/dspack.surface.v0_1.schema.json), and a [shadcn/ui reference example](./examples/shadcn-ui.dspack.json). v0.3 adds the machine-checkable **governance blocks** — `intents`, `rules`, `examples` — and is strictly additive: a valid v0.2 document with `"dspack": "0.3"` validates against the v0.3 schema (see the [migration guide](./spec/migration-v0.2-to-v0.3.md)). Earlier specs and schemas ([v0.2](./spec/dspack-v0.2.md), [v0.1](./spec/dspack-v0.1.md)) are preserved for reference. This is a draft — breaking changes may occur before v1.0. Contributions to the design are welcome at any stage. +> The current draft is [`spec/dspack-v0.4.md`](./spec/dspack-v0.4.md) (written as a delta over [v0.3](./spec/dspack-v0.3.md)), with a matching [JSON Schema](./schema/dspack.v0.4.schema.json), a companion [surface schema](./schema/dspack.surface.v0_1.schema.json), and a [shadcn/ui reference example](./examples/shadcn-ui.dspack.json). v0.4 adds **component categories** (a contract-defined registry that rules can select by) and the **`required-props` rule type** ("this component must carry named content directly" — the refinement driven by measured projection-gap failures), and is strictly additive: a valid v0.3 document with `"dspack": "0.4"` validates against the v0.4 schema (see the [migration guide](./spec/migration-v0.3-to-v0.4.md)). Earlier specs and schemas ([v0.3](./spec/dspack-v0.3.md), [v0.2](./spec/dspack-v0.2.md), [v0.1](./spec/dspack-v0.1.md)) are preserved for reference. This is a draft — breaking changes may occur before v1.0. Contributions to the design are welcome at any stage. --- @@ -121,6 +121,7 @@ The following milestones represent the current intended direction. They are not | **shadcn/ui example dspack** | A reference dspack file for the [shadcn/ui](https://ui.shadcn.com) component library — _[available](./examples/shadcn-ui.dspack.json)_ | | **v0.2 spec draft** | Adds structured generation constraints: lifecycle status, accessibility, composition rules, contextual constraints, variant semantics, token hierarchy, themes, layout primitives, and anti-pattern severity — _[available](./spec/dspack-v0.2.md)_ | | **v0.3 spec draft** | Adds the machine-checkable governance blocks: named intents, typed deterministic rules with rationales, compilable examples, and the companion dspack surface format — _[available](./spec/dspack-v0.3.md)_ | +| **v0.4 spec draft** | Adds component categories (contract-defined registry, category-based rule selection) and the `required-props` rule type, both driven by measured pipeline evidence — _[available](./spec/dspack-v0.4.md)_ | | **ds-mcp v0 release** | First release of the reference implementation, validated against the v0.2 spec — _[available](https://github.com/aestheticfunction/ds-mcp)_ | | **Community RFCs** | Open RFC process for proposing additions and changes to the spec | | **v1.0 spec stabilization** | First stable, versioned release of the specification; breaking changes require a formal process after this point | diff --git a/examples/README.md b/examples/README.md index 2f41f60..1f711d2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,4 +6,4 @@ Example dspack files live in this directory. Examples are intended to help imple - [`shadcn-ui.dspack.json`](./shadcn-ui.dspack.json) — a reference dspack file for the [shadcn/ui](https://ui.shadcn.com) component library -The shadcn/ui example demonstrates all five top-level concepts (tokens, components, patterns, anti-patterns, framework bindings) and validates against the [v0.1 JSON Schema](../schema/dspack.v0.1.schema.json). +The shadcn/ui example demonstrates the corpus concepts (tokens, components, patterns, anti-patterns, framework bindings), the v0.3 governance blocks (intents, rules, examples), and the v0.4 additions (component categories, a `required-props` rule, a category-based `forbidden-composition` rule). It validates against the [v0.4 JSON Schema](../schema/dspack.v0.4.schema.json). diff --git a/examples/shadcn-ui.dspack.json b/examples/shadcn-ui.dspack.json index 535356c..2de7105 100644 --- a/examples/shadcn-ui.dspack.json +++ b/examples/shadcn-ui.dspack.json @@ -1,9 +1,9 @@ { - "$schema": "../schema/dspack.v0.3.schema.json", - "dspack": "0.3", + "$schema": "../schema/dspack.v0.4.schema.json", + "dspack": "0.4", "name": "shadcn/ui", "description": "A collection of reusable components built with Radix UI and Tailwind CSS. Components are copied into your project, not installed as a dependency.", - "version": "2.0.0", + "version": "2.1.0", "metadata": { "source": "https://ui.shadcn.com", "license": "MIT" @@ -311,7 +311,8 @@ ], "tokens": ["primary", "primary-foreground", "secondary", "secondary-foreground", "destructive", "destructive-foreground", "border", "ring", "radius"], "relatedComponents": ["toggle"], - "tags": ["interactive", "form", "action"] + "tags": ["interactive", "form", "action"], + "categories": ["interactive"] }, "alert-dialog": { "name": "Alert Dialog", @@ -360,7 +361,8 @@ "description": "The element that opens the alert dialog.", "required": true, "slot": "trigger", - "acceptsChildren": "components" + "acceptsChildren": "components", + "categories": ["interactive"] }, { "id": "alert-dialog-content", @@ -403,14 +405,16 @@ "name": "AlertDialogAction", "description": "The button that confirms the destructive action.", "required": true, - "acceptsChildren": "text" + "acceptsChildren": "text", + "categories": ["interactive"] }, { "id": "alert-dialog-cancel", "name": "AlertDialogCancel", "description": "The button that cancels and closes the dialog.", "required": true, - "acceptsChildren": "text" + "acceptsChildren": "text", + "categories": ["interactive"] } ], "notes": "AlertDialogContent must contain AlertDialogTitle and AlertDialogDescription for accessibility. AlertDialogAction and AlertDialogCancel must appear in AlertDialogFooter. Place cancel before confirm in reading order." @@ -434,7 +438,8 @@ ], "tokens": ["background", "foreground", "primary", "primary-foreground", "destructive", "destructive-foreground", "border", "radius"], "relatedComponents": ["dialog", "button"], - "tags": ["modal", "confirmation", "destructive", "accessibility"] + "tags": ["modal", "confirmation", "destructive", "accessibility"], + "categories": ["overlay"] }, "dialog": { "name": "Dialog", @@ -483,7 +488,8 @@ "name": "DialogTrigger", "description": "The element that opens the dialog.", "slot": "trigger", - "acceptsChildren": "components" + "acceptsChildren": "components", + "categories": ["interactive"] }, { "id": "dialog-content", @@ -524,7 +530,8 @@ "id": "dialog-close", "name": "DialogClose", "description": "A button that closes the dialog.", - "acceptsChildren": "text" + "acceptsChildren": "text", + "categories": ["interactive"] } ] }, @@ -542,7 +549,8 @@ ], "tokens": ["background", "foreground", "border", "radius"], "relatedComponents": ["alert-dialog"], - "tags": ["modal", "overlay", "form"] + "tags": ["modal", "overlay", "form"], + "categories": ["overlay"] }, "card": { "name": "Card", @@ -659,7 +667,8 @@ ], "tokens": ["background", "foreground", "border", "ring", "radius-sm"], "relatedComponents": [], - "tags": ["form", "input", "text"] + "tags": ["form", "input", "text"], + "categories": ["interactive"] }, "badge": { "name": "Badge", @@ -733,7 +742,8 @@ "description": "The element that opens the dropdown menu.", "required": true, "slot": "trigger", - "acceptsChildren": "components" + "acceptsChildren": "components", + "categories": ["interactive"] }, { "id": "dropdown-menu-content", @@ -747,13 +757,15 @@ "id": "dropdown-menu-item", "name": "DropdownMenuItem", "description": "A single actionable item in the menu.", - "acceptsChildren": "text" + "acceptsChildren": "text", + "categories": ["interactive"] }, { "id": "dropdown-menu-checkbox-item", "name": "DropdownMenuCheckboxItem", "description": "A menu item that can be toggled on/off.", - "acceptsChildren": "text" + "acceptsChildren": "text", + "categories": ["interactive"] }, { "id": "dropdown-menu-radio-group", @@ -765,7 +777,8 @@ "id": "dropdown-menu-radio-item", "name": "DropdownMenuRadioItem", "description": "A radio-style menu item for single selection within a group.", - "acceptsChildren": "text" + "acceptsChildren": "text", + "categories": ["interactive"] }, { "id": "dropdown-menu-label", @@ -795,7 +808,8 @@ "id": "dropdown-menu-sub-trigger", "name": "DropdownMenuSubTrigger", "description": "The item that opens a sub-menu on hover or keyboard navigation.", - "acceptsChildren": "text" + "acceptsChildren": "text", + "categories": ["interactive"] }, { "id": "dropdown-menu-sub-content", @@ -807,7 +821,8 @@ }, "tokens": ["background", "foreground", "accent", "muted", "muted-foreground", "border", "radius"], "relatedComponents": ["button"], - "tags": ["interactive", "menu", "overlay"] + "tags": ["interactive", "menu", "overlay"], + "categories": ["interactive", "overlay"] }, "table": { "name": "Table", @@ -931,6 +946,16 @@ "tags": ["data", "table", "tabular", "display"] } }, + "categories": { + "interactive": { + "name": "Interactive", + "description": "Receives pointer or keyboard activation: buttons, inputs, menu items, and trigger/action/cancel sub-components. Category-based composition rules select by this instead of enumerating ids." + }, + "overlay": { + "name": "Overlay", + "description": "Renders content in a layer above the page with its own focus and dismiss semantics: dialogs, alert dialogs, dropdown menus." + } + }, "patterns": [ { "id": "destructive-action-confirmation", @@ -1049,6 +1074,26 @@ "rationale": "Nested interactive elements create ambiguous click targets and are an accessibility violation: screen readers cannot determine intent and click handling varies across browsers.", "examples": ["ex.delete-account-confirmation"], "tags": ["accessibility", "interaction"] + }, + { + "id": "rule.trigger-carries-label", + "type": "required-props", + "severity": "must", + "component": "button", + "within": "alert-dialog-trigger", + "requiredText": true, + "rationale": "The trigger button must present its label as its own text. Assistive technology reads the control's accessible name from the element itself, and protocol projections lift a trigger label from the label-bearing component's direct text — a label nested in a child (e.g. a badge) or absent entirely produces a control with no accessible name and an instance downstream emitters must refuse.", + "examples": ["ex.delete-account-confirmation"], + "tags": ["accessibility", "projection"] + }, + { + "id": "rule.alertdialog-no-nested-overlays", + "type": "forbidden-composition", + "severity": "must", + "component": "alert-dialog", + "forbiddenCategories": ["overlay"], + "rationale": "An alert dialog is a single focused interruption. Stacking another overlay (dialog, dropdown menu, another alert dialog) inside it breaks focus containment and dismiss semantics and buries the confirmation decision under a second layer.", + "tags": ["accessibility", "interaction"] } ], "examples": [ diff --git a/fixtures/negative/component-unknown-category-ref.dspack.json b/fixtures/negative/component-unknown-category-ref.dspack.json new file mode 100644 index 0000000..473b63f --- /dev/null +++ b/fixtures/negative/component-unknown-category-ref.dspack.json @@ -0,0 +1,61 @@ +{ + "dspack": "0.4", + "name": "negative-fixture", + "categories": { + "interactive": { + "description": "Receives activation." + } + }, + "components": { + "button": { + "name": "Button", + "description": "A button.", + "categories": [ + "interactive", + "clickable" + ], + "props": { + "variant": { + "type": "enum", + "values": [ + "default", + "destructive" + ] + } + } + }, + "alert-dialog": { + "name": "AlertDialog", + "description": "A confirmation dialog.", + "composition": { + "subComponents": [ + { + "id": "alert-dialog-trigger", + "name": "AlertDialogTrigger", + "categories": [ + "interactive" + ] + }, + { + "id": "alert-dialog-title", + "name": "AlertDialogTitle" + }, + { + "id": "alert-dialog-cancel", + "name": "AlertDialogCancel", + "categories": [ + "interactive" + ] + } + ] + } + } + }, + "intents": [ + { + "id": "destructive-action", + "description": "Irreversible operations." + } + ], + "_comment": "CONSISTENCY: component/sub-component categories must be registered in the top-level categories registry." +} diff --git a/fixtures/negative/rule-required-props-empty.dspack.json b/fixtures/negative/rule-required-props-empty.dspack.json new file mode 100644 index 0000000..67546af --- /dev/null +++ b/fixtures/negative/rule-required-props-empty.dspack.json @@ -0,0 +1,69 @@ +{ + "dspack": "0.4", + "name": "negative-fixture", + "categories": { + "interactive": { + "description": "Receives activation." + } + }, + "components": { + "button": { + "name": "Button", + "description": "A button.", + "categories": [ + "interactive" + ], + "props": { + "variant": { + "type": "enum", + "values": [ + "default", + "destructive" + ] + } + } + }, + "alert-dialog": { + "name": "AlertDialog", + "description": "A confirmation dialog.", + "composition": { + "subComponents": [ + { + "id": "alert-dialog-trigger", + "name": "AlertDialogTrigger", + "categories": [ + "interactive" + ] + }, + { + "id": "alert-dialog-title", + "name": "AlertDialogTitle" + }, + { + "id": "alert-dialog-cancel", + "name": "AlertDialogCancel", + "categories": [ + "interactive" + ] + } + ] + } + } + }, + "intents": [ + { + "id": "destructive-action", + "description": "Irreversible operations." + } + ], + "_comment": "SCHEMA: a required-props rule must carry at least one of requiredText / requiredProps.", + "rules": [ + { + "id": "rule.fixture", + "type": "required-props", + "severity": "must", + "rationale": "Fixture rationale.", + "component": "button" + } + ] +} diff --git a/fixtures/negative/rule-required-props-unknown-within.dspack.json b/fixtures/negative/rule-required-props-unknown-within.dspack.json new file mode 100644 index 0000000..767df6d --- /dev/null +++ b/fixtures/negative/rule-required-props-unknown-within.dspack.json @@ -0,0 +1,71 @@ +{ + "dspack": "0.4", + "name": "negative-fixture", + "categories": { + "interactive": { + "description": "Receives activation." + } + }, + "components": { + "button": { + "name": "Button", + "description": "A button.", + "categories": [ + "interactive" + ], + "props": { + "variant": { + "type": "enum", + "values": [ + "default", + "destructive" + ] + } + } + }, + "alert-dialog": { + "name": "AlertDialog", + "description": "A confirmation dialog.", + "composition": { + "subComponents": [ + { + "id": "alert-dialog-trigger", + "name": "AlertDialogTrigger", + "categories": [ + "interactive" + ] + }, + { + "id": "alert-dialog-title", + "name": "AlertDialogTitle" + }, + { + "id": "alert-dialog-cancel", + "name": "AlertDialogCancel", + "categories": [ + "interactive" + ] + } + ] + } + } + }, + "intents": [ + { + "id": "destructive-action", + "description": "Irreversible operations." + } + ], + "_comment": "CONSISTENCY: required-props `within` must resolve to a component or sub-component of the contract.", + "rules": [ + { + "id": "rule.fixture", + "type": "required-props", + "severity": "must", + "rationale": "Fixture rationale.", + "component": "button", + "within": "menu-trigger", + "requiredText": true + } + ] +} diff --git a/fixtures/negative/rule-unknown-category-ref.dspack.json b/fixtures/negative/rule-unknown-category-ref.dspack.json new file mode 100644 index 0000000..aedd262 --- /dev/null +++ b/fixtures/negative/rule-unknown-category-ref.dspack.json @@ -0,0 +1,72 @@ +{ + "dspack": "0.4", + "name": "negative-fixture", + "categories": { + "interactive": { + "description": "Receives activation." + } + }, + "components": { + "button": { + "name": "Button", + "description": "A button.", + "categories": [ + "interactive" + ], + "props": { + "variant": { + "type": "enum", + "values": [ + "default", + "destructive" + ] + } + } + }, + "alert-dialog": { + "name": "AlertDialog", + "description": "A confirmation dialog.", + "composition": { + "subComponents": [ + { + "id": "alert-dialog-trigger", + "name": "AlertDialogTrigger", + "categories": [ + "interactive" + ] + }, + { + "id": "alert-dialog-title", + "name": "AlertDialogTitle" + }, + { + "id": "alert-dialog-cancel", + "name": "AlertDialogCancel", + "categories": [ + "interactive" + ] + } + ] + } + } + }, + "intents": [ + { + "id": "destructive-action", + "description": "Irreversible operations." + } + ], + "_comment": "CONSISTENCY: forbiddenCategories ids must be registered in the top-level categories registry.", + "rules": [ + { + "id": "rule.fixture", + "type": "forbidden-composition", + "severity": "must", + "rationale": "Fixture rationale.", + "component": "alert-dialog", + "forbiddenCategories": [ + "overlay" + ] + } + ] +} diff --git a/schema/README.md b/schema/README.md index 84e3a65..796e447 100644 --- a/schema/README.md +++ b/schema/README.md @@ -4,8 +4,10 @@ JSON Schema and related validation artifacts for dspack live in this directory. ## Current schemas +- [`dspack.v0.4.schema.json`](./dspack.v0.4.schema.json) — JSON Schema for dspack v0.4 + (current draft; adds component categories and the `required-props` rule type) - [`dspack.v0.3.schema.json`](./dspack.v0.3.schema.json) — JSON Schema for dspack v0.3 - (current draft; adds the governance blocks: `intents`, `rules`, `examples`) + (adds the governance blocks: `intents`, `rules`, `examples`) - [`dspack.surface.v0_1.schema.json`](./dspack.surface.v0_1.schema.json) — JSON Schema for the dspack **surface** document (v0.1), the protocol-neutral component tree that governance rules are evaluated against and that `examples[].surface` embeds diff --git a/schema/dspack.v0.4.schema.json b/schema/dspack.v0.4.schema.json new file mode 100644 index 0000000..ce6c9fd --- /dev/null +++ b/schema/dspack.v0.4.schema.json @@ -0,0 +1,1540 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/aestheticfunction/dspack/blob/main/schema/dspack.v0.4.schema.json", + "title": "dspack v0.4", + "description": "Schema for dspack v0.4 — a JSON format for representing design system corpora. Backward-compatible with v0.1–v0.3 documents; adds component categories (a contract-defined category registry referenced by component metadata and rules) and the required-props rule type.", + "type": "object", + "required": [ + "dspack", + "name" + ], + "properties": { + "$schema": { + "type": "string", + "description": "URI reference to this JSON Schema. Optional; consumers MUST NOT require this property." + }, + "dspack": { + "type": "string", + "const": "0.4", + "description": "Specification version. MUST be \"0.4\" for documents conforming to this version." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable name of the design system." + }, + "description": { + "type": "string", + "description": "Brief description of the design system's purpose and scope." + }, + "version": { + "type": "string", + "description": "Version of the design system content (not the spec version)." + }, + "metadata": { + "$ref": "#/$defs/metadata" + }, + "tokens": { + "type": "object", + "description": "Token definitions organized by category.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/tokenCategory" + } + }, + "components": { + "type": "object", + "description": "Component definitions keyed by component ID.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/componentEntry" + } + }, + "categories": { + "type": "object", + "description": "Contract-defined component category registry. Categories are the contract's own taxonomy (dspack bakes none in); component/sub-component metadata and category-based rule fields reference these ids. Referenced ids MUST be registered here.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/categoryEntry" + } + }, + "patterns": { + "type": "array", + "description": "Pattern entries describing preferred ways of combining components.", + "items": { + "$ref": "#/$defs/patternEntry" + } + }, + "antiPatterns": { + "type": "array", + "description": "Anti-pattern entries describing approaches that are deliberately ruled out.", + "items": { + "$ref": "#/$defs/antiPatternEntry" + } + }, + "intents": { + "type": "array", + "description": "Named generation intents — the vocabulary that scopes governance rules and examples. Referenced by rules[].appliesTo.intents and examples[].intent, and declared by callers when requesting generation.", + "items": { + "$ref": "#/$defs/intentEntry" + } + }, + "rules": { + "type": "array", + "description": "Machine-checkable governance rules, evaluated deterministically over dspack surface documents. Each rule is a typed, structured predicate plus a human-readable rationale. Evaluation semantics per type are normative in the v0.3 specification.", + "items": { + "$ref": "#/$defs/ruleEntry" + } + }, + "examples": { + "type": "array", + "description": "Compilable example surfaces tied to named intents. Examples serve double duty as documentation and few-shot exemplars; each surface must validate against the dspack surface schema and the contract vocabulary.", + "items": { + "$ref": "#/$defs/exampleEntry" + } + }, + "frameworkBindings": { + "type": "object", + "description": "Framework-specific information keyed by framework identifier.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/frameworkBinding" + } + }, + "themes": { + "type": "object", + "description": "Named sets of token overrides representing alternative visual modes.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/themeEntry" + } + }, + "layout": { + "$ref": "#/$defs/layoutPrimitives" + } + }, + "additionalProperties": true, + "$defs": { + "metadata": { + "type": "object", + "description": "Extensible metadata about the dspack file.", + "properties": { + "generatedBy": { + "type": "string", + "description": "Tool or process that created this file." + }, + "generatedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 datetime when the file was generated." + }, + "source": { + "type": "string", + "description": "URL or description of the upstream source." + }, + "license": { + "type": "string", + "description": "SPDX license identifier or freeform description." + } + }, + "additionalProperties": true + }, + "tokenCategory": { + "type": "object", + "description": "A category of tokens.", + "required": [ + "values" + ], + "properties": { + "description": { + "type": "string", + "description": "What this category covers." + }, + "tier": { + "type": "string", + "enum": [ + "primitive", + "semantic", + "component" + ], + "description": "Default abstraction level for tokens in this category." + }, + "values": { + "type": "object", + "description": "Map of token name to token entry.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/tokenEntry" + } + } + }, + "additionalProperties": true + }, + "tokenEntry": { + "type": "object", + "description": "A single token definition.", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string", + "description": "The resolved value of the token." + }, + "description": { + "type": "string", + "description": "Semantic meaning of the token." + }, + "type": { + "type": "string", + "description": "The value type (e.g., color, dimension, fontFamily)." + }, + "deprecated": { + "type": "boolean", + "description": "Whether this token is deprecated.", + "default": false + }, + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Other names by which this token is known." + }, + "status": { + "oneOf": [ + { + "type": "string", + "enum": [ + "draft", + "experimental", + "stable", + "deprecated" + ] + }, + { + "$ref": "#/$defs/statusObject" + } + ], + "description": "Lifecycle stage. String for uniform status, object for per-platform granularity." + }, + "aliasOf": { + "oneOf": [ + { + "type": "string", + "description": "Token name that this token aliases." + }, + { + "$ref": "#/$defs/aliasReference" + } + ], + "description": "Token that this token aliases. String for unambiguous names, object for cross-category disambiguation." + }, + "tier": { + "type": "string", + "enum": [ + "primitive", + "semantic", + "component" + ], + "description": "Abstraction level of this token, overriding the category default." + } + }, + "additionalProperties": true + }, + "statusObject": { + "type": "object", + "description": "Lifecycle status with per-platform granularity.", + "required": [ + "default" + ], + "properties": { + "default": { + "type": "string", + "enum": [ + "draft", + "experimental", + "stable", + "deprecated" + ], + "description": "Default lifecycle stage when no platform-specific override applies." + }, + "platforms": { + "type": "object", + "description": "Map of platform/framework ID to platform status object.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/platformStatus" + } + } + }, + "additionalProperties": true + }, + "platformStatus": { + "type": "object", + "description": "Lifecycle status for a specific platform.", + "required": [ + "stage" + ], + "properties": { + "stage": { + "type": "string", + "enum": [ + "draft", + "experimental", + "stable", + "deprecated" + ], + "description": "Lifecycle stage for this platform." + }, + "since": { + "type": "string", + "description": "Version of the design system content at which this stage took effect." + }, + "migrateTo": { + "type": "string", + "description": "Component ID or token name of the recommended replacement." + }, + "note": { + "type": "string", + "description": "Prose migration guidance or context for this platform's status." + } + }, + "additionalProperties": true + }, + "aliasReference": { + "type": "object", + "description": "Structured token alias reference for cross-category disambiguation.", + "required": [ + "category", + "token" + ], + "properties": { + "category": { + "type": "string", + "description": "Token category name containing the referenced token." + }, + "token": { + "type": "string", + "description": "Token name within that category." + } + }, + "additionalProperties": true + }, + "componentEntry": { + "type": "object", + "description": "A component definition.", + "required": [ + "name", + "description" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable display name." + }, + "description": { + "type": "string", + "description": "What the component is for." + }, + "whenToUse": { + "type": "string", + "description": "Guidance on when to use this component." + }, + "whenNotToUse": { + "type": "string", + "description": "Guidance on when to choose a different component." + }, + "props": { + "type": "object", + "description": "Map of prop name to prop descriptor.", + "additionalProperties": { + "$ref": "#/$defs/propDescriptor" + } + }, + "tokens": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Token names this component depends on." + }, + "relatedComponents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Component IDs of related components." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Freeform classification tags." + }, + "deprecated": { + "type": "boolean", + "description": "Whether this component is deprecated.", + "default": false + }, + "deprecatedMessage": { + "type": "string", + "description": "What to use instead of this component." + }, + "status": { + "oneOf": [ + { + "type": "string", + "enum": [ + "draft", + "experimental", + "stable", + "deprecated" + ] + }, + { + "$ref": "#/$defs/statusObject" + } + ], + "description": "Lifecycle stage. String for uniform status, object for per-platform granularity." + }, + "accessibility": { + "$ref": "#/$defs/accessibilityConstraints" + }, + "composition": { + "$ref": "#/$defs/compositionRules" + }, + "constraints": { + "type": "array", + "description": "Structured usage constraints.", + "items": { + "$ref": "#/$defs/constraintEntry" + } + }, + "categories": { + "type": "array", + "description": "Category ids this entry belongs to; each MUST be registered in the top-level categories registry.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "propDescriptor": { + "type": "object", + "description": "Describes a single component prop.", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "description": "The value type of the prop (e.g., string, number, boolean, enum)." + }, + "description": { + "type": "string", + "description": "What this prop controls." + }, + "values": { + "type": "array", + "description": "For enum type, the allowed values. Items may be bare values or value descriptor objects.", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/$defs/valueDescriptor" + } + ] + } + }, + "default": { + "description": "Default value of the prop." + }, + "required": { + "type": "boolean", + "description": "Whether this prop must be provided.", + "default": false + }, + "propRole": { + "type": "string", + "enum": [ + "flag", + "dimension", + "choice", + "slot", + "handler", + "content", + "state" + ], + "description": "Semantic role of this prop." + } + }, + "additionalProperties": true + }, + "valueDescriptor": { + "type": "object", + "description": "Describes a single allowed value for an enum prop.", + "required": [ + "value" + ], + "properties": { + "value": { + "description": "The actual enum value." + }, + "description": { + "type": "string", + "description": "When to choose this value." + }, + "deprecated": { + "type": "boolean", + "description": "Whether this specific value is deprecated.", + "default": false + } + }, + "additionalProperties": true + }, + "accessibilityConstraints": { + "type": "object", + "description": "Accessibility constraints and expectations for a component.", + "properties": { + "role": { + "type": "string", + "description": "WAI-ARIA role this component fulfills." + }, + "requiredAttributes": { + "type": "array", + "description": "HTML or ARIA attributes that must be present for correct accessible usage.", + "items": { + "$ref": "#/$defs/attributeDescriptor" + } + }, + "keyboardInteractions": { + "type": "array", + "description": "Expected keyboard behaviors.", + "items": { + "$ref": "#/$defs/keyboardInteraction" + } + }, + "contrastRequirement": { + "type": "string", + "description": "Minimum contrast ratio or WCAG level." + }, + "focusManagement": { + "type": "string", + "description": "Prose description of focus behavior expectations." + }, + "labelRequirement": { + "type": "string", + "enum": [ + "required-visible", + "required-accessible-name", + "required-aria", + "optional", + "none" + ], + "description": "How the component must be labeled." + }, + "notes": { + "type": "string", + "description": "Additional accessibility guidance in prose." + } + }, + "additionalProperties": true + }, + "attributeDescriptor": { + "type": "object", + "description": "Describes a required HTML or ARIA attribute for accessible usage.", + "required": [ + "attribute" + ], + "properties": { + "attribute": { + "type": "string", + "description": "The attribute name (e.g., aria-label, aria-describedby, id, type)." + }, + "description": { + "type": "string", + "description": "When and how to provide this attribute." + }, + "condition": { + "type": "string", + "description": "Condition under which this attribute is required." + } + }, + "additionalProperties": true + }, + "keyboardInteraction": { + "type": "object", + "description": "Describes an expected keyboard behavior.", + "required": [ + "key", + "description" + ], + "properties": { + "key": { + "type": "string", + "description": "The key or key combination." + }, + "description": { + "type": "string", + "description": "What this key does in the context of this component." + } + }, + "additionalProperties": true + }, + "compositionRules": { + "type": "object", + "description": "Rules governing how a component composes with other components.", + "properties": { + "subComponents": { + "type": "array", + "description": "Sub-components that belong to this compound component.", + "items": { + "$ref": "#/$defs/subComponentDescriptor" + } + }, + "requiredChildren": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Component IDs or sub-component IDs that must appear as descendants." + }, + "allowedChildren": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Component IDs or sub-component IDs that may appear as direct children." + }, + "requiredParent": { + "type": "string", + "description": "Component ID or sub-component ID that must be an ancestor." + }, + "allowedParents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Component IDs or sub-component IDs that may be the parent." + }, + "requiredSiblings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Component IDs or sub-component IDs that must also be present among siblings." + }, + "notes": { + "type": "string", + "description": "Prose description of composition constraints not captured by structured fields." + } + }, + "additionalProperties": true + }, + "subComponentDescriptor": { + "type": "object", + "description": "Describes a sub-component of a compound component.", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "description": "Identifier for the sub-component. Should be parent-prefixed." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable display name." + }, + "description": { + "type": "string", + "description": "What this sub-component is for." + }, + "required": { + "type": "boolean", + "description": "Whether this sub-component must be present when the parent is used.", + "default": false + }, + "slot": { + "type": "string", + "description": "Named slot this sub-component fills." + }, + "acceptsChildren": { + "type": "string", + "enum": [ + "any", + "text", + "components", + "none" + ], + "description": "What this sub-component expects as children." + }, + "categories": { + "type": "array", + "description": "Category ids this entry belongs to; each MUST be registered in the top-level categories registry.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "constraintEntry": { + "type": "object", + "description": "A structured usage constraint.", + "required": [ + "context", + "rule", + "severity" + ], + "properties": { + "context": { + "type": "string", + "description": "The situation or condition this constraint applies to." + }, + "rule": { + "type": "string", + "description": "What to do or not do." + }, + "severity": { + "type": "string", + "enum": [ + "must", + "should", + "should-not", + "must-not" + ], + "description": "RFC 2119 strength of the constraint." + } + }, + "additionalProperties": true + }, + "patternEntry": { + "type": "object", + "description": "A pattern describing a preferred way of combining components.", + "required": [ + "id", + "name", + "description" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "description": "Unique identifier for this pattern." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable name." + }, + "description": { + "type": "string", + "description": "What problem this pattern addresses." + }, + "intent": { + "type": "string", + "description": "The underlying design goal or UX objective." + }, + "context": { + "type": "string", + "description": "When this pattern applies." + }, + "components": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Component IDs involved in this pattern." + }, + "guidance": { + "type": "string", + "description": "Prose guidance on how to apply the pattern correctly." + }, + "relatedPatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Pattern IDs of related patterns." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Freeform classification tags." + } + }, + "additionalProperties": true + }, + "antiPatternEntry": { + "type": "object", + "description": "An anti-pattern describing an approach that is deliberately ruled out.", + "required": [ + "id", + "name", + "description", + "reason" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "description": "Unique identifier for this anti-pattern." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable name describing what not to do." + }, + "description": { + "type": "string", + "description": "What this anti-pattern is." + }, + "reason": { + "type": "string", + "description": "Why this approach is ruled out." + }, + "severity": { + "type": "string", + "enum": [ + "must-not", + "should-not", + "discouraged" + ], + "description": "Strength of the prohibition. Defaults to should-not.", + "default": "should-not" + }, + "insteadUse": { + "type": "string", + "description": "Pattern ID of the preferred approach." + }, + "components": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Component IDs involved in this anti-pattern." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Freeform classification tags." + } + }, + "additionalProperties": true + }, + "intentEntry": { + "type": "object", + "description": "A named generation intent: what kind of UI is being requested. Intents scope governance rules and select examples.", + "required": [ + "id", + "description" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "description": "Unique identifier for this intent." + }, + "name": { + "type": "string", + "description": "Human-readable display name." + }, + "description": { + "type": "string", + "description": "What kind of requested UI this intent covers. Written for both humans and generation prompts." + }, + "relatedPatterns": { + "type": "array", + "description": "IDs of patterns that document how to satisfy this intent.", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "description": "Free-form tags for grouping and search.", + "items": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ruleEntry": { + "type": "object", + "description": "A machine-checkable governance rule. The type field selects the evaluation algorithm (normative semantics in the v0.3/v0.4 specs). Linters MUST fail loudly on unknown types — never skip silently. Contract severity uses RFC 2119 terms; tools map must→error and should→warn.", + "required": [ + "id", + "type", + "severity", + "rationale" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^rule\\.[a-z0-9.-]+$", + "description": "Unique, stable identifier for this rule (rule.* namespace)." + }, + "type": { + "type": "string", + "enum": [ + "component-choice", + "required-composition", + "forbidden-composition", + "required-props" + ], + "description": "The rule type, selecting the evaluation algorithm. New types are added in future spec versions only (additive)." + }, + "severity": { + "type": "string", + "enum": [ + "must", + "should" + ], + "description": "RFC 2119 strength. Tools map must→error (triggers repair/failure) and should→warn (reported only)." + }, + "rationale": { + "type": "string", + "minLength": 1, + "description": "Why this rule exists. Shown verbatim in lint findings, repair feedback, and audit reports." + }, + "appliesTo": { + "type": "object", + "description": "Scope of the rule. Absent means universal (fires for every surface).", + "properties": { + "intents": { + "type": "array", + "description": "Intent IDs this rule fires for.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "examples": { + "type": "array", + "description": "IDs of examples demonstrating compliance; included in repair feedback as corrected references.", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "description": "Free-form tags for grouping and search.", + "items": { + "type": "string" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "component-choice" + } + } + }, + "then": { + "anyOf": [ + { + "required": [ + "require" + ] + }, + { + "required": [ + "forbid" + ] + } + ], + "properties": { + "require": { + "type": "array", + "description": "Component IDs that MUST each appear at least once in the surface.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "forbid": { + "type": "array", + "description": "Component IDs that MUST NOT appear in the surface.", + "items": { + "type": "string" + }, + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "required-composition" + } + } + }, + "then": { + "required": [ + "component" + ], + "anyOf": [ + { + "required": [ + "requiredSubComponents" + ] + }, + { + "required": [ + "requiredProps" + ] + } + ], + "properties": { + "component": { + "type": "string", + "description": "Component ID every instance of which is checked." + }, + "requiredSubComponents": { + "type": "array", + "description": "Sub-components that MUST appear among each matching node's descendants.", + "items": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Sub-component ID that must be present." + }, + "min": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Minimum number of occurrences among descendants." + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "minItems": 1 + }, + "requiredProps": { + "type": "array", + "description": "Prop constraints that MUST hold on each matching node (or on descendant sub-component nodes when 'on' is given).", + "items": { + "type": "object", + "required": [ + "prop", + "oneOf" + ], + "properties": { + "on": { + "type": "string", + "description": "Sub-component ID the constraint applies to; absent means the matching component node itself." + }, + "prop": { + "type": "string", + "description": "Prop name the constraint applies to." + }, + "oneOf": { + "type": "array", + "description": "Allowed values; the prop MUST be present and take one of these.", + "minItems": 1 + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "forbidden-composition" + } + } + }, + "then": { + "required": [ + "component" + ], + "anyOf": [ + { + "required": [ + "forbiddenDescendants" + ] + }, + { + "required": [ + "forbiddenProps" + ] + }, + { + "required": [ + "forbiddenCategories" + ] + } + ], + "properties": { + "component": { + "type": "string", + "description": "Component ID every instance of which is checked." + }, + "forbiddenDescendants": { + "type": "array", + "description": "Component or sub-component IDs that MUST NOT appear among a matching node's descendants.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "forbiddenProps": { + "type": "array", + "description": "Prop values that MUST NOT be used on matching nodes (or on descendant sub-component nodes when 'on' is given).", + "items": { + "type": "object", + "required": [ + "prop", + "values" + ], + "properties": { + "on": { + "type": "string", + "description": "Sub-component ID the constraint applies to; absent means the matching component node itself." + }, + "prop": { + "type": "string", + "description": "Prop name the constraint applies to." + }, + "values": { + "type": "array", + "description": "Forbidden values for the prop.", + "minItems": 1 + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "minItems": 1 + }, + "forbiddenCategories": { + "type": "array", + "description": "Category ids: no descendant of a matching node may belong to any of these categories. Each id MUST be registered in the top-level categories registry.", + "items": { + "type": "string" + }, + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "required-props" + } + } + }, + "then": { + "required": [ + "component" + ], + "anyOf": [ + { + "required": [ + "requiredText" + ] + }, + { + "required": [ + "requiredProps" + ] + } + ], + "properties": { + "component": { + "type": "string", + "description": "Component or sub-component ID whose instances are checked (unlike other rule types, sub-component ids are accepted here)." + }, + "within": { + "type": "string", + "description": "Optional scope: a component or sub-component ID. When present, only nodes with an ancestor matching this id are checked, and every node matching `within` MUST contain at least one node matching `component`." + }, + "requiredText": { + "const": true, + "description": "The matching node MUST carry non-empty direct text (its own `text` field, never text in descendants)." + }, + "requiredProps": { + "type": "array", + "description": "Props that MUST be present directly on the matching node's `props`. Unlike required-composition's requiredProps, entries have no `on` (the rule's component IS the target) and `oneOf` is optional (presence-only when absent).", + "items": { + "type": "object", + "required": [ + "prop" + ], + "properties": { + "prop": { + "type": "string", + "description": "Prop name that must be present on the node itself." + }, + "oneOf": { + "type": "array", + "minItems": 1, + "description": "Optional allowed values; when present the prop value MUST be a member." + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "minItems": 1 + } + } + } + } + ] + }, + "exampleEntry": { + "type": "object", + "description": "A compilable example surface tied to a named intent. Serves as documentation and as a few-shot exemplar for generation.", + "required": [ + "id", + "intent", + "surface" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^ex\\.[a-z0-9.-]+$", + "description": "Unique identifier for this example (ex.* namespace)." + }, + "intent": { + "type": "string", + "description": "Intent ID this example demonstrates." + }, + "name": { + "type": "string", + "description": "Human-readable display name." + }, + "description": { + "type": "string", + "description": "What the example shows and why it is correct." + }, + "prompt": { + "type": "string", + "description": "A representative user request this example answers; used as the user turn in few-shot blocks." + }, + "surface": { + "type": "object", + "description": "A dspack surface document. Validated against dspack.surface.v0_1.schema.json plus the contract vocabulary by tooling; kept loose here to avoid a cross-file $ref." + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "frameworkBinding": { + "type": "object", + "description": "Framework-specific information for the design system.", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable framework name." + }, + "package": { + "type": "string", + "description": "Primary package name." + }, + "installCommand": { + "type": "string", + "description": "How to install the framework binding." + }, + "description": { + "type": "string", + "description": "What this binding provides." + }, + "guidance": { + "type": "string", + "description": "Framework-wide guidance." + }, + "components": { + "type": "object", + "description": "Per-component framework details keyed by component ID.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/componentBinding" + } + } + }, + "additionalProperties": true + }, + "componentBinding": { + "type": "object", + "description": "Framework-specific details for a single component.", + "properties": { + "importPath": { + "type": "string", + "description": "Where to import the component." + }, + "installCommand": { + "type": "string", + "description": "Component-specific install command." + }, + "exportName": { + "type": "string", + "description": "Named export if different from the component name." + }, + "guidance": { + "type": "string", + "description": "Framework-specific usage guidance for this component." + }, + "subComponents": { + "type": "object", + "description": "Map of sub-component ID to sub-component binding.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "$ref": "#/$defs/subComponentBinding" + } + } + }, + "additionalProperties": true + }, + "subComponentBinding": { + "type": "object", + "description": "Framework-specific details for a sub-component.", + "properties": { + "exportName": { + "type": "string", + "description": "Named export for this sub-component." + }, + "importPath": { + "type": "string", + "description": "Import path if different from the parent component's import path." + } + }, + "additionalProperties": true + }, + "themeEntry": { + "type": "object", + "description": "A named set of token overrides representing an alternative visual mode.", + "required": [ + "name", + "overrides" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable theme name." + }, + "description": { + "type": "string", + "description": "What this theme is for." + }, + "overrides": { + "type": "object", + "description": "Map of token reference (category.tokenName) to overridden resolved value.", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$" + }, + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": true + }, + "layoutPrimitives": { + "type": "object", + "description": "Layout system primitives: breakpoints, grid, containers, and spacing scale.", + "properties": { + "breakpoints": { + "type": "object", + "description": "Named responsive breakpoints.", + "additionalProperties": { + "$ref": "#/$defs/breakpointEntry" + } + }, + "grid": { + "$ref": "#/$defs/gridConfig" + }, + "containers": { + "type": "object", + "description": "Named container width configurations.", + "additionalProperties": { + "$ref": "#/$defs/containerEntry" + } + }, + "spacingScale": { + "$ref": "#/$defs/spacingScaleConfig" + } + }, + "additionalProperties": true + }, + "breakpointEntry": { + "type": "object", + "description": "A responsive breakpoint definition.", + "required": [ + "minWidth" + ], + "properties": { + "minWidth": { + "type": "string", + "description": "Minimum viewport width for this breakpoint." + }, + "description": { + "type": "string", + "description": "What this breakpoint targets." + } + }, + "additionalProperties": true + }, + "gridConfig": { + "type": "object", + "description": "Grid system parameters.", + "properties": { + "columns": { + "type": "number", + "description": "Number of columns in the grid system." + }, + "gutter": { + "type": "string", + "description": "Default gutter width between columns." + }, + "margin": { + "type": "string", + "description": "Default outer margin of the grid container." + }, + "description": { + "type": "string", + "description": "Guidance on grid usage." + } + }, + "additionalProperties": true + }, + "containerEntry": { + "type": "object", + "description": "A container width configuration.", + "required": [ + "maxWidth" + ], + "properties": { + "maxWidth": { + "type": "string", + "description": "Maximum width of this container." + }, + "description": { + "type": "string", + "description": "When to use this container size." + } + }, + "additionalProperties": true + }, + "spacingScaleConfig": { + "type": "object", + "description": "Spacing scale system description.", + "properties": { + "baseUnit": { + "type": "string", + "description": "The fundamental unit of the spacing scale." + }, + "description": { + "type": "string", + "description": "How the scale is constructed." + } + }, + "additionalProperties": true + }, + "categoryEntry": { + "type": "object", + "description": "A component category: a named role components/sub-components can declare and rules can select by.", + "required": [ + "description" + ], + "properties": { + "name": { + "type": "string", + "description": "Display name." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "What membership in this category means; written for maintainers and reviewers." + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + } + } +} diff --git a/scripts/validate.mjs b/scripts/validate.mjs index a46173a..e4ae91b 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -8,10 +8,12 @@ * with dspack-emit). * 2. examples — every examples/*.dspack.json validates against the schema * matching its declared `dspack` version. - * 3. back-compat — for v0.3 documents, the document with the governance - * blocks (intents/rules/examples) removed still validates against the - * v0.3 schema (the "v0.2 shape + dspack: 0.3 is valid" guarantee). - * 4. governance consistency — for v0.3 documents: unique IDs, intent + * 3. back-compat — for v0.3+ documents, the document with that version's + * additive blocks removed (intents/rules/examples; for 0.4 also the + * categories registry and membership fields) still validates against + * its own schema (the "v0.2 shape + a newer dspack version is valid" + * guarantee, per each version's strictly-additive promise). + * 4. governance consistency — for v0.3+ documents: unique IDs, intent * references resolve, rule component references resolve, rule example * references resolve, and every examples[].surface passes: * S1 — the generic dspack surface schema, and @@ -19,6 +21,9 @@ * names, enum prop values, declared slot names). * S2 here checks exactly what the v0.3 spec defines for the gate; it * does not check acceptsChildren semantics or non-enum prop types. + * 5. categories consistency — for v0.4 documents: category ids referenced + * by component/sub-component metadata and by rule forbiddenCategories + * resolve in the top-level categories registry. * * Negative mode (`npm run validate -- --fixtures negative`): * Runs the same full validation over fixtures/negative/*.dspack.json and @@ -41,7 +46,10 @@ const DSPACK_SCHEMAS = { "0.1": "dspack.v0.1.schema.json", "0.2": "dspack.v0.2.schema.json", "0.3": "dspack.v0.3.schema.json", + "0.4": "dspack.v0.4.schema.json", }; +/** Versions with governance blocks (and, from 0.4, categories) to consistency-check. */ +const GOVERNANCE_VERSIONS = new Set(["0.3", "0.4"]); const SURFACE_SCHEMA = "dspack.surface.v0_1.schema.json"; function newAjv() { @@ -156,15 +164,45 @@ function ruleComponentRefs(rule) { }; push("require", rule.require); push("forbid", rule.forbid); - if (rule.component) refs.push({ kind: "component", id: rule.component }); + // required-props (v0.4) is the one type whose `component` accepts a + // sub-component id (spec v0.4 §4.1); `within` accepts either kind. + if (rule.component) { + refs.push({ kind: rule.type === "required-props" ? "componentOrSub" : "component", id: rule.component }); + } + if (rule.within) refs.push({ kind: "componentOrSub", id: rule.within }); push("forbiddenDescendants", rule.forbiddenDescendants); push("requiredSubComponents", (rule.requiredSubComponents ?? []).map((s) => s.id)); - push("on", (rule.requiredProps ?? []).map((p) => p.on).filter(Boolean)); + // `on` entries exist only on required-composition/forbidden-composition + // requiredProps/forbiddenProps; required-props (v0.4) entries have no `on`. + if (rule.type !== "required-props") { + push("on", (rule.requiredProps ?? []).map((p) => p.on).filter(Boolean)); + } push("on", (rule.forbiddenProps ?? []).map((p) => p.on).filter(Boolean)); return refs; } -/** Governance consistency checks for a v0.3 document. Returns error strings. */ +/** Category consistency checks for a v0.4 document. Returns error strings. */ +function checkCategories(doc) { + const errors = []; + const registry = new Set(Object.keys(doc.categories ?? {})); + const checkMember = (where, ids) => { + for (const id of ids ?? []) { + if (!registry.has(id)) errors.push(`${where}: category '${id}' is not registered in categories`); + } + }; + for (const [cid, entry] of Object.entries(doc.components ?? {})) { + checkMember(`components.${cid}`, entry.categories); + for (const sub of entry.composition?.subComponents ?? []) { + checkMember(`components.${cid} sub-component '${sub.id}'`, sub.categories); + } + } + for (const rule of doc.rules ?? []) { + checkMember(rule.id ?? "(rule without id)", rule.forbiddenCategories); + } + return errors; +} + +/** Governance consistency checks for a v0.3+ document. Returns error strings. */ function checkGovernance(doc, validateSurface) { const errors = []; // Spec §5 scopes governance consistency (incl. sub-component id uniqueness) @@ -210,7 +248,7 @@ function checkGovernance(doc, validateSurface) { ? resolvesToSub : kind === "component" ? resolvesToComponent - : resolvesToComponent || resolvesToSub; + : resolvesToComponent || resolvesToSub; // componentOrSub, require, forbid, forbiddenDescendants if (!ok) errors.push(`${rule.id}: ${kind} reference '${id}' does not resolve in the contract`); } for (const ex of rule.examples ?? []) { @@ -255,9 +293,12 @@ function validateDocument(doc, validators) { for (const e of validate.errors ?? []) errors.push(`schema ${fmtErr(e)}`); return errors; } - if (version === "0.3") { + if (GOVERNANCE_VERSIONS.has(version)) { errors.push(...checkGovernance(doc, validators.get(SURFACE_SCHEMA))); } + if (version === "0.4") { + errors.push(...checkCategories(doc)); + } return errors; } @@ -318,14 +359,35 @@ function main() { const doc = loadJson(path); const errors = validateDocument(doc, validators); - // Back-compat guarantee: a v0.3 document minus governance blocks stays valid. - if (doc?.dspack === "0.3" && errors.length === 0) { + // Back-compat guarantee: a v0.3+ document minus that version's additive + // blocks stays valid — i.e. the pre-governance core shape is untouched. + // For 0.4 that means also stripping categories (the registry AND the + // membership fields), so the check really exercises the "v0.2 shape + a + // newer dspack version is valid" guarantee rather than passing v0.4 + // features through. + if (GOVERNANCE_VERSIONS.has(doc?.dspack) && errors.length === 0) { const stripped = { ...doc }; delete stripped.intents; delete stripped.rules; delete stripped.examples; + if (doc.dspack === "0.4") { + delete stripped.categories; + stripped.components = Object.fromEntries( + Object.entries(doc.components ?? {}).map(([id, entry]) => { + const e = { ...entry }; + delete e.categories; + if (e.composition?.subComponents) { + e.composition = { + ...e.composition, + subComponents: e.composition.subComponents.map(({ categories, ...sub }) => sub), + }; + } + return [id, e]; + }), + ); + } const strippedErrors = validateDocument(stripped, validators); - for (const e of strippedErrors) errors.push(`back-compat (governance blocks removed): ${e}`); + for (const e of strippedErrors) errors.push(`back-compat (version's additive blocks removed): ${e}`); } if (errors.length) { diff --git a/spec/README.md b/spec/README.md index 1c69d73..b0299d2 100644 --- a/spec/README.md +++ b/spec/README.md @@ -4,14 +4,21 @@ Versioned specification documents for dspack live in this directory. ## Current drafts -- [`dspack-v0.3.md`](./dspack-v0.3.md) — v0.3 specification (current draft, written as a - delta over v0.2): the governance blocks (`intents`, `rules`, `examples`), normative rule +- [`dspack-v0.4.md`](./dspack-v0.4.md) — v0.4 specification (current draft, written as a + delta over v0.3): component categories (contract-defined registry, membership metadata, + the `forbiddenCategories` rule form) and the `required-props` rule type ("this component + must carry named content directly") +- [`migration-v0.3-to-v0.4.md`](./migration-v0.3-to-v0.4.md) — migration guide (v0.4 is + strictly additive; includes the worked v0.4 deltas from the shadcn contract) +- [`dspack-v0.3.md`](./dspack-v0.3.md) — v0.3 specification (written as a + delta over v0.2; remains normative for everything the v0.4 delta does not cover): the + governance blocks (`intents`, `rules`, `examples`), normative rule evaluation semantics, the severity mapping (must→error / should→warn), the dspack surface format, and the S1/S2/S3 validation gates - [`migration-v0.2-to-v0.3.md`](./migration-v0.2-to-v0.3.md) — migration guide (v0.3 is strictly additive; includes a worked anti-pattern → rule conversion) - [`dspack-v0.2.md`](./dspack-v0.2.md) — v0.2 specification (remains normative for everything - the v0.3 delta does not cover) + the v0.3/v0.4 deltas do not cover) - [`dspack-v0.1.md`](./dspack-v0.1.md) — v0.1 specification draft All drafts may change before stabilization at v1.0. diff --git a/spec/dspack-v0.4.md b/spec/dspack-v0.4.md new file mode 100644 index 0000000..bfb4e9e --- /dev/null +++ b/spec/dspack-v0.4.md @@ -0,0 +1,206 @@ +# dspack Specification — v0.4 (delta) + +**Status: draft.** + +This document specifies dspack v0.4 as a **delta over v0.3**. Everything in the +[v0.3 specification](./dspack-v0.3.md) (and, through it, v0.2) remains normative and +unchanged; v0.4 is strictly additive. A valid v0.3 document with `"dspack": "0.4"` is a +valid v0.4 document. + +v0.4 lifts two items from v0.3's deliberate ceiling (v0.3 §9), both driven by evidence +from real contracts and measured pipeline runs: + +1. **Component categories** — a contract-defined category registry, category membership + on component and sub-component metadata, and a category-based form of + `forbidden-composition`. Lifts the "category-based selection" ceiling item: rules like + *no interactive descendants* no longer enumerate ids, which does not scale past small + vocabularies. +2. **The `required-props` rule type** — *a component must carry named content directly*. + Lifts the gap between governance and projection measured at 78/78 gate failures across + three model families (dspack-gen findings, 2026-07-03): surfaces whose required text + sat where no rule could require it to be, and where protocol emitters cannot lift it + from. + +The matching JSON Schema is [`schema/dspack.v0.4.schema.json`](../schema/dspack.v0.4.schema.json). +The surface format is unchanged (`dspackSurface: "0.1"`). + +## Table of Contents + +- [1. Conformance](#1-conformance) +- [2. File Identification](#2-file-identification) +- [3. Component Categories](#3-component-categories) +- [4. Rules](#4-rules) +- [5. Validation Gates](#5-validation-gates) +- [6. Deliberate Ceiling](#6-deliberate-ceiling) + +## 1. Conformance + +RFC 2119 terms as in v0.3 §1. This document adds requirements for producers of the +`categories` block and for linters implementing the `required-props` type and the +`forbiddenCategories` field. + +Per v0.3 §5.4–§5.5, a **v0.3 linter** encountering a `required-props` rule MUST fail +loudly (unknown rule type, recommended exit code 4). This is by design: a v0.4 contract +is not silently half-governed by v0.3 tools. + +## 2. File Identification + +A v0.4 document declares `"dspack": "0.4"`. All governance blocks and the `categories` +block are optional; consumers that understand only earlier versions MUST ignore unknown +top-level properties, so v0.4 documents degrade safely — with the deliberate exception of +unknown rule types, which fail loudly in older linters as specified above. + +## 3. Component Categories + +```json +"categories": { + "interactive": { + "name": "Interactive", + "description": "Receives pointer or keyboard activation: buttons, inputs, menu items…" + }, + "overlay": { + "name": "Overlay", + "description": "Renders content in a layer above the page with its own focus and dismiss semantics." + } +} +``` + +A **category** is a named role that components and sub-components can declare and rules +can select by. The registry is **contract-defined**: dspack bakes in no taxonomy, and a +category id means exactly what its `description` says it means for this contract. + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| *(registry key)* | string (`^[a-z][a-z0-9-]*$`) | — | The category id. | +| `name` | string | no | Display name. | +| `description` | string | yes | What membership means; written for maintainers and reviewers. | + +Membership is declared where the component is declared: + +- `components..categories: string[]` — on a component entry. +- `composition.subComponents[].categories: string[]` — on a sub-component descriptor. + +A membership array, when present, MUST be non-empty (omit the field rather than declaring +`[]`; the schema enforces `minItems: 1`). + +**Referential integrity.** Every category id referenced by a membership list or by a +rule field (§4.2) MUST be registered in `categories`; otherwise the document is +inconsistent and validating tools MUST reject it. Registered categories with no members +or no referencing rules are permitted (a registry may be populated ahead of the rules +that use it). + +**Categories are contract metadata, not surface vocabulary.** Surfaces never name +categories; gate S2 is unaffected (§5). Categories exist so rules can select over the +contract's own structure — membership is resolved through the contract at lint time, +never carried in the artifact. + +## 4. Rules + +The common rule fields, severity model, applicability, findings requirements, and +unknown-type behavior are unchanged (v0.3 §5). v0.4 adds one rule type and one field on +an existing type. Per v0.3 §5.5, the three v0.3 types' semantics are frozen; the +`forbiddenCategories` addition is a new optional field, not a change to existing +fields' semantics. + +### 4.1 `required-props` — content every instance of a component must carry directly + +```json +{ + "id": "rule.trigger-carries-label", + "type": "required-props", + "severity": "must", + "component": "button", + "within": "alert-dialog-trigger", + "requiredText": true, + "rationale": "The trigger button must present its label as its own text…", + "examples": ["ex.delete-account-confirmation"] +} +``` + +Fields: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `component` | string | yes | Component **or sub-component** id whose instances are checked. This is the one rule type whose `component` accepts a sub-component id. | +| `within` | string | no | Component or sub-component id scoping the check (see below). | +| `requiredText` | `true` | one of these two | The node MUST carry non-empty **direct** text (its own `text` field). | +| `requiredProps` | `{prop, oneOf?}[]` | one of these two | Props that MUST be present **directly on the node's `props`**; when `oneOf` is given the value MUST be a member. | + +**Normative evaluation semantics.** Terms as in v0.3 §5.3 ("descendants", "matches"). + +The **checked set**: + +- When `within` is absent: every node in the surface matching `component`. +- When `within` is present: every node matching `component` that has an ancestor + matching `within`. Additionally, **every node matching `within` MUST contain at least + one descendant matching `component`** (one finding per `within` node with none, + located at that node). This existence clause mirrors v0.3's `requiredProps.on` + semantics ("at least one such descendant MUST exist") and closes the hole where a + scope carries no label-bearing node at all. + +For each node in the checked set: + +- `requiredText: true` — the node MUST have a `text` field that is a non-empty string. + Text carried by descendants does not satisfy the requirement; that is the point of + the rule type. One finding per violating node, located at it. +- Each `requiredProps` entry — the node's own `props[prop]` MUST be present; when + `oneOf` is present, its value MUST be a member. One finding per violated entry, + located at the node. + +**Distinction from `required-composition.requiredProps`** (v0.3 §5.3): that field is +`on`-scoped (checks descendants of the anchoring component) and requires `oneOf` +(value-membership only). `required-props` entries have no `on` — the rule's `component` +IS the target — `oneOf` is optional (presence-only when absent), and `requiredText` +covers the node's own `text` field, which no v0.3 form can reach. `required-props` is +**conditional**: apart from the `within` existence clause, it constrains instances that +exist; use `required-composition` to require that structures exist. + +### 4.2 `forbidden-composition` — the `forbiddenCategories` field + +```json +{ + "id": "rule.alertdialog-no-nested-overlays", + "type": "forbidden-composition", + "severity": "must", + "component": "alert-dialog", + "forbiddenCategories": ["overlay"], + "rationale": "An alert dialog is a single focused interruption…" +} +``` + +`forbiddenCategories?: string[]` joins `forbiddenDescendants` and `forbiddenProps` (at +least one of the three MUST be present). + +**Normative evaluation semantics.** For **every** node matching `component`: no +descendant may match a component or sub-component whose contract entry declares +membership in any id listed in `forbiddenCategories` (one finding per offending +descendant, located at it). Membership is resolved through the contract's `categories` +declarations at lint time. The finding's message MUST name both the concrete offending +component id and the matched category, so repair feedback stays actionable without the +contract in hand. + +The anchoring node itself is not a descendant of itself: a component that belongs to a +forbidden category may still anchor the rule (as `alert-dialog` — itself an overlay — +does above). + +## 5. Validation Gates + +S1, S2, and S3 are unchanged (v0.3 §8). In particular, S2 still checks the **full +contract vocabulary** and knows nothing of categories; category-based selection is S3 +territory, resolved through the contract at lint time. Generation schemas continue to +encode vocabulary and shape only (v0.3 §3) — neither categories nor any rule content +belongs in them. + +## 6. Deliberate Ceiling + +Still not expressible in v0.4, recorded so the ceiling stays explicit: + +- **Ordering constraints** — "cancel appears before confirm in reading order." +- **Cardinality beyond `min`** — no `max`, no exact counts. +- **Token-usage and layout rules.** +- **Category-based forms beyond forbidden descendants** — `require`/`forbid` by category + in `component-choice`, category-scoped `required-composition`. Add them when a real + contract needs them, not before. +- **Soft/heuristic judgments** — out of scope; every v0.4 rule remains deterministic. + +Future types arrive additively per v0.3 §5.5; existing types' semantics are frozen. diff --git a/spec/migration-v0.3-to-v0.4.md b/spec/migration-v0.3-to-v0.4.md new file mode 100644 index 0000000..c631a1a --- /dev/null +++ b/spec/migration-v0.3-to-v0.4.md @@ -0,0 +1,94 @@ +# Migrating from dspack v0.3 to v0.4 + +**Short version: change `"dspack": "0.3"` to `"0.4"` and you are done.** v0.4 is strictly +additive; everything else in this guide is about what you can *now add*, not what you must +change. + +## What changes + +Nothing, structurally. v0.4 adds: + +| Addition | Where | Purpose | +| --- | --- | --- | +| `categories` | new optional top-level block | Contract-defined category registry: named roles components can declare and rules can select by. | +| `categories: string[]` | component entries and sub-component descriptors | Membership in registered categories. | +| `required-props` | new rule type | "This component must carry named content **directly**" — direct text (`requiredText`) or directly-present props. | +| `forbiddenCategories` | new optional field on `forbidden-composition` | Forbid descendants by category instead of enumerating ids. | + +No existing field changes meaning, type, or required status. No field is removed. The +three v0.3 rule types' semantics are frozen (v0.3 §5.5); enumerated +`forbiddenDescendants` rules remain valid and unchanged alongside the category form. + +## Validity guarantees + +- **Any valid v0.3 document with `"dspack"` set to `"0.4"` validates against the v0.4 + schema.** The same guarantee every version has given its predecessor. +- **Older consumers ignore the new blocks** — with one deliberate exception: a v0.3 + linter encountering a `required-props` rule MUST fail loudly (unknown rule type, + v0.3 §5.4). A v0.4 contract is never silently half-governed by v0.3 tools; upgrade the + linter or keep the contract at v0.3. +- **Forward:** a v0.4 document remains valid under future schemas; new rule types arrive + additively. + +## Worked example: the v0.4 shadcn contract deltas + +The repository's [`examples/shadcn-ui.dspack.json`](../examples/shadcn-ui.dspack.json) +shows all three additions in use. + +**A registry of two categories, populated where the metadata already lived** (the +`tags` fields sketched the taxonomy; categories make it referenceable by rules): + +```json +"categories": { + "interactive": { "name": "Interactive", "description": "Receives pointer or keyboard activation…" }, + "overlay": { "name": "Overlay", "description": "Renders content in a layer above the page…" } +} +``` + +with memberships such as `button.categories: ["interactive"]`, +`dropdown-menu.categories: ["interactive", "overlay"]`, and — on sub-component +descriptors — `alert-dialog-trigger.categories: ["interactive"]`. + +**A `required-props` rule closing a measured gap.** Three model families produced +governance-clean surfaces whose trigger-button label sat in a nested child (or nowhere), +which no v0.3 rule could require and no protocol projection could lift — 78/78 of the +eval's emitter-gate failures shared that one signature. The rule that makes it a lintable, +repairable finding: + +```json +{ + "id": "rule.trigger-carries-label", + "type": "required-props", + "severity": "must", + "component": "button", + "within": "alert-dialog-trigger", + "requiredText": true, + "rationale": "The trigger button must present its label as its own text…" +} +``` + +**A category-based `forbidden-composition` rule** that would otherwise enumerate ids and +silently rot as the vocabulary grows: + +```json +{ + "id": "rule.alertdialog-no-nested-overlays", + "type": "forbidden-composition", + "severity": "must", + "component": "alert-dialog", + "forbiddenCategories": ["overlay"], + "rationale": "An alert dialog is a single focused interruption…" +} +``` + +## What to check after migrating + +1. `npm run validate` (or your equivalent): category references — memberships and rule + fields — must resolve in the registry; `required-props` `component`/`within` must + resolve in the vocabulary. These are consistency checks, not schema checks. +2. Your linter implements `required-props` and `forbiddenCategories` before any contract + using them goes live — otherwise every lint fails loudly, by design. +3. Existing rules: consider whether enumerated `forbiddenDescendants` lists should + *stay* enumerated. Migrating an in-production rule to the category form changes which + nodes it matches (categories usually cover more ids); treat that as a governed change + to measure, not a mechanical rewrite.