Skip to content

Commit a467ca3

Browse files
GarthDBclaude
andcommitted
feat(sdk,spec): Phase 2 cascade token format, resolution engine, migration tooling (#770)
* docs(spec): resolve Wave 0 decisions for Phase 2 cascade work - spec/cascade.md: replace implementation-defined tie-breaking with normative document-order rule (earlier in array wins; lexicographically earlier file path wins across files); closes #757 - spec/cascade.md: add Alias resolution section specifying that $ref chains MUST resolve after cascade selection; closes #758 - rules/rules.yaml: update SPEC-006 message and assert to reference the deterministic tie-breaking algorithm - spec/token-format.md: add RFC #646 relationship section documenting that the flat name object is canonical, and how it differs from the RFC's name.structure / name.original shape; closes #759 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(spec): cascade file schema, dimension declarations, document shape - schemas/cascade-file.schema.json: new Layer 1 schema for cascade token files; ordered array of token objects with document-order tie-breaking semantics; closes #756 - spec/token-format.md: add document shape section specifying array envelope, .tokens.json extension recommendation, and example - dimensions/color-scheme.json: declare colorScheme dimension (light, dark, wireframe; default: light) - dimensions/scale.json: declare scale dimension (desktop, mobile; default: desktop; keeps legacy mode names per phase 2 decisions) - dimensions/contrast.json: declare contrast dimension (regular, high; default: regular) - spec/dimensions.md: update built-in dimensions table with actual modes and defaults; add dimension catalog section with discovery convention; closes #746 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk): cascade resolution engine, dimension loader, CLI resolve command graph.rs: - add index field to TokenRecord for document-order tie-breaking - extend from_json_dir to handle cascade-format array files - add load_spec_dimensions() to load from spec dimensions/ catalog cascade.rs (new): - ResolutionContext builder for dimension → mode pairs - specificity(): count non-default dimension fields; closes #745 - matches_context(): wildcard matching for absent dimensions; closes #761 - resolve(): full cascade algorithm (filter → specificity → tie-break) with 8 unit tests; closes #744 cli/src/main.rs: - add --dimensions-path flag to validate command; closes #760 - add resolve subcommand with --color-scheme, --scale, --contrast flags and pretty/json output formats; closes #764 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk): set-to-cascade migration converter and CLI convert command Implements Wave 3 of Phase 2: - sdk/core/src/migrate.rs: convert_token() splits color-set/scale-set entries into individual cascade tokens; convert_dir() processes a directory of legacy files and writes .tokens.json output files. Alias syntax `value: "{foo}"` → `$ref: "foo"`, outer lifecycle fields propagate to all mode tokens, entry-level values override outer. 8 unit tests covering all conversion paths. - sdk/core/src/lib.rs: exports pub mod migrate - sdk/cli/src/main.rs: adds `design-data migrate convert <INPUT> --output <OUTPUT>` subcommand (closes #765) - sdk/core/src/validate/structural.rs: structural validator now accepts cascade-format files (top-level JSON arrays) in addition to legacy object maps — validates each element against its individual $schema (closes #767) Closes #743, #765, #767 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk): add SPEC-008 cascade-completeness and SPEC-009 name-field-enum-sync rules Wave 4 of Phase 2: - packages/design-data-spec/rules/rules.yaml: adds SPEC-008 and SPEC-009 rule definitions to the normative catalog - sdk/core/src/validate/rules/spec008.rs: warns when a cascade token property has non-default dimension mode variants but no base/default variant, which would cause resolution gaps (closes #762) - sdk/core/src/validate/rules/spec009.rs: stub for registry/enum sync validation; emits no diagnostics until design-system-registry data is threaded into ValidationContext (closes #763 — structure in place) - sdk/core/src/validate/rules/mod.rs: wires SPEC-008 and SPEC-009 into the default rule set 4 new tests covering spec008 base/default/no-dim-declarations behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk,spec): resolution conformance and migration roundtrip test suites Wave 5 of Phase 2: - packages/design-data-spec/conformance/resolution/: three fixture cases covering base-fallback (wildcard), specificity-wins, and alias-resolved-after-cascade scenarios (closes #768) - sdk/core/src/lib.rs: resolution_conformance module drives all three fixtures against the real cascade::resolve() engine; migration_roundtrip module verifies color-set and scale-set conversion outputs are loadable and resolve correctly (closes #769) - relational_conformance: adds spec008 conformance test - packages/design-data-spec/conformance/README.md: documents resolution fixture format and table 46 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk,spec): dimensions in validate pipeline, SPEC-008 fixture, legacy output generator Three gap-fills for the Phase 2 PR: - sdk/core/src/validate/mod.rs: adds validate_all_with_options() that accepts an optional dimensions_path; loads spec-format dimension declarations into the graph before running relational rules so that SPEC-008 can fire during 'design-data validate'. Previous validate_all_with_exceptions() now delegates to this function. - sdk/cli/src/main.rs: wires --dimensions-path into run_validate() via validate_all_with_options(); adds 'design-data migrate legacy-output <INPUT> --output <OUTPUT>' subcommand (closes #766) - packages/design-data-spec/conformance/invalid/SPEC-008/: portable fixture files (cascade tokens + dimension declaration + expected-errors) that any implementation can use to verify SPEC-008 enforcement - sdk/core/src/legacy.rs: inverse of migrate.rs — converts cascade .tokens.json arrays back to legacy set-format JSON objects; groups tokens by property, reconstructs color-set/scale-set wrappers, hoists consistent lifecycle fields to outer level, denormalizes $ref back to value: "{foo}" alias syntax; 7 unit tests (closes #766) 53 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): p1 cascade duplicate uuid/name dropped before validation, multi-dim lossy output - graph: key cascade tokens by file:index (always unique) instead of uuid/serialized-name so SPEC-004 and SPEC-006 see all duplicates before deduplication - graph: add uuid_index (uuid -> primary key) for alias $ref resolution in resolve_leaf - legacy: collect_dimension_keys replaces detect_dimension_key; returns BTreeSet of all recognized dimension keys in a property group - legacy: convert_array now returns Result; errors with MultiDimensionalToken when a property group spans more than one dimension (colorScheme x scale etc.) instead of silently emitting a lossy legacy file - lib: add MultiDimensionalToken CoreError variant - lib: add TempDir-isolated regression tests for SPEC-004 and SPEC-006 cascade paths - core: add tempfile dev-dependency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b0a7864 commit a467ca3

41 files changed

Lines changed: 2632 additions & 43 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/design-data-spec/conformance/README.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,38 @@
22

33
Each **invalid** case lives under `invalid/<RULE_ID>/` with:
44

5-
- One or more JSON **fixture** files (structurally valid for Layer 1 when targeting Layer 2 rules).
6-
- **`expected-errors.json`** — expected diagnostics after Layer 2 validation (see `errors[].rule_id`, `severity`, optional `message_pattern`).
5+
* One or more JSON **fixture** files (structurally valid for Layer 1 when targeting Layer 2 rules).
6+
* **`expected-errors.json`** — expected diagnostics after Layer 2 validation (see `errors[].rule_id`, `severity`, optional `message_pattern`).
77

88
**Valid** baselines live under `valid/`.
99

10-
| Folder | Rule | Intent |
11-
| ------------------ | -------- | ------------------------------------------------- |
12-
| `invalid/SPEC-001` | SPEC-001 | Alias target does not exist. |
13-
| `invalid/SPEC-002` | SPEC-002 | Alias resolves to incompatible type (semantic). |
14-
| `invalid/SPEC-003` | SPEC-003 | Circular alias chain. |
15-
| `invalid/SPEC-004` | SPEC-004 | Duplicate `uuid` across tokens. |
16-
| `invalid/SPEC-005` | SPEC-005 | Dimension `default` not in `modes`. |
17-
| `invalid/SPEC-006` | SPEC-006 | Ambiguous resolution / specificity tie (warning). |
10+
| Folder | Rule | Intent |
11+
| ------------------ | -------- | ------------------------------------------------------- |
12+
| `invalid/SPEC-001` | SPEC-001 | Alias target does not exist. |
13+
| `invalid/SPEC-002` | SPEC-002 | Alias resolves to incompatible type (semantic). |
14+
| `invalid/SPEC-003` | SPEC-003 | Circular alias chain. |
15+
| `invalid/SPEC-004` | SPEC-004 | Duplicate `uuid` across tokens. |
16+
| `invalid/SPEC-005` | SPEC-005 | Dimension `default` not in `modes`. |
17+
| `invalid/SPEC-006` | SPEC-006 | Ambiguous resolution / specificity tie (warning). |
18+
| `invalid/SPEC-008` | SPEC-008 | Non-default mode variants with no base/default variant. |
1819

1920
Implementors SHOULD run these fixtures once the Rust validator exposes rule IDs ([#724](https://github.com/adobe/spectrum-design-data/issues/724), [#725](https://github.com/adobe/spectrum-design-data/issues/725)).
21+
22+
***
23+
24+
## Resolution conformance fixtures
25+
26+
Each **resolution** case lives under `resolution/<name>/` with:
27+
28+
* `input/` — cascade-format `.tokens.json` files
29+
* `dimensions/` — (optional) dimension declaration JSON files
30+
* `query.json``{ "property": "...", "context": { ... } }` — the resolution query
31+
* `expected.json``{ "resolved": bool, "expected_uuid": "..." }` — expected outcome
32+
33+
| Folder | Intent |
34+
| ----------------------------------------- | -------------------------------------------------------------------- |
35+
| `resolution/base-fallback` | Dimensionless base token MUST match any context (wildcard behavior). |
36+
| `resolution/specificity-wins` | Higher-specificity variant MUST win over base when both match. |
37+
| `resolution/alias-resolved-after-cascade` | Cascade selects the winner first; alias `$ref` chain resolves after. |
38+
39+
The Rust SDK drives these fixtures in `sdk/core/src/lib.rs` (`resolution_conformance` module, closes [#768](https://github.com/adobe/spectrum-design-data/issues/768)).
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "colorScheme",
3+
"modes": ["light", "dark", "wireframe"],
4+
"default": "light"
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"description": "A cascade token property that has non-default colorScheme variants (dark, wireframe) but no base/default variant (dimensionless or explicit light) MUST trigger SPEC-008.",
3+
"errors": [
4+
{
5+
"rule_id": "SPEC-008",
6+
"severity": "warning",
7+
"message_pattern": "background-color-default"
8+
}
9+
]
10+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[
2+
{
3+
"name": { "property": "background-color-default", "colorScheme": "dark" },
4+
"value": "#000000",
5+
"uuid": "spec008-dark-4000-8000-000000000001"
6+
},
7+
{
8+
"name": {
9+
"property": "background-color-default",
10+
"colorScheme": "wireframe"
11+
},
12+
"value": "#f5f5f5",
13+
"uuid": "spec008-wire-4000-8000-000000000002"
14+
}
15+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "colorScheme",
3+
"modes": ["light", "dark", "wireframe"],
4+
"default": "light"
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"description": "Cascade selects the winning token first (by specificity). Alias chains ($ref) are resolved AFTER the cascade winner is determined — not before. The winner here is the dark alias token; following its $ref yields blue-600.",
3+
"resolved": true,
4+
"expected_uuid": "cccccccc-0001-4000-8000-000000000001"
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"name": { "property": "action-color-default", "colorScheme": "dark" },
4+
"$ref": "cccccccc-0002-4000-8000-000000000002",
5+
"uuid": "cccccccc-0001-4000-8000-000000000001"
6+
},
7+
{
8+
"name": { "property": "blue-600" },
9+
"value": "#0050b3",
10+
"uuid": "cccccccc-0002-4000-8000-000000000002"
11+
}
12+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"property": "action-color-default",
3+
"context": { "colorScheme": "dark" }
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "colorScheme",
3+
"modes": ["light", "dark", "wireframe"],
4+
"default": "light"
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"description": "When only a base (dimensionless) variant exists, it MUST match any context. Querying for 'dark' resolves to the base token.",
3+
"resolved": true,
4+
"expected_uuid": "aaaaaaaa-0001-4000-8000-000000000001"
5+
}

0 commit comments

Comments
 (0)