Replace the useSchema / Type class system with plain rune identifiers
Context
Every rune in refrakt — core and community — goes through a three-file ceremony to register its identity:
Schema class (e.g.
packages/runes/src/schema/hint.ts):export class Hint { hintType: 'check' | 'note' | 'warning' | 'caution' = 'note'; }Registry entry (e.g.
packages/runes/src/registry.ts):Hint: useSchema(Hint).defineType('Hint'),Usage in the tag's transform (e.g.
packages/runes/src/tags/hint.ts):return createComponentRenderable(schema.Hint, { tag: 'section', ... });
The useSchema() factory creates a TypeFactory, which creates a Type instance carrying:
name— a string like"Hint"schemaCtr— the class constructorcontext— always{}schemaOrgType— an optional string like"FAQPage"create()— a method that instantiates the class
At runtime, createComponentRenderable reads exactly two values from the Type object:
'data-rune': toKebabCase(type.name), // "Hint" → "hint"
typeof: result.typeof ?? type.schemaOrgType, // e.g. "FAQPage"
The create() method is never called anywhere in the codebase. The context record is always empty. The class constructor stored in schemaCtr is never used after registration. The schema class fields and their default values are never read at runtime.
This system was designed early in the project anticipating that Type would do more — validate attributes against the class shape, instantiate typed models, enable IDE autocompletion on rune properties. That vision didn't materialize because createContentModelSchema + the engine config became the real workhorses. The schema classes are now vestigial.
Problems
Three files to carry two strings. A rune name and an optional schema.org type are the only runtime-relevant data, but they require a class definition, a registry line, and an import chain.
Misleading semantics. The class syntax implies fields and types matter at runtime. New rune authors see
hintType: 'check' | 'note' | 'warning' | 'caution' = 'note'and reasonably assume this drives validation or defaults. It doesn't — the Markdocattributesdefinition in the tag schema and the engine config'smodifiersare what actually matter. The schema class is a third, disconnected description of the same information.Triple redundancy for modifier fields. A modifier like
hintTypeis declared in:- The schema class (
schema/hint.ts) — unused at runtime - The Markdoc tag attributes (
tags/hint.ts) — drives parse-time validation - The engine config (
config.ts) — drives theme-time BEM classes and data attributes
- The schema class (
Community package friction. Every community package must replicate the pattern: a
schema/directory with classes, atypes.tsregistry file withuseSchemacalls, and imports threading through to each tag. This is ~8-15 lines of boilerplate per rune that does nothing.RuneDescriptor.typeon theRuneclass. TheRuneclass (packages/runes/src/rune.ts) carries an optionaltype: Typefield. The only consumer isrefrakt inspect, which readsrune.type?.nameto display the typeof value. This could trivially be a plain string field.
Decision
Option 2: Inline name and schemaOrgType into TransformResult. This is the cleanest end state and eliminates the concept entirely rather than just simplifying it.
The migration is mechanical: every createComponentRenderable(schema.X, { ... }) call becomes createComponentRenderable({ rune: 'x', ... }), optionally adding schemaOrgType for runes that have one. This can be done incrementally — support both signatures during a transition period.
Migration Strategy
Phase 1: Dual-signature support (non-breaking)
Update
createComponentRenderableto accept either the currentTypefirst argument or a unifiedTransformResultwith arunefield:export function createComponentRenderable( typeOrResult: Type | UnifiedTransformResult, result?: TransformResult ): TagWhen
typeOrResulthas aruneproperty, use the new path. Otherwise fall back to the existingType-based path.Update the
RuneDescriptorandRuneclass: replacetype?: TypewithtypeName?: stringandschemaOrgType?: string.Update
refrakt inspectto readrune.typeNameinstead ofrune.type?.name.
Phase 2: Migrate runes (incremental, package-by-package)
Core runes (
packages/runes/src/tags/*.ts): Update each tag'stransformfunction to use the new signature. Remove the corresponding schema class import and registry entry.Community packages (
runes/*/src/tags/*.ts): Same migration per package. Remove each package'sschema/directory andtypes.tsregistry when all its runes are migrated.Order doesn't matter — both signatures work simultaneously.
Phase 3: Cleanup (breaking, major version)
- Remove the old
Typesignature fromcreateComponentRenderable. - Delete
packages/types/src/schema/index.ts(theType,TypeFactory,useSchemaexports). - Delete all
schema/directories and registry files across core and community packages. - Remove
useSchemafrom the public API of@refrakt-md/types. - Update authoring docs to remove references to schema classes and
useSchema.
Scope estimate
- ~100 runes to update (37 core + ~65 community)
- Each update is a 2-3 line change in the transform function
- ~30 schema class files to delete
- ~10 registry files to delete
- ~1 function signature to update (
createComponentRenderable) - ~1 interface to update (
RuneDescriptor) - ~4 lines in
inspect.tsto update
The migration is wide but shallow — no logic changes, just plumbing.
Consequences
Positive:
- Rune authoring drops from 3 concepts (schema class + registry + tag) to 1 (tag with inline metadata)
- No more triple-redundancy for modifier field declarations
- Community package scaffolding becomes simpler — no
schema/directory ortypes.tsneeded - The authoring docs can remove the "Type definition" step entirely
createComponentRenderablebecomes self-documenting — all inputs visible at the call site
Negative:
- If a future need arises for runtime type instances (validation, editor introspection based on class shapes), that capability would need to be rebuilt. However, the Markdoc
attributesdefinition already serves this purpose and is the authoritative source. - Wide migration surface means this should be coordinated, not done piecemeal across unrelated PRs.
Neutral:
- The
data-runeandtypeofHTML attributes are unchanged. No theme or CSS impact. - The engine config, identity transform, and rendering pipeline are completely unaffected.
- Existing content (Markdown files) requires zero changes.
Relationships
Related
- WORK-105doneworkAdd dual-signature support to createComponentRenderable and update RuneDescriptor
- WORK-107doneworkMigrate community package runes to inline rune identifiers
- WORK-106doneworkMigrate core runes to inline rune identifiers
- WORK-109doneworkRemove Type system — delete schema classes, registries, and old signature
- WORK-108doneworkUpdate inspect tooling for string-based rune types