WORK-608
ID:WORK-608Status:done

Promote the three styles transform helpers into the named-transform enum

RuneConfig.styles[…].transform is one of only two function-typed fields in the whole of RuneConfig (packages/transform/src/types.ts:301; the other is postTransform, which stays imperative by design). Unlike postTransform, it does not need to be a function: a declarative sibling already ships, and the function form is used by a closed set of three helpers.

Priority:mediumComplexity:simpleMilestone:v0.38.0Source:SPEC-143
claude/v0-37-post-release-plan-dc3va1 View source

Criteria completion

Criteria completion: 8 of 8 (100%) checked; history from Oct 6 to Oct 60%25%50%75%100%Oct 6Oct 6
Branches 4
History 3
  1. 222314e
    • ☑ `npx refrakt contracts --check` reports no drift on either contract copy
    by bjornolofandersson
  2. 6129b8c
    • ☑ `'valign'`, `'ratio-fr'` and `'gap'` (or better names) join the named-transform enum, with the existing three
    • ☑ All 11 function-form sites are converted to the string form
    • ☑ The function form is removed from the `styles` type in `packages/transform/src/types.ts`, so it cannot come back
    • ☑ `postTransform` is the only remaining function-typed field in `RuneConfig`, asserted by a test rather than by inspection
    • ☑ `npm run seo:baseline:check` reports no drift
    • ☑ `npx vitest run packages/lumina/test/css-coverage.test.ts` passes unchanged
    • ☑ A `RuneConfig` round-trips through `JSON.parse(JSON.stringify(…))` unchanged for every core and plugin rune
    by bjornolofandersson
  3. 11b89a1
    Created (in-progress)by bjornolofandersson

Why

Two payoffs, either of which would justify it alone:

It makes RuneConfig serialisable. A RuneConfig with no function values can cross a JSON boundary, which is what a user-authored or hosted rune definition requires — see SPEC-143's analysis of the engine config as the second half of a rune's payload. The plugin boundary is already typed as opaque data (PluginThemeConfig.runes?: Record<string, Record<string, unknown>>, packages/types/src/package.ts:48), so this closes the gap between how that boundary is typed and what actually passes through it.

It removes a cross-package import for a string. Today a plugin config must import resolveValign from a shared module to set one CSS custom property. Nine of the eleven sites are that single helper, imported into six different config files.

Acceptance Criteria

  • 'valign', 'ratio-fr' and 'gap' (or better names) join the named-transform enum, with the existing three
  • All 11 function-form sites are converted to the string form
  • The function form is removed from the styles type in packages/transform/src/types.ts, so it cannot come back
  • postTransform is the only remaining function-typed field in RuneConfig, asserted by a test rather than by inspection
  • npx refrakt contracts --check reports no drift on either contract copy
  • npm run seo:baseline:check reports no drift
  • npx vitest run packages/lumina/test/css-coverage.test.ts passes unchanged
  • A RuneConfig round-trips through JSON.parse(JSON.stringify(…)) unchanged for every core and plugin rune

Approach

The enum lives in two places in types.ts (:120 and :574) and is resolved in one (engine.ts:1130) — extend all three together. The existing three entries are value formatters taking a string and returning a string, which is exactly the shape the three helpers have, so no new dispatch mechanism is needed.

Name the entries after what they produce rather than after the helper, since the helper names are internal: resolveValign maps an alignment keyword to a flexbox value, ratioToFr turns "1 2" into "1fr 2fr", resolveGap maps a gap keyword to a token reference. Read each helper before naming it — the names above are provisional.

Deleting the function form from the type is what makes this stick; leaving it in place means the next config to need a transform reaches for a closure again.

References

  • SPEC-143 — where the requirement surfaced: the engine config has to travel with a serialised rune
  • SPEC-140 — transform boilerplate consolidation; shares the serialisability goal
  • SPEC-081 — declarative structure assembly; why postTransform stays a function

Resolution

Completed: 2026-10-06

Branch: claude/v0-37-post-release-plan-dc3va1

What was done

  • packages/transform/src/types.ts: NamedTransform = 'duration' | 'uppercase' | 'capitalize' | 'align' | 'fr' | 'gap', shared by meta fields, structure entries and styles; styles[…].transform accepts only a name. Exported from @refrakt-md/transform.
  • packages/transform/src/engine.ts: align (resolveValign), fr (ratioToFr), gap (resolveGap) join the one transform table; styles dispatches through it. The helpers stay exported.
  • packages/transform/src/contracts.ts: the second copy of the styles type (inlineStyles) narrowed the same way — found by the build, not the survey.
  • 11 function sites → strings: packages/runes/src/config.ts (grid ×3, split), learning, marketing ×3, media, storytelling ×2; the now-unused helper imports dropped.
  • packages/lumina/test/rune-config-serialisable.test.ts: across 131 core + plugin rune configs, postTransform is the only function, and every config minus postTransform round-trips through JSON.
  • site/content/extend/theme-authoring/config-api.md: the transform form of styles and the shared vocabulary table.

Notes

  • Contracts gained the transform names — accepted, not byte-identical. The 14 styles entries now carry "transform": "align" | "fr" | "gap"; as functions they were silently dropped by JSON.stringify. Additive only; both copies regenerated and --check passes. Kept by decision on #659: the contract now records how each value is computed.
  • Rendered HTML is identical across every rune × variant on both sites (inspect --json, 3,890 outputs); the only diff is the same names in the config inspect echoes.
  • Breaking for a published type → minor changeset on @refrakt-md/transform.