kit-schema.ts
PURPOSE
Defines the Kit data model — the second-level VFX-workbench unit above Component. A Kit is a component graph for one weapon: it lists which ComponentDefs the weapon uses and how they relate (which component sits on which anchor of which other). Runtime agents consume Kit JSONs to wire weapons onto their baked/live components. Also exports validateKitDef, the structural validator used before persisting or loading a Kit.
Reference: docs/superpowers/specs/2026-04-20-weapon-workbench-2.0-upgrades.md §9.
OWNS
- Type aliases:
KitEntryRole,KitInstanceCount,RelationshipKind. - Interfaces:
KitEntry,Relationship,PreviewMotionRef,KitDef. - Internal lookup arrays:
ROLES,COUNTS,REL_KINDS(used by the validator). - Validator function:
validateKitDef(d: unknown).
READS FROM
- Nothing at runtime — this module is pure type + validation. No imports.
- Conceptually references
ComponentDef.idstrings viaKitEntry.componentRef, but does not import the Component module (string-keyed).
PUSHES TO
- Consumers that import the types or the validator. Producers (workbench editors) and consumers (runtime VFX agents, persistence layers) both depend on this schema.
- No side effects, no I/O.
DOES NOT
- Does not resolve
componentRefto a realComponentDef— validation is structural only; reference existence is checked elsewhere. - Does not validate
previewMotion.paramsshape (template-dependent, deferred). - Does not enforce semantic rules per
RelationshipKind(e.g. doesn’t check thatspanBetweenhas two anchors). Only checkskindis in the allowed set andparentId/childIdexist among entries. - Does not ship
previewMotionto runtime — it is a shooting-range preview hint only.
Signals
Type signals
KitEntryRole— one of'spawn' | 'body' | 'connection' | 'persistent' | 'impact'. Five-role taxonomy for a kit’s entries.KitInstanceCount—'fixed' | 'runtime'.'fixed'= kit defines N copies viafixedCount;'runtime'= game decides (chain-jumps, shotgun pellets, etc.).RelationshipKind— five graph-edge kinds:attachPivot— child’s pivot anchor snaps to parent’s named anchor.spanBetween— child stretches between two anchors (beam-like).orbitAround— child orbits a named anchor of parent.trailBehind— child ribbons behind parent’s motion.stackOnto— child draws stacked on top of parent’s pivot.
Interface signals
KitEntry { id, componentRef, role, instanceCount, fixedCount?, notes? }— stable id used by relationship lookup;componentRefis aComponentDef.id.Relationship { kind, parentId, childId, parentAnchor?, childAnchor? }—parentAnchordefaults differ perkind(e.g.'center'forattachPivot);childAnchordefaults to'pivot'.PreviewMotionRef { template, params? }— built-in motion template ('straight_line' | 'orbit' | 'tracking' | 'beam_sustain' | 'arc_ballistic') plus optional template-specific param overrides. Preview-only, not shipped to runtime.KitDef { id, name, forWeaponId, entries, relationships, previewMotion, notes? }— the top-level Kit document.
Entry points
validateKitDef(d: unknown): { ok: true } | { ok: false; reason: string }
Structural validator. Returns first-failure shape { ok: false, reason } or { ok: true }. Checks performed, in order:
dis a non-null object.idis a non-empty string.nameis a string.forWeaponIdis a non-empty string.entriesis an array; for each entry:idnon-empty string.idunique within kit (duplicate detection viaSet).componentRefis a string.roleis inROLES.instanceCountis inCOUNTS.
relationshipsis an array; for each relationship:kindis inREL_KINDS.parentIdis a string AND exists as an entry id.childIdis a string AND exists as an entry id.
previewMotionis an object with stringtemplate.
Pattern notes
- Structural-only validation. The validator is pure shape-checking; semantic invariants (anchor-name validity per
RelationshipKind,fixedCountpresence wheninstanceCount === 'fixed',componentRefresolution) are deferred to higher layers. This keepskit-schema.tsdependency-free. - First-failure-wins reason string. Validation returns the first reason found rather than collecting all errors. Reason strings are indexed (
entries[${i}].id required) for editor surfacing. - String-keyed cross-module references.
componentRefis a string id — schema does not import the Component module. Lets schema sit at the bottom of the dependency graph for the VFX workbench. - Two-axis instance model. A kit entry has both a
role(functional taxonomy) and aninstanceCount(fixed-vs-runtime cardinality). These compose orthogonally — e.g. a'spawn'role with'runtime'count covers shotgun-pellet spawners. - Preview vs. runtime split.
previewMotionexists for the shooting-range preview only. The comment onPreviewMotionRef.templateis explicit: NOT shipped to runtime. Runtime motion comes from the weapon’s actual code path. - Defaults documented in comments, not enforced.
parentAnchor’s default (‘center’ forattachPivot) andchildAnchor’s default ('pivot') are described in JSDoc but applied by the consumer, not the schema.
EXTRACT-CANDIDATE
- Shared anchor-default policy. The per-
RelationshipKinddefault anchor logic (e.g.attachPivot→'center', default child anchor'pivot') is currently comment-only here. If multiple consumers (runtime agent, preview, persistence) need to apply these defaults, extract aresolveRelationshipDefaults(rel: Relationship)helper to a shared module so the defaults table lives in one place. - Reference-resolution layer. A
validateKitRefs(kit, componentDefsById)helper that checkscomponentRefexistence against aComponentDefregistry belongs in a sibling module — distinct from this structural validator — and would be shared by both the workbench editor and the runtime loader.