GraphOS Skill: graph-world-ts
Implement GraphOS world runtime code in TypeScript after the world graph has already been designed and validated.
This skill depends on graph-world for graph modeling and closure validation. Use graph-world first whenever the request changes World/Context/Variant/System/Event/EventSystem topology.
Scope Boundary
- This skill covers npm + TypeScript project bootstrap, generated code integration, runtime implementation, and registration wiring.
- This skill does not replace graph modeling. If the request changes graph structure or event/data ownership, run
graph-worldfirst. - Keep presentation-layer work outside this skill. Presentation concerns belong in a separate app/view skill such as
graph-world-pixijs.
When to Use
- You need to initialize a fresh GraphOS world package with npm and TypeScript.
- You need to configure GraphOS code generation in
package.json. - You need to implement
SystemorEventSystemcode against generated./gen/Worldtypes. - You need to wire startup bootstrap behavior in
System.spawn. - You need to register generated runtime handlers in
src/app.ts.
Preconditions
- Complete graph design with
graph-worldfirst. - Finish graph closed-loop validation before writing runtime code.
- If Graph changed, regenerate types before updating TypeScript implementations.
- Preserve the repository's existing TypeScript import style; do not force
.jssuffix rewrites unless the project already requires them.
Project Bootstrap (npm + TypeScript)
Use this when the user asks to initialize a new world logic project.
- Initialize npm project and install TypeScript + Node typings:
npm init -y
npm i -D typescript @types/node
- Install
graphos-world-pluginandgraphos-clias dev dependencies:
npm i -D graphos-world-plugin graphos-cli
- Ensure
package.jsoncontains the followinggraphosconfig:
{
"type": "module",
"graphos": {
"world": {
"genTypeScript": {
"enabled": true,
"outDir": "gen"
},
"genWebTypeScript": {
"enabled": true,
"outDir": "app"
},
"genCocosCreator": {
"enabled": false,
"outDir": "../cocos/assets/gen"
}
}
}
}
- Ensure
package.jsonscripts includes:
{
"scripts": {
"graphos": "graphos",
"build": "tsc -p tsconfig.world.json"
}
}
- Ensure root
tsconfig.world.jsonexists (minimal example):
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*.ts", "gen/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
- Create
World.graph.jsonin the project root as the initial empty graph:
{
"id": "main",
"name": "World",
"nodes": [],
"edges": []
}
- Optional verification:
npm run graphos -- --help
npm run build
Workflow
Step 1: Sync Graph Output Into TypeScript
- Confirm Graph changes are complete and validated.
- Regenerate
./gen/Worldand related outputs if Graph changed. - Re-read generated types before implementing runtime code.
Completion checks:
- Generated
Contextand event types match the current graph. - No runtime code is being written against stale generated APIs.
Step 2: Implement Systems
Implement concrete System behavior only after Graph data ownership is finalized.
import type { GameplayContext } from './gen/World';
import type { ISystem } from 'graphos-world-plugin';
export function createGameBootstrapSystem(): ISystem<GameplayContext> {
// Optional internal cache owned by this specific system instance.
return {
spawn(ctx: GameplayContext): void {
// TODO
},
despawn(ctx: GameplayContext): void {
// TODO
},
update(ctx: GameplayContext, deltaTime: number): void {
// TODO
},
change(ctx: GameplayContext): void {
// TODO
},
};
}
Implementation guidance:
- Keep method behavior aligned with the closure checks from
graph-worldStep 4. - Do not implement business logic before Graph data model and ownership are finalized.
- If Graph changes update generated context types, regenerate types first, then update this
Systemimplementation. - Keep presentation behavior outside
System; the View layer should react by observingContextdata changes. - Intermediate cache should be handled privately inside each
Systemand should not leak into presentation concerns. - Do not create module-level or global cache/state for
Systemimplementations. - Any cache must live inside the
create...Systemfactory function scope so each created system instance owns its own cache. - Put shared data contracts, constants, and Context singleton ids/names used by generated
Systemcode intosrc/types.ts, then import them fromSystemfiles. - Put runtime configuration into
src/config.tsand read it from generatedSystemlogic; prefer configurable defaults over hard-coded values. - When generating
Systemcode files, use PascalCase file names and keep them exactly aligned with the graphSystemnodename. - Example: graph node
GameBootstrapSystem-> fileGameBootstrapSystem.ts. - Since
Contextprovides storage capability, implement a get-or-create pattern inSystemlogic: fetch existing Context/state first, and create/initialize only when missing. - For singleton
Contextaccess inSystemimplementations, use a fixed id withget...ById('id'). - Do not fetch singleton
Contextinstances through positional access such asget...Children()[0], because order-based lookup is unstable and obscures identity.
Step 3: Implement World Startup Entry
Goal: define a clear and deterministic startup entry for world logic initialization.
You must implement the following startup pattern:
- Initialize required
Contextin aSystem.spawnmounted underWorld.- Add a bootstrap system node under
World(for exampleWorldBootstrapSystem). - In
spawn, initialize the required root/domain Context instances using get-or-create semantics. - Keep initialization idempotent: repeated
spawncalls must not create duplicate singleton Context data.
- Add a bootstrap system node under
Startup responsibility:
System.spawn: baseline/default world Context bootstrap that should exist before runtime flows.
import type { ISystem } from 'graphos-world-plugin';
import { WorldContext } from '../gen/World';
export function createWorldBootstrapSystem(): ISystem<WorldContext> {
return {
spawn(ctx: WorldContext): void {
// Get-or-create required singleton contexts here.
// Example: ctx.getGameplayById('gameplay') ?? ctx.createGameplay({ id: 'gameplay' })
},
};
}
Completion checks:
- Startup initialization is idempotent.
- Required singleton/root contexts are available before downstream runtime flows.
Step 4: Implement EventSystems
Implement event handlers only after Graph trigger chains and payload design are validated.
import type { WorldContext, IChickenShootEvent } from './gen/World';
import type { ISystem } from 'graphos-world-plugin';
export type SpawnBulletOnShootEventSystem = ISystem<WorldContext, IChickenShootEvent>;
export function createSpawnBulletOnShootEventSystem(): SpawnBulletOnShootEventSystem {
// Optional internal cache owned by this specific event-system instance.
return {
handle(event: IChickenShootEvent): void {
const world = event.source as unknown as WorldContext;
// TODO
},
};
}
Implementation guidance:
- Ensure Event payload fields are defined in Graph and match the generated event type.
- For events consumed mainly by the presentation/UI layer, prefer signal-style Events with no payload; add fields only when the UI cannot derive the required state from
Context. - Keep
handleside effects scoped to the owning Context and verified by graph closure. - If Event or payload schema changes in Graph, regenerate
./gen/Worldtypes before updating EventSystem code. - Do not create module-level or global cache/state for
EventSystemimplementations. - Any cache must live inside the
create...EventSystemfactory function scope so each created event-system instance owns its own cache. - Put shared data contracts, constants, and Context singleton ids/names used by generated
EventSystemcode intosrc/types.ts, then import them fromEventSystemfiles. - Put runtime configuration into
src/config.tsand read it from generatedEventSystemlogic; prefer configurable defaults over hard-coded values. - When generating
EventSystemcode files, use PascalCase file names and keep them exactly aligned with the graphEventSystemnodename. - Example: graph node
SpawnBulletOnShootEventSystem-> fileSpawnBulletOnShootEventSystem.ts. - Since
Contextprovides storage capability,EventSystemhandlers should fetch target Context/state first and create/initialize only when it does not exist. - For singleton
Contextaccess inEventSystemimplementations, use a fixed id withget...ById('id'). - Do not resolve singleton
Contextinstances throughget...Children()[0]or any other position-based child lookup.
Step 5: Register Runtime Wiring in src/app.ts
After implementing handlers, wire the runtime explicitly in src/app.ts.
import { App } from 'graphos-world-plugin';
import { MatchLoopContext, WorldContext } from '../gen/World';
import { createMatchLifecycleSystem } from './MatchLifecycleSystem';
import { createApplyConfigHotReloadEventSystem } from './ApplyConfigHotReloadEventSystem';
export default function (app: App): WorldContext {
app.addSystem(MatchLoopContext.Table, createMatchLifecycleSystem());
app.addEventSystem('ConfigHotReloadRequested', createApplyConfigHotReloadEventSystem());
return WorldContext.default(app.ctx, app.cache)!;
}
Registration guidance:
- After implementing a
System, register it insrc/app.tswithapp.addSystem(...). - Register the World bootstrap system with
app.addSystem(WorldContext.Table, createWorldBootstrapSystem()). - Ensure bootstrap initialization remains idempotent and safe on retry/re-entry.
- After implementing an
EventSystem, register it insrc/app.tswithapp.addEventSystem(...). - Keep imported symbol names aligned with the generated PascalCase file names and graph node names.
- Use the owning Context table when calling
app.addSystem(...). - Use the exact graph
Eventname when callingapp.addEventSystem(...). - Keep the
src/app.tsdefault export signature fixed asexport default function (app: App): WorldContext; this format is mandatory and must not be changed.
Completion checks:
- Every implemented
SystemandEventSystemis registered. src/app.tsmatches the current generated graph contracts.
Quality Gates
- Graph-first gate: no runtime implementation started before
graph-worldclosure validation passed. - Generated-type gate:
./gen/Worldhas been regenerated and re-read after Graph changes. - Scope gate:
SystemandEventSystemcode respects graph ownership and does not invent new topology. - Cache gate: no module-level or global mutable cache; per-instance closure cache only.
- Config gate: implementation-specific config lives in
src/config.ts, not in Graph topology. - Registration gate: every runtime handler is wired in
src/app.ts. - Startup gate:
Worldbootstrap initialization is idempotent and singleton-safe.
Failure Recovery
- Generated types do not match expectations: regenerate Graph output first, then re-open
./gen/Worldbefore editing runtime code. - A handler needs data that is missing from the graph: stop TypeScript patching and route back to
graph-world. - Runtime code is using positional child lookup for singleton contexts: replace with fixed-id get-or-create access.
- Imports appear to need
.jssuffixes: only change import paths if the target repository already enforces that convention. - Registration is incomplete: finish
src/app.tswiring before treating the feature as complete.
Example Requests
- Bootstrap a GraphOS world npm package with
package.json,tsconfig.world.json, andWorld.graph.json. - Implement
WorldBootstrapSystemand register it insrc/app.ts. - Regenerate
./gen/Worldand update runtime code after adding a newEventSystemin graph-world. - Refactor Systems to use
src/config.tsandsrc/types.tsinstead of hard-coded constants.