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.id strings via KitEntry.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 componentRef to a real ComponentDef — validation is structural only; reference existence is checked elsewhere.
  • Does not validate previewMotion.params shape (template-dependent, deferred).
  • Does not enforce semantic rules per RelationshipKind (e.g. doesn’t check that spanBetween has two anchors). Only checks kind is in the allowed set and parentId/childId exist among entries.
  • Does not ship previewMotion to 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 via fixedCount; '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; componentRef is a ComponentDef.id.
  • Relationship { kind, parentId, childId, parentAnchor?, childAnchor? }parentAnchor defaults differ per kind (e.g. 'center' for attachPivot); childAnchor defaults 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:

  1. d is a non-null object.
  2. id is a non-empty string.
  3. name is a string.
  4. forWeaponId is a non-empty string.
  5. entries is an array; for each entry:
    • id non-empty string.
    • id unique within kit (duplicate detection via Set).
    • componentRef is a string.
    • role is in ROLES.
    • instanceCount is in COUNTS.
  6. relationships is an array; for each relationship:
    • kind is in REL_KINDS.
    • parentId is a string AND exists as an entry id.
    • childId is a string AND exists as an entry id.
  7. previewMotion is an object with string template.

Pattern notes

  • Structural-only validation. The validator is pure shape-checking; semantic invariants (anchor-name validity per RelationshipKind, fixedCount presence when instanceCount === 'fixed', componentRef resolution) are deferred to higher layers. This keeps kit-schema.ts dependency-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. componentRef is 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 an instanceCount (fixed-vs-runtime cardinality). These compose orthogonally — e.g. a 'spawn' role with 'runtime' count covers shotgun-pellet spawners.
  • Preview vs. runtime split. previewMotion exists for the shooting-range preview only. The comment on PreviewMotionRef.template is 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’ for attachPivot) and childAnchor’s default ('pivot') are described in JSDoc but applied by the consumer, not the schema.

EXTRACT-CANDIDATE

  • Shared anchor-default policy. The per-RelationshipKind default 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 a resolveRelationshipDefaults(rel: Relationship) helper to a shared module so the defaults table lives in one place.
  • Reference-resolution layer. A validateKitRefs(kit, componentDefsById) helper that checks componentRef existence against a ComponentDef registry belongs in a sibling module — distinct from this structural validator — and would be shared by both the workbench editor and the runtime loader.