Public Readiness
This document is the north-star analysis for dtcg-formulas as a public, installable companion pipeline for DTCG tokens. It names the objective, compares the project to prior art, enumerates the gaps between today and that objective, and proposes a phased rollout.
Objective
Any developer whose design system uses DTCG-latest-spec-compliant tokens should be able to:
- Pull this project into their pipeline in a streamlined way (one or two
npm installs). - Use the
$extensions.org.dtcg-formulassyntax to define computed token functions directly inside their tokens. - Run a build step that lints the formula syntax and writes the computed result back as each token's
$value.
The result is a companion pipeline to modern DTCG — tool-agnostic at its core, with first-class integrations (Style Dictionary, Terrazzo, raw JSON in/out) layered on top, so design engineers and front-end engineers can take advantage of useful computed functions defined inside the tokens themselves.
Design principles
- Tool-agnostic core. A pure resolver works on DTCG JSON in/out, with no hard dependency on Style Dictionary or any one build system.
- Streamlined DX. Install one package, point at
tokens.json, get computed tokens. Clear errors with codes and spans. Preflight (lint) and build (compile) are separate verbs. - Education built in. The repo teaches: how DTCG works, why formulas, how to author one, how to integrate.
- Extendable. Adapter packages plug in via a documented contract. Style Dictionary and Terrazzo integrations are thin wrappers over the core, not the core itself.
Where this project sits
DTCG reached its first stable Format Module v1 in October 2025. The ecosystem has converged on JSON tokens with $value / $type / $extensions, multi-file support, theming, and modern color (Display P3, Oklch, CSS Color 4). The compiler/build layer is well covered:
- Style Dictionary 4.x — DTCG-native build with transitive transforms, plugin-friendly transforms/formats/actions.
- Terrazzo (formerly Cobalt) — DTCG-native compiler with
@terrazzo/parsercore and@terrazzo/clithin wrapper. - design-tokens-format-module — TypeScript reference parser/validator with alias resolution.
- Tokens Studio — authoring surface that emits DTCG.
What none of these standardize is a way to define computed token formulas inside the tokens themselves. They resolve aliases, evaluate basic math expressions in some cases, but don't preserve formula provenance, don't share function definitions across tools, and don't have a vocabulary for higher-order operations like contrast-aware color, modular scales, or alpha compositing.
That's the gap dtcg-formulas fills: an authoring-time formula layer + an execution layer that any DTCG consumer can plug into.
Prior-art architectures we lean on
- Dart Sass — lexer → parser → AST → visitor-based evaluator, exposed as both CLI and JS API (
compile,compileString, async variants). Validates the "pure core + thin wrappers" split we're recommending. The visitor pattern is also the right shape for our resolver. - Style Dictionary — token traversal with transitive transforms that run after reference resolution. This is the right hook point for our SD plugin: the resolver runs as a transitive preprocessor, after SD has resolved DTCG aliases but before platform-specific format generation.
- Terrazzo —
@terrazzo/parserJS core +@terrazzo/clithin wrapper. Closest analogue for the CLI shape we want, and proves a tool-agnostic DTCG compiler has market demand.
Gap analysis
Tagging gaps by impact on the public-use objective:
Blockers for 0.1.0 (publishable) — ✅ resolved
These were the blockers before the first publish. Kept here as a record; all resolved as of 0.1.0 on npm.
No npm publication. Every package isprivate: trueat0.0.0. Nothing is installable.No CJS build. TypeScript-only output is fine for ESM consumers but breaks CJS callers.NopublishConfig, repo/bugs/homepage/keywords. Required for a credible npm presence.Workspace dep uses*notworkspace:*. Will publish a broken graph.No CI for tests/lint/typecheck. Only a Pages deploy exists today.- No CHANGELOG / release process. No automated versioning; can't communicate changes.
- No CONTRIBUTING / SECURITY / CODE_OF_CONDUCT. Standard bar for a public OSS library.
- No lint/format config. Style isn't enforced; PRs from outside contributors will drift.
Blockers for 0.2.0 (the compiler)
- No resolver.
@dtcg-formulas/resolveris a stub. The library cannot compute$valuefrom a formula. This is the single biggest blocker against the stated objective. - No CLI.
dtcg-formulas compile | lint | checkdoes not exist. - No executable built-ins.
round,clamp,min,max,mixare metadata only. - No diagnostics-first error model. Parser throws plain strings. Public users will demand codes, spans, and stable error contracts.
- No JSON Schema for
org.dtcg-formulas. Editor validation, the futurelintcommand, and downstream tools all need this.
Important for 0.3.0 (integrations)
- No adapter executability contract. Adapters like
leo(),composite(),optimal-foreground(),material-shadow()are metadata only. They need a documentedimplement(args, ctx)contract and shipped JS implementations. - No Style Dictionary plugin implementation. The package exists as a stub.
- No docs generator. Site is hand-curated; should be derivable from
.module.scssdefmetadata.
Nice-to-have
- Spec/docs minor inconsistency. The README/spec use
"call"for the formula string; doc examples use"formula". Pick one (recommend"formula", since that's what's already in user-facing docs) and align the spec. - Editor LSP. Once the JSON Schema and diagnostic codes are stable, an LSP becomes straightforward.
- Generator/recipe model. One-to-many token derivation isn't in v0; revisit when the scalar story is solid.
Recommended architecture
Five packages, cleanly separated:
@dtcg-formulas/parser parses .module.scssdef files
@dtcg-formulas/registry stores function declarations
@dtcg-formulas/resolver pure core: walks DTCG, resolves refs, invokes impls, writes $value
@dtcg-formulas/builtins executable JS for math built-ins (paired with definitions)
@dtcg-formulas/cli thin CLI: compile | lint | checkPlus opt-in adapter packages (@dtcg-formulas/adapter-leonardo, -color-names, -composite, -optimal-foreground, -material-shadow, etc.) and integration wrappers (@dtcg-formulas/style-dictionary-plugin, @dtcg-formulas/terrazzo-plugin).
Adapter packaging contract
Every adapter package exports two things:
import scssdefSource from './leonardo.module.scssdef?raw';
export const definition = parse(scssdefSource);
export function implement(args, ctx) {
// Pure JS implementation of the function.
// args: positional arguments, already resolved to literals.
// ctx: { tokenPath, registry, diagnostics }
// Returns the computed value.
}Adapters are opt-in dependencies for consumers — nobody pays for Leonardo unless they actually use leo(). The resolver auto-loads adapters listed in its config and routes calls by function name.
Resolver contract
import { compile } from '@dtcg-formulas/resolver';
const { tokens, diagnostics } = await compile(input, {
definitions: ['tokens/functions/**/*.module.scssdef'],
adapters: ['@dtcg-formulas/adapter-leonardo'],
});Pure function: same input, same diagnostics, same output. No I/O beyond what's in the config. No SD or Terrazzo dependencies in the core.
CLI verbs
compile— full resolution; writes a new tokens file with computed$values.lint— preflight only; parses every formula, validates against the registry, checks references and arity, never executes a formula. Adapter packages don't need to be installed forlintto run.check— dry-run compile; produces the full diagnostic report without writing output. Useful for CI.
Separating lint from compile is deliberate: preflight should be cheap, fast, and adapter-free so a CI job can fail-fast on syntax problems without pulling in heavy color or contrast engines.
Preflight contract
dtcg-formulas lint guarantees, without executing any formula:
- The input JSON parses.
- The token tree is valid DTCG.
- Every
$extensions.org.dtcg-formulasentry is well-formed (formula,definition, optionalsyntax). - Every
definitionref resolves to a registered function. - The
formulacall string parses. - The call's arity and argument shapes match the registered declaration.
- Every
{path.to.token}reference inside the call resolves to a token in the tree. - No cycles exist in the reference graph.
Anything that requires running a function (e.g. checking that clamp(min, mid, max) actually has min <= max) is reported by compile or check, not lint.
Error model
Every diagnostic carries:
interface Diagnostic {
code: string; // 'DTCG-FORMULAS-L107'
severity: 'error' | 'warning';
message: string;
span?: { file: string; line: number; column: number; length: number };
hint?: string;
docsUrl?: string; // points at /guide/troubleshooting#code
}Code prefixes:
P###— parse (.module.scssdef)L###— lint / preflightR###— resolve (execution)V###— DTCG$valuevalidation
Mirrors how dart-sass and TypeScript publish diagnostics. The full code reference lives in Troubleshooting.
Versioning & release
- Semver per package.
- Internal deps use
workspace:*(or pinnedworkspace:^X.Y.Z). - Changesets owns the release flow: every PR adds a
.changeset/*.mddescribing user-facing impact, and a release workflow opens a Version PR that bumps versions, updatesCHANGELOG.mdfiles, and publishes to npm on merge. @dtcg-formulas/specversions the syntax (scssdef@0.1,extension@0.1) independently of the runtime packages.
Phased rollout
| Version | Ships | Public DX |
|---|---|---|
| 0.1.0 (released) | Published parser + registry + spec, full education surface, architecture analysis, CI, release plumbing | Authoring + registration + documentation. No execution yet. |
| 0.2.0 | resolver + builtins + cli (compile/lint/check), JSON Schema, diagnostics-first errors | End-to-end DX is real. Bare-JSON consumers can install one CLI and compile their tokens. |
| 0.3.0 | Executable adapter packages (leonardo, color-names, composite, optimal-foreground, material-shadow), Style Dictionary plugin, docs generator | Color and contrast adapters ship. SD users get a turnkey integration. |
| 0.4.0+ | Remaining adapters, Terrazzo plugin, editor LSP, generator/recipe exploration | Full ecosystem coverage; editor-grade authoring. |
Open questions
- Namespaced calls. Should
leonardo.color(...)be first-class in the parser, or always rewritten toleo(...)? Affects how adapters declare themselves. - Inline expressions. Should the resolver support expressions beyond function calls (
{a.b} + 4px) in v0, or stay strict-call-only? (Per vision.md open question #3.) - Adapter testing. How do we test adapters that wrap non-deterministic external engines (color rounding, LCH gamut clipping)? Snapshot? Tolerance bands?
- Spec/docs key alignment. Spec source under
packages/spec/formula-extension-spec.mduses"call"; user-facing docs settled on"formula". Decision pending: align the spec to match"formula"in a 0.1.x patch, or migrate docs back to"call". Either way, decide before 0.2.0 publishes any tooling that hardcodes the key name.