From c3ad7e7df054388fd6a2c0de24590ffe0ae84865 Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Fri, 7 Aug 2026 12:22:13 -0400 Subject: [PATCH] =?UTF-8?q?spec(v0.4):=20requiredCategories=20on=20require?= =?UTF-8?q?d-composition=20=E2=80=94=20the=20first=20ceiling=20item=20lift?= =?UTF-8?q?ed=20by=20measurement=20(0.4.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The T1 representation milestone's Build evaluation (dspack-emit#31) produced lint-clean surfaces whose form-control nodes carried literal text and no interactive control at all — structurally valid, semantically empty form composition that every gate passed and the emitter correctly refused. The invariant "must contain an approved interactive control" is an OR across a category's members; requiredSubComponents can only AND exact ids, and category vocabulary existed only on the forbidden side (§4.2). §6's Deliberate Ceiling anticipated exactly this addition "when a real contract needs them, not before". This is that need, with the probe surfaces committed as evidence in dspack-emit's eval/. The amendment, additive and narrow: - schema: required-composition gains requiredCategories?: {id, min=1}[] (third arm of the existing anyOf; additionalProperties stays false; x-* preserved on entries); - lib/validate: each entry's id must be registered in categories — the same consistency check forbiddenCategories carries, producing the same class of finding (proven by fixture: a typo'd id is rejected against the registry, never silently never-matching); - spec §4.3: normative semantics mirroring §4.2 — per matching node, LOCAL to its descendants (a member elsewhere satisfies nothing), min default 1, entries AND like requiredSubComponents, membership OR within a category, findings name the category and count. No boolean expressions, oneOf, or predicates: a requirement a category cannot express is a missing category or a different rule, not a grammar extension; - two negative fixtures (malformed entry refused by schema; unknown category refused by consistency); - §6 ceiling updated: the first item lifted by measurement, recorded as such. Existing documents remain valid and behaviourally unchanged (both examples green; all negative fixtures still reject; evaluation of the new field lands in dspack-gen S3, which per v0.3 §5.4-§5.5 fails loudly on fields it does not yet understand only for unknown TYPES — an unknown FIELD on a known type is refused by this schema until tools update, preserving fail-closed). Co-Authored-By: Claude Opus 5 --- ...-required-categories-malformed.dspack.json | 23 + ...ed-categories-unknown-category.dspack.json | 28 + lib/validate.mjs | 4 + package.json | 2 +- schema/dspack.v0.4.schema.json | 2976 +++++++++-------- spec/dspack-v0.4.md | 53 +- 6 files changed, 1610 insertions(+), 1476 deletions(-) create mode 100644 fixtures/negative/rule-required-categories-malformed.dspack.json create mode 100644 fixtures/negative/rule-required-categories-unknown-category.dspack.json diff --git a/fixtures/negative/rule-required-categories-malformed.dspack.json b/fixtures/negative/rule-required-categories-malformed.dspack.json new file mode 100644 index 0000000..3d58da9 --- /dev/null +++ b/fixtures/negative/rule-required-categories-malformed.dspack.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://aestheticfunction.github.io/dspack/schema/dspack.v0.4.schema.json", + "dspack": "0.4", + "name": "negative/required-categories-malformed", + "description": "requiredCategories entries are {id, min?} only: a bare string, a missing id, or min 0 must all fail schema validation.", + "version": "0.0.1", + "tokens": { "color": { "values": { "primary": { "value": "#3366ff" } } } }, + "components": { + "field": { "name": "Field", "description": "A field wrapper.", "props": {}, "categories": ["form"] } + }, + "categories": { "form": { "name": "Form", "description": "Form structure." } }, + "intents": [ { "id": "data-entry", "name": "Data entry", "description": "Collect input." } ], + "rules": [ + { + "id": "rule.malformed", + "type": "required-composition", + "severity": "must", + "component": "field", + "requiredCategories": [ { "min": 0 } ], + "rationale": "Malformed entry: no id, min below 1." + } + ] +} diff --git a/fixtures/negative/rule-required-categories-unknown-category.dspack.json b/fixtures/negative/rule-required-categories-unknown-category.dspack.json new file mode 100644 index 0000000..9a09663 --- /dev/null +++ b/fixtures/negative/rule-required-categories-unknown-category.dspack.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://aestheticfunction.github.io/dspack/schema/dspack.v0.4.schema.json", + "dspack": "0.4", + "name": "negative/required-categories-unknown-category", + "description": "A required-composition rule referencing a category the document does not register must fail category consistency — the same class of finding forbiddenCategories produces.", + "version": "0.0.1", + "tokens": { "color": { "values": { "primary": { "value": "#3366ff" } } } }, + "components": { + "field": { + "name": "Field", + "description": "A field wrapper.", + "props": {}, + "categories": ["form"] + } + }, + "categories": { "form": { "name": "Form", "description": "Form structure." } }, + "intents": [ { "id": "data-entry", "name": "Data entry", "description": "Collect input." } ], + "rules": [ + { + "id": "rule.field-carries-control", + "type": "required-composition", + "severity": "must", + "component": "field", + "requiredCategories": [ { "id": "interactve", "min": 1 } ], + "rationale": "Typo'd category id: must be reported against the registry, never silently never-matching." + } + ] +} diff --git a/lib/validate.mjs b/lib/validate.mjs index 9f9b9df..b7d759f 100644 --- a/lib/validate.mjs +++ b/lib/validate.mjs @@ -179,6 +179,10 @@ export function checkCategories(doc) { } for (const rule of doc.rules ?? []) { checkMember(rule.id ?? "(rule without id)", rule.forbiddenCategories); + checkMember( + rule.id ?? "(rule without id)", + (rule.requiredCategories ?? []).map((r) => r.id), + ); } return errors; } diff --git a/package.json b/package.json index ce3fe5d..de28594 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aestheticfunction/dspack-spec", - "version": "0.4.2", + "version": "0.4.3", "description": "The dspack specification: spec documents, JSON Schemas, reference example contracts, and the validation harness (bin: dspack-validate).", "type": "module", "license": "Apache-2.0", diff --git a/schema/dspack.v0.4.schema.json b/schema/dspack.v0.4.schema.json index e288828..9840c9e 100644 --- a/schema/dspack.v0.4.schema.json +++ b/schema/dspack.v0.4.schema.json @@ -1,1554 +1,1586 @@ { - "$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." + "$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 \u2014 a JSON format for representing design system corpora. Backward-compatible with v0.1\u2013v0.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 \u2014 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 (the three original types) and the v0.4 specification (required-props, forbiddenCategories).", + "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." }, - "dspack": { - "type": "string", - "const": "0.4", - "description": "Specification version. MUST be \"0.4\" for documents conforming to this version." + "generatedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 datetime when the file was generated." }, - "name": { - "type": "string", - "minLength": 1, - "description": "Human-readable name of the design system." + "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": "Brief description of the design system's purpose and scope." + "type": "string", + "description": "What this category covers." }, - "version": { - "type": "string", - "description": "Version of the design system content (not the spec version)." + "tier": { + "type": "string", + "enum": [ + "primitive", + "semantic", + "component" + ], + "description": "Default abstraction level for tokens in this category." }, - "metadata": { - "$ref": "#/$defs/metadata" + "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." }, - "tokens": { - "type": "object", - "description": "Token definitions organized by category.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9-]*$" - }, - "additionalProperties": { - "$ref": "#/$defs/tokenCategory" - } + "description": { + "type": "string", + "description": "Semantic meaning of the token." }, - "components": { - "type": "object", - "description": "Component definitions keyed by component ID.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9-]*$" + "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" + ] }, - "additionalProperties": { - "$ref": "#/$defs/componentEntry" + { + "$ref": "#/$defs/statusObject" } + ], + "description": "Lifecycle stage. String for uniform status, object for per-platform granularity." }, - "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-]*$" + "aliasOf": { + "oneOf": [ + { + "type": "string", + "description": "Token name that this token aliases." }, - "additionalProperties": { - "$ref": "#/$defs/categoryEntry" + { + "$ref": "#/$defs/aliasReference" } + ], + "description": "Token that this token aliases. String for unambiguous names, object for cross-category disambiguation." }, - "patterns": { - "type": "array", - "description": "Pattern entries describing preferred ways of combining components.", - "items": { - "$ref": "#/$defs/patternEntry" - } + "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." }, - "antiPatterns": { - "type": "array", - "description": "Anti-pattern entries describing approaches that are deliberately ruled out.", - "items": { - "$ref": "#/$defs/antiPatternEntry" - } + "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." }, - "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" - } + "since": { + "type": "string", + "description": "Version of the design system content at which this stage took effect." }, - "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 (the three original types) and the v0.4 specification (required-props, forbiddenCategories).", - "items": { - "$ref": "#/$defs/ruleEntry" - } + "migrateTo": { + "type": "string", + "description": "Component ID or token name of the recommended replacement." }, - "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" - } + "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." }, - "frameworkBindings": { - "type": "object", - "description": "Framework-specific information keyed by framework identifier.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9-]*$" - }, - "additionalProperties": { - "$ref": "#/$defs/frameworkBinding" - } + "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." }, - "themes": { - "type": "object", - "description": "Named sets of token overrides representing alternative visual modes.", - "propertyNames": { - "pattern": "^[a-z][a-z0-9-]*$" + "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" + ] }, - "additionalProperties": { - "$ref": "#/$defs/themeEntry" + { + "$ref": "#/$defs/statusObject" } + ], + "description": "Lifecycle stage. String for uniform status, object for per-platform granularity." + }, + "accessibility": { + "$ref": "#/$defs/accessibilityConstraints" + }, + "composition": { + "$ref": "#/$defs/compositionRules" }, - "layout": { - "$ref": "#/$defs/layoutPrimitives" + "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 }, - "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 + "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)." }, - "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 + "description": { + "type": "string", + "description": "What this prop controls." }, - "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 + "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" + } + ] + } }, - "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 + "default": { + "description": "Default value of the prop." }, - "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 + "required": { + "type": "boolean", + "description": "Whether this prop must be provided.", + "default": false }, - "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 + "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." }, - "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 + "description": { + "type": "string", + "description": "When to choose this value." }, - "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 + "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." }, - "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 + "requiredAttributes": { + "type": "array", + "description": "HTML or ARIA attributes that must be present for correct accessible usage.", + "items": { + "$ref": "#/$defs/attributeDescriptor" + } }, - "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 + "keyboardInteractions": { + "type": "array", + "description": "Expected keyboard behaviors.", + "items": { + "$ref": "#/$defs/keyboardInteraction" + } }, - "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 + "contrastRequirement": { + "type": "string", + "description": "Minimum contrast ratio or WCAG level." }, - "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 + "focusManagement": { + "type": "string", + "description": "Prose description of focus behavior expectations." }, - "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 + "labelRequirement": { + "type": "string", + "enum": [ + "required-visible", + "required-accessible-name", + "required-aria", + "optional", + "none" + ], + "description": "How the component must be labeled." }, - "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 + "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)." }, - "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 + "description": { + "type": "string", + "description": "When and how to provide this attribute." }, - "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 + "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." }, - "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 + "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" + } }, - "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" + "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 \u2014 never skip silently. Contract severity uses RFC 2119 terms; tools map must\u2192error and should\u2192warn.", + "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\u2192error (triggers repair/failure) and should\u2192warn (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": { - "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 + "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 + } + } + } }, - "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.", + { + "if": { + "properties": { + "type": { + "const": "required-composition" + } + } + }, + "then": { "required": [ - "id", - "type", - "severity", - "rationale" + "component" + ], + "anyOf": [ + { + "required": [ + "requiredSubComponents" + ] + }, + { + "required": [ + "requiredProps" + ] + }, + { + "required": [ + "requiredCategories" + ] + } ], "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-": {} + "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." }, - "additionalProperties": false - }, - "examples": { - "type": "array", - "description": "IDs of examples demonstrating compliance; included in repair feedback as corrected references.", - "items": { - "type": "string" + "min": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Minimum number of occurrences among descendants." } - }, - "tags": { - "type": "array", - "description": "Free-form tags for grouping and search.", - "items": { - "type": "string" - } - } - }, - "allOf": [ - { - "if": { - "properties": { - "type": { - "const": "component-choice" - } - } + }, + "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." }, - "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" - } - } + "prop": { + "type": "string", + "description": "Prop name the constraint applies to." }, - "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 - } - } + "oneOf": { + "type": "array", + "description": "Allowed values; the prop MUST be present and take one of these.", + "minItems": 1 } - }, - { - "if": { - "properties": { - "type": { - "const": "required-props" - } - } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "minItems": 1 + }, + "requiredCategories": { + "type": "array", + "minItems": 1, + "description": "Categories from which at least `min` descendants MUST appear beneath each matching node. Membership within one category is naturally OR across its components; multiple entries are independently required (AND), matching requiredSubComponents. The 2026-08-07 amendment, lifted from the \u00a76 ceiling on measured evidence.", + "items": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Category id (must be registered in the document's categories)." }, - "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 text. Where the text may live is set by `textScope`: its own `text` field only (the default, \"self\") or anywhere in its subtree (\"subtree\")." - }, - "textScope": { - "type": "string", - "enum": [ - "self", - "subtree" - ], - "default": "self", - "description": "Where requiredText looks for the text: \"self\" (the node's own `text` field — the default) or \"subtree\" (direct text on the node or any descendant; for compound wrappers whose documented projections lift a label from within). Amendment 2026-07-04, on PR-15 evidence." - }, - "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 - } - }, - "dependentRequired": { - "textScope": [ - "requiredText" - ] - } + "min": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Minimum number of member descendants." } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false } - ] + } + } + } }, - "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" - ], + { + "if": { "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.", + "type": { + "const": "forbidden-composition" + } + } + }, + "then": { "required": [ - "name" + "component" + ], + "anyOf": [ + { + "required": [ + "forbiddenDescendants" + ] + }, + { + "required": [ + "forbiddenProps" + ] + }, + { + "required": [ + "forbiddenCategories" + ] + } ], "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-]*$" + "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." }, - "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-]*$" + "prop": { + "type": "string", + "description": "Prop name the constraint applies to." }, - "additionalProperties": { - "$ref": "#/$defs/subComponentBinding" + "values": { + "type": "array", + "description": "Forbidden values for the prop.", + "minItems": 1 } - } - }, - "additionalProperties": true + }, + "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 + } + } + } }, - "subComponentBinding": { - "type": "object", - "description": "Framework-specific details for a sub-component.", + { + "if": { "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.", + "type": { + "const": "required-props" + } + } + }, + "then": { "required": [ - "name", - "overrides" + "component" + ], + "anyOf": [ + { + "required": [ + "requiredText" + ] + }, + { + "required": [ + "requiredProps" + ] + } ], "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-]*$" + "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 text. Where the text may live is set by `textScope`: its own `text` field only (the default, \"self\") or anywhere in its subtree (\"subtree\")." + }, + "textScope": { + "type": "string", + "enum": [ + "self", + "subtree" + ], + "default": "self", + "description": "Where requiredText looks for the text: \"self\" (the node's own `text` field \u2014 the default) or \"subtree\" (direct text on the node or any descendant; for compound wrappers whose documented projections lift a label from within). Amendment 2026-07-04, on PR-15 evidence." + }, + "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." }, - "additionalProperties": { - "type": "string" + "oneOf": { + "type": "array", + "minItems": 1, + "description": "Optional allowed values; when present the prop value MUST be a member." } - } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "minItems": 1 + } }, - "additionalProperties": true + "dependentRequired": { + "textScope": [ + "requiredText" + ] + } + } + } + ] + }, + "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)." }, - "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 + "intent": { + "type": "string", + "description": "Intent ID this example demonstrates." }, - "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 + "name": { + "type": "string", + "description": "Human-readable display name." }, - "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 + "description": { + "type": "string", + "description": "What the example shows and why it is correct." }, - "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 + "prompt": { + "type": "string", + "description": "A representative user request this example answers; used as the user turn in few-shot blocks." }, - "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 + "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." }, - "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 + "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/spec/dspack-v0.4.md b/spec/dspack-v0.4.md index 6a4358f..a2f2cf6 100644 --- a/spec/dspack-v0.4.md +++ b/spec/dspack-v0.4.md @@ -200,6 +200,52 @@ The anchoring node itself is not a descendant of itself: a component that belong forbidden category may still anchor the rule (as `alert-dialog` — itself an overlay — does above). +### 4.3 `required-composition` — the `requiredCategories` field + +> **Amendment (2026-08-07), on measured evidence, lifted from §6.** The T1 +> representation milestone's Build evaluation (dspack-emit#31) produced +> lint-clean surfaces whose `form-control` nodes carried literal text and no +> interactive control at all — structurally valid, semantically empty form +> composition that every gate passed and the emitter (correctly) refused. The +> invariant "must contain an approved interactive control" is an OR across a +> category's members; `requiredSubComponents` can only AND exact ids, and +> category vocabulary existed only on the forbidden side. §6 anticipated +> exactly this addition "when a real contract needs them, not before" — this +> is that need, with the probe surfaces committed as evidence +> (dspack-emit `eval/t1-build-matrix*.json`). + +```json +{ + "id": "rule.form-control-carries-control", + "type": "required-composition", + "severity": "must", + "component": "form-control", + "requiredCategories": [{ "id": "interactive", "min": 1 }], + "rationale": "A form-control represents the location of the user-editable control in a field…" +} +``` + +`requiredCategories?: {id, min=1}[]` joins `requiredSubComponents` and +`requiredProps` (at least one of the three MUST be present). Each entry's `id` +MUST be registered in the document's `categories` — the same consistency check +`forbiddenCategories` carries. + +**Normative evaluation semantics.** For **every** node matching `component`: +each `requiredCategories` entry MUST have ≥ `min` descendants whose contract +entry declares membership in category `id` (one finding per violated entry, +located at the matching node). Membership is resolved through the contract's +`categories` declarations at lint time, exactly as in §4.2. The check is +**local to each matching node's descendants** — a member elsewhere in the +surface satisfies nothing. Multiple entries are independently required (AND), +matching `requiredSubComponents`; membership within one category is naturally +OR across that category's components. The finding's message MUST name the +required category and the count found, so repair feedback stays actionable +without the contract in hand. + +No boolean expressions, `oneOf` groups, or predicates: a requirement a +category cannot express is a missing category or a different rule, not a +grammar extension. + ## 5. Validation Gates S1, S2, and S3 are unchanged (v0.3 §8). In particular, S2 still checks the **full @@ -215,9 +261,10 @@ 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. +- **Category-based forms beyond §4.2 and §4.3** — `require`/`forbid` by category + in `component-choice`. Add them when a real contract needs them, not before. + (Category-scoped `required-composition` graduated to §4.3 on 2026-08-07, on the + T1 Build evidence — the first ceiling item to be lifted by measurement.) - **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.