"Use when adding a new ECS component, MCP command, or engine capability. Checklist of all files that must be updated across Rust engine, web layer, and integration points."
Install
npx skillscat add tristan578/project-forge/component-checklist Install via the SkillsCat registry.
SKILL.md
New Component / Command Checklist
When adding a new ECS component, update these domain-scoped files:
Rust Engine (4 required files)
engine/src/core/<component>.rs— Component struct + marker (addpub modincore/mod.rs)engine/src/core/pending/<domain>.rs— Request structs + queue methods + bridge fnsengine/src/core/commands/<domain>.rs— Dispatch entry + handler functionengine/src/bridge/<domain>.rs— Apply system + selection emit (register inbridge/mod.rsSelectionPlugin::build())
Rust Engine (supporting, if needed)
engine/src/core/history.rs—UndoableActionvariant +EntitySnapshotfieldengine/src/core/entity_factory.rs— delete/duplicate/undo/redo +spawn_from_snapshot.
Both the undo and the redo arm must queue a re-report —queue_resync(ComponentResync::<Kind> { .. }, ..),
with the variant added tocore/component_resync.rsand routed to your emitter
inbridge/component_resync.rs. The selection emitters in #4 are gated onselection.primaryANDChanged<T>, so without this an undo touching a
non-selected entity leaves the browser's mirror stale and the next edit sends a
full-replace built from that stale value (#9290, #9291).spawn_from_snapshot
owes the same for anything the snapshot carries — add it toresyncs_for_snapshot.
The parity gate incore/component_resync_tests.rsfails the build until you do,
or until the arm is listed inEXEMPT_ARMSwith a reason. Seerules/entity-snapshot.md-> "Every history arm owes the browser a re-report"engine/src/core/engine_mode.rs—snapshot_scene(separate query param)engine/src/bridge/events.rs— Emit function(s)engine/src/bridge/query.rs— Query handler (if component has query support)
Web Layer (4 required files)
web/src/stores/slices/<domain>Slice.ts— State + actions (+ re-export fromslices/index.ts)web/src/hooks/events/<domain>Events.ts— Event handler(s). Any handler that
writes aprimary*store field must go throughapplyWhenPrimary(entityId, ...)
(hooks/events/primaryGate.ts) — the resync drain emits for non-selected
entities too, and the gate's microtask deferral is what keeps a same-tickSELECTION_CHANGEDfrom being read as the previous selectionweb/src/lib/chat/handlers/<domain>Handlers.ts— Tool call handler(s) (registered inexecutor.tshandler registry)web/src/components/editor/<Inspector>.tsx— Inspector panel
Integration (5 required files)
web/src/components/editor/InspectorPanel.tsx— Import + renderweb/src/components/chat/ToolCallCard.tsx— Display labelsmcp-server/manifest/commands.json— MCP commands. Setvisibility: 'public'or'internal'(mandatory)web/src/data/commands.json— COPY of #16 (keep in sync;apps/docs/data/commands.jsonis the third copy)
17a. If the command spends a generation capability: add it toCOMMAND_CAPABILITYinweb/src/lib/config/providers.ts(withholds it from AI tool sets when the capability is declared unavailable — pinned byweb/src/lib/chat/__tests__/toolAvailability.test.ts), setcapability:on itscreateGenerationHandlerroute config AND add itsroute:string toROUTE_CAPABILITYin the sameproviders.ts(pinned byweb/src/lib/config/__tests__/routeCapability.test.ts, which fails on any generate route absent from that table), add itsforge.ai.*method toAI_METHODSinweb/src/lib/scripting/channels/aiChannel.ts(everyAI_METHODSroute must be aROUTE_CAPABILITYkey with the same capability — pinned byaiChannel.test.ts), and add a row to the decision table indocs/guides/platform-keys.mdTESTING.md— Manual test cases
Game Components (a much shorter list — do NOT run the 18 above)
A game component (health, damage, characterController, …) is a variant of one
existing enum, not a new ECS component. Adding a 14th type touches six places:
engine/src/core/game_components.rs—GameComponentDatavariant + its data struct
(the enum is#[serde(tag = "type", rename_all = "camelCase")], so the variant name
IS the wire tag)engine/src/core/game_components.rs— thebuild_game_componentmatch arm, usingprop_f32/prop_u32for every numeric field so the engine clamps itweb/src/stores/slices/types.ts— the member of theGameComponentDataunionweb/src/lib/engine/gameComponentWire.ts—ENGINE_TYPE_BY_STORE_TYPE(the
snake_case command name ↔ camelCase serde tag pair) and, for every numeric field,
an entry inF32_RANGES/U32_MAXESmirroring the Rust boundsweb/src/lib/engine/__tests__/gameComponentWire.test.ts— the tables there are
pinned against the Rust by a textual scan with a COUNT assertion, so a newprop_f32/prop_u32call site FAILS the suite until it is mirrored. The prose
counts in the coercer doc comments are pinned too — update the sentence, not just
the tableweb/src/components/editor/GameComponentInspector.tsx— the editing UI
Everything between the store and the engine — both directions — goes throughgameComponentWire.ts. Never hand-build a component payload and never cast an
emitted one: see rules/gotchas-engine.md → "dispatchCommand returns void".