Character Sheet API 0.1 — De Facto Specification
Status: Descriptive — documents the contract as it ships on branch
version/0.43.0(platform contract skeleton + Play Mode) and the System 8 moduleversion/system8-0.5.0(reference implementation). Versioned byMODULE_API_VERSION1.7.0 (there is no separate constant). Quirks and deferrals flagged ⚠︎.1.6.0 (Character Sheet Overhaul, Phase 0) adds CS-1.6
formatAttribute(attribute, { surface, format })— pure, synchronous display formatting,nullto fall through. It is tier 2 of the resolution chain (campaign-scoped override → module →String(value)), which moves the stock formatters out of seededrenderFunctionstrings and into module code. The same release makesIPropTypethe single attribute-type registry: CS-1.2'sNumericAttributeFormatis deprecated in favour of apropTypereference. Seemodule-api-1.6.md.Promotes the committed draft
4-system-platform-separation/character-sheet-api.md(retired there) to the as-built reference. Master spec:4-system-platform-separation/requirements.md(R1).
1. What it is
The Character Sheet API is an SDK, not a REST API. The platform ships generic
React sheet primitives and generic logic runners; a system module supplies
(a) declarative schemas/vocabularies, (b) pure logic functions
(project / validate / suggest / generate), and (c) optional escape-hatch React
components — the exact {schema + logic + component} split already used by
ArtworkSourceExtension / DiceRollerExtension.
It is not a parallel registry. Every surface is an optional member on the
existing SystemModule (systems/types.ts),
loaded through the existing SystemModuleExtension.loadModule() and
moduleRegistry. A module that implements nothing new still loads and renders a
no-validation sheet (today's custom behavior); surfaces degrade independently.
The platform keeps exactly one piece of system knowledge: how to load a module
by systemId and render/execute the generic shapes it returns.
2. Version
Ships as part of Module API 1.5.0 (MODULE_API_VERSION,
module.ts). All members are
additive and optional, so dnd5e-2014 / dnd5e-2024 / system8 / custom
keep satisfying SystemModule unchanged. Modules declare the range they target
via their engine field; the platform negotiates compatibility from that.
3. The seven-plus-one capability surfaces (CS-1 … CS-8)
All types in systems/types.ts
unless noted. All members optional on SystemModule.
CS-1 — Schema & vocabulary (sheetSchema?: CharacterSheetSchema)
The module declares, as data, the character-model vocabulary the generic sheet renders — no platform component contains a system-specific enum or label:
| Part | Type | Declares |
|---|---|---|
| CS-1.1 ability scores | AbilityScoreSchema |
ability names + shortAliases, ordered components (label/help/editable), maxBaseScore, level-aware secondaryBonusCapAtLevel, generationMethods |
| CS-1.2 numeric attributes | NumericAttributeSchema[] |
HP/AC/Speed with format (integer|range|d20-modifier) + editMode (standard|override) |
| CS-1.3 trait channels | TraitChannelSchema[] |
per generic channel (traits/equipment): subtype taxonomy (TraitSubtypeSchema: label/color/icon + per-subtype renderMode/slotSchedule), catalog flag |
| CS-1.4 proficiency ladder | ProficiencyTierLadder |
ordered tiers + allowExpertise (absent for systems with no skills) |
| CS-1.5 derived-stat panels | DerivedStatPanel[] |
{ id, label, compute(character) → DerivedStat[] } |
⚠︎ Global trait-slug uniqueness (CS-1.3): trait slugs must be unique across all subtypes in a system (a feat and a spell both named "Great Weapon Master" →
great-weapon-master-feat/-spell). The module may auto-namespace by subtype (System 8 does — seeprojection.tsgetTraits).
CS-2 — Read projection (construct + accessors)
One set of pure methods, consumed by both the build sheet and the token projection — collapsing the "two pipelines" problem:
construct?(record): Character | Promise<Character> // choice record → complete manifest
getAttributes?(c): ProjectedAttribute[] // abilities + numerics, post-derivation
getTraits?(c): ProjectedTrait[] // feats/powers/… as generic traits
getActions?(c): IAction[] // the platform's existing IAction
getDerivedStats?(c): DerivedStat[] // CS-1.5 panels, evaluated
construct turns a persisted choice record into a complete generic
manifest (primary attributes + derived attributes/traits/actions the record
never stored); the accessors are facets of that manifest. Return types are the
platform's existing generic shapes (IAction from
vtt.ts, a
CharacterAttribute-shaped ProjectedAttribute).
CS-3 — Validation (validate / validateEdit)
validate?(c): ValidationIssue[]— whole-character (composes the existingabilityScores.validate*as specializations).validateEdit?(before, change): EditValidation— enforce-mode verdict for a single proposedEditIntent. ⚠︎ Net-new; no Edit-Mode UI wired this project.
CS-4 — Automation / auto-fill (autoFill?: AutoFillEngine)
Line / section / character auto-fill, each returning a CharacterDelta
(Partial<Character>) the platform applies. Hard constraint (CS-4.2):
autoFill.fillCharacter is the same function the Monster API's
generateCharacter composes — one engine, no parallel monster stack.
CS-5 — Content / choice provider (contentProvider?: ContentProvider)
Where character-building content comes from — see the standalone
Content API. Consumed via listTraits(type, filter),
getTraitChoices(c, slot), listEquipment(filter), listMonsters(filter).
CS-6 — Token projection (projectToken) & action derivation
projectToken?(character): IToken | Promise<IToken> — the single idempotent
Character→Token converter; the home for every transform
characterToToken.ts hardcodes
(standing reactions, attack-bonus, damage tiers + action code, HP/AC, ability
map, power→action). CS-2's accessors and projectToken share one interpretation
layer, so a sheet Action button is a view of the same derivation the map
consumes. Emits the code strings the platform's
Action API sandbox executes.
- Idempotent / deterministic given
(character, module data). Determinism is with respect to module-provided content (fetched/cached via CS-5). - Derived ⊕ runtime is a MERGE, never a clobber
(
mergeProjectedToken.ts): preservex,y/current HP/conditions/instance overrides. Range Current/Max reconciliation — shift Current by the Max delta, clamp[0, newMax](reconcileRange). Cross-map continuity default: PC = move, monster = spawn (defaultCrossMapContinuity).
⚠︎ Planned change — CS-6 becomes the propagation primitive
Not yet implemented; recorded here so module authors are not surprised. The Character Sheet Overhaul's refactor makes a linked token a one-way projection of its character, which changes when
projectTokenruns and what the merge preserves. Tracked as tasks P3 / P4 in todo-v2.md.1. It runs on every character write, not only at creation. Today every call site is a creation path or the explicit "Refresh from Character" button. It becomes the mechanism by which a character edit reaches its placed tokens:
PATCH /characters/:idre-projects and writes every linked token, then broadcasts.2. It must run server-side — and this is now Module API 1.7.0. The fan-out happens in the request path, so the projection has to be callable outside a browser. Task P3a audited it and found no signature change needed: both shipped modules'
projectTokenis already synchronous and free ofwindow,document, React andfetch. System 8's says so outright — wiki derivation is a server-side backfill job whose output lands onpower.actions, keeping the projection pure and deterministic.The obligation 1.7.0 adds is therefore packaging, not code: a module's
SystemModulemust be registrable from a server-reachable entry point. TodayregisterSystemModuleis called only from each module'sclient/index.ts— which contains nothing browser-specific and is "client" by packaging convention — so the server never registers one andloadSystemModulereturnsnullthere. Closing that is task P3b.A module whose
projectTokengenuinely needs a browser cannot satisfy 1.7.0 and should declare so rather than fail at runtime inside the fan-out.3. The merge list shrinks to four fields. "Preserve
x,y/ current HP /conditions/ instance overrides" becomes preserve position, facing, vision/light and size — and nothing else. Current HP and conditions become character-scoped, and "instance overrides" ceases to be a concept: under one-way projection a linked token cannot diverge from its character.reconcileRangedoes not go away, but it applies character-side (raising Max HP must not reset Current) rather than at merge time.Scope rules and the full token-scoped list: data-model.md.
CS-7 — Escape hatch (CharacterSheetSectionExtension)
module.ts — same shape as
ArtworkSourceExtension: { id, label, icon?, moduleId?, component }, props =
CharacterSheetSectionProps { character, onChange, authFetch }. Registered via
moduleRegistry.registerCharacterSheetSection /
getCharacterSheetSections. The sanctioned home for a section the generic model
can't express, so the platform never grows a system-specific component again.
CS-8 — Write translation (applyEdit + EditIntent) ⚠︎ types-only
applyEdit?(record, intent): record' is the inverse of construct: the platform
issues EditIntents (set-attribute-component, add-trait, remove-trait,
adjust-ability) in its own vocabulary; the module decides how each lands as
atomic choices. Pairs with CS-3.2. ⚠︎ Types ship, but no Edit-Mode UI or write
route is wired — that is the next project.
4. Read-only Play Mode (the shipped consumer)
Character Context renders a read-only Play Mode entirely from the projection
(no systemId branches, no createCharacterView):
useModuleProjection.ts— loads the module, constructs the manifest cached by(characterId, updatedAt)(manifestCache.ts), and derives attributes/traits/derived-stats + the projected token.ModuleProjectionSheet.tsx— renders attributes / stats / traits (grouped by CS-1.3 subtype) + action buttons that are a view of the projected token'sIToken.actions, dispatched through the existing Action API sandbox.- Mounted in
CharacterContext.tsxbehind a "Classic | Projected (beta)" toggle (classic stays default).
5. Proven against System 8 (reference implementation)
In modules/system8/src/ (in this repository since v0.118.0): sheetSchema.ts (CS-1),
projection.ts (construct + CS-2 accessors), projectToken.ts (CS-6, P1–P9),
validate in system.ts (CS-3), generate.ts autoFill (CS-4),
contentProvider.ts (CS-5). 38 module unit tests.
6. Known gaps / deferred
- ⚠︎ Edit Mode not built. CS-3.2
validateEdit, CS-8applyEdit, generic write routes, and the reconstruct-on-mutation job flow are types-only; only read-only Play Mode ships. (Next project — Character Sheet Overhaul.) - ⚠︎ Old sheet untouched.
ui-components/CharacterSheet.tsxand theAdd*Modalconsumers keep theirsystemIdbranches until the old sheet retires next project. - ⚠︎
mergeProjectedTokennot yet consumed by the re-projection flow (useRefreshTokenFromCharacterstill replaces metadata wholesale). - ⚠︎
getAttributes(S8) surfaces the six abilities + HP; AC/Speed are declared numeric attributes but not yet computed into the projection.