Skip to content

Commit d82cc9e

Browse files
author
DavidQ
committed
Phase 18 runtime layer scaffolding
1 parent a81b181 commit d82cc9e

12 files changed

Lines changed: 297 additions & 17 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
MODEL: GPT-5.3-codex
22
REASONING: high
3-
COMMAND: Implement Phase 18 core services skeleton and wiring, package to <project folder>/tmp/BUILD_PR_LEVEL_18_3_PHASE18_CORE_SERVICES.zip
3+
COMMAND: Implement Phase 18 runtime layer scaffolding and integrate with core services, package to <project folder>/tmp/BUILD_PR_LEVEL_18_4_PHASE18_RUNTIME_LAYER.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Phase 18 core services skeleton
1+
Phase 18 runtime layer scaffolding
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
- [ ] services initialize
2-
- [ ] no runtime errors
1+
- [ ] runtime initializes
2+
- [ ] loop executes
3+
- [ ] no errors
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# BUILD_PR_LEVEL_18_4_PHASE18_RUNTIME_LAYER
2+
3+
## Purpose
4+
Implement a minimal Phase 18 runtime-layer scaffold and integrate it with the existing Phase 18 core-services skeleton.
5+
6+
## Source of Truth
7+
- `docs/pr/PLAN_PR_LEVEL_18_4_PHASE18_RUNTIME_LAYER.md`
8+
- `docs/pr/BUILD_PR_LEVEL_18_3_PHASE18_CORE_SERVICES.md`
9+
10+
## Exact Build Target
11+
1. Add runtime-layer scaffolding under:
12+
- `samples/phase-18/shared/runtimeLayer/`
13+
2. Include:
14+
- runtime loop orchestration (`start`, `update`, `stop`)
15+
- scheduler hooks (before/after update and state-change hook channels)
16+
- explicit runtime state transitions
17+
3. Integrate runtime layer with Phase 18 core services:
18+
- runtime layer starts/updates/stops service registry
19+
- `samples/phase-18/1801` uses runtime layer instead of directly driving services
20+
21+
## Non-Goals
22+
- no engine-core changes
23+
- no gameplay/system feature logic
24+
- no additional Phase 18 sample entries
25+
- no roadmap status updates
26+
27+
## Validation
28+
- targeted runtime test for runtime state transitions + scheduler hooks + service integration
29+
- existing Phase 18 core-service skeleton test still passes
30+
- sample `1801` renders runtime/service status without errors
31+
32+
## Packaging Rule
33+
Package only this PR's created/modified files into:
34+
`tmp/BUILD_PR_LEVEL_18_4_PHASE18_RUNTIME_LAYER.zip`
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# PLAN_PR_LEVEL_18_4_PHASE18_RUNTIME_LAYER
2+
3+
Purpose:
4+
Introduce runtime orchestration layer.
5+
6+
Scope:
7+
- update loop
8+
- scheduling hooks
9+
- state transitions

samples/phase-18/1801/Phase18FoundationScene.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,53 @@ import { drawFrame, drawPanel } from '/src/engine/debug/index.js';
1111
const theme = new Theme(ThemeTokens);
1212

1313
export default class Phase18FoundationScene extends Scene {
14-
constructor({ coreServices = null } = {}) {
14+
constructor({ runtimeLayer = null } = {}) {
1515
super();
1616
this.elapsed = 0;
17-
this.coreServices = coreServices;
17+
this.runtimeLayer = runtimeLayer;
1818
this.lastHeartbeatTick = 0;
1919
this.lastHeartbeatTime = 0;
20+
this.lastRuntimeTransition = 'idle';
2021
this.unsubscribeHeartbeat = null;
22+
this.unsubscribeRuntimeState = null;
2123
}
2224

2325
enter(engine) {
24-
if (!this.coreServices) return;
25-
const channel = this.coreServices.get('phase18.channel');
26+
if (!this.runtimeLayer) return;
27+
const channel = this.runtimeLayer.getService('phase18.channel');
2628
if (channel && typeof channel.subscribe === 'function') {
2729
this.unsubscribeHeartbeat = channel.subscribe('phase18.heartbeat', (payload) => {
2830
this.lastHeartbeatTick = Number(payload?.tick) || 0;
2931
this.lastHeartbeatTime = Number(payload?.t) || 0;
3032
});
3133
}
32-
this.coreServices.start({ engine, scene: this });
34+
this.unsubscribeRuntimeState = this.runtimeLayer.onStateChange(({ previous, next }) => {
35+
this.lastRuntimeTransition = `${previous} -> ${next}`;
36+
});
37+
this.runtimeLayer.start({ engine, scene: this });
3338
}
3439

3540
update(dtSeconds) {
3641
this.elapsed += dtSeconds;
37-
this.coreServices?.update(dtSeconds, { scene: this });
42+
this.runtimeLayer?.update(dtSeconds, { scene: this });
3843
}
3944

4045
exit() {
4146
if (typeof this.unsubscribeHeartbeat === 'function') {
4247
this.unsubscribeHeartbeat();
4348
this.unsubscribeHeartbeat = null;
4449
}
45-
this.coreServices?.stop({ scene: this });
50+
if (typeof this.unsubscribeRuntimeState === 'function') {
51+
this.unsubscribeRuntimeState();
52+
this.unsubscribeRuntimeState = null;
53+
}
54+
this.runtimeLayer?.stop({ scene: this });
4655
}
4756

4857
render(renderer) {
4958
drawFrame(renderer, theme, [
5059
'Sample 1801 - Phase 18 Foundation',
51-
'Minimal Phase 18 structure bootstrap and launcher wiring.',
60+
'Minimal Phase 18 runtime-layer scaffolding integrated with core services.',
5261
'No feature implementation in this scaffold.',
5362
]);
5463

@@ -63,11 +72,18 @@ export default class Phase18FoundationScene extends Scene {
6372
font: '16px monospace',
6473
});
6574

75+
const runtimeSnapshot = this.runtimeLayer?.getSnapshot?.() || {
76+
state: 'idle',
77+
tickCount: 0,
78+
serviceIds: [],
79+
};
6680
drawPanel(renderer, 620, 34, 300, 160, 'Phase 18 Bootstrap', [
6781
'Status: initialized',
6882
'Folder: samples/phase-18',
6983
'Entry sample: 1801',
70-
`Services: ${this.coreServices?.listServiceIds().length ?? 0}`,
84+
`Runtime: ${runtimeSnapshot.state} | tick ${runtimeSnapshot.tickCount}`,
85+
`Transition: ${this.lastRuntimeTransition}`,
86+
`Services: ${runtimeSnapshot.serviceIds.length}`,
7187
`Heartbeat tick: ${this.lastHeartbeatTick} @ ${this.lastHeartbeatTime.toFixed(2)}s`,
7288
]);
7389
}

samples/phase-18/1801/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<body>
1515
<main>
1616
<h1>Sample 1801 - Phase 18 Foundation</h1>
17-
<p>Bootstrap scaffold for Phase 18 with minimal core-service lifecycle wiring and no feature logic.</p>
17+
<p>Bootstrap scaffold for Phase 18 with minimal runtime-layer and core-service wiring, without feature logic.</p>
1818
<canvas id="game" width="960" height="540"></canvas>
1919

2020
<section>

samples/phase-18/1801/main.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Engine from '/src/engine/core/Engine.js';
88
import { InputService } from '/src/engine/input/index.js';
99
import { Theme, ThemeTokens } from '/src/engine/theme/index.js';
1010
import createPhase18CoreServices from '/samples/phase-18/shared/coreServices/createPhase18CoreServices.js';
11+
import createPhase18RuntimeLayer from '/samples/phase-18/shared/runtimeLayer/createPhase18RuntimeLayer.js';
1112
import Phase18FoundationScene from './Phase18FoundationScene.js';
1213

1314
const theme = new Theme(ThemeTokens);
@@ -25,5 +26,6 @@ const engine = new Engine({
2526
});
2627

2728
const coreServices = createPhase18CoreServices();
28-
engine.setScene(new Phase18FoundationScene({ coreServices }));
29+
const runtimeLayer = createPhase18RuntimeLayer({ coreServices });
30+
engine.setScene(new Phase18FoundationScene({ runtimeLayer }));
2931
engine.start();
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase18RuntimeLayer.js
6+
*/
7+
import createPhase18SchedulerHooks from './createPhase18SchedulerHooks.js';
8+
9+
const RUNTIME_STATES = {
10+
IDLE: 'idle',
11+
RUNNING: 'running',
12+
STOPPED: 'stopped',
13+
};
14+
15+
export default function createPhase18RuntimeLayer({ coreServices } = {}) {
16+
const schedulerHooks = createPhase18SchedulerHooks();
17+
let runtimeState = RUNTIME_STATES.IDLE;
18+
let tickCount = 0;
19+
let lastDtSeconds = 0;
20+
21+
function notifyStateChange(previous, next, context = {}) {
22+
schedulerHooks.stateChange.run({ previous, next, context });
23+
const channel = coreServices?.get?.('phase18.channel');
24+
if (channel && typeof channel.publish === 'function') {
25+
channel.publish('phase18.runtime.state', { previous, next });
26+
}
27+
}
28+
29+
function transitionTo(nextState, context = {}) {
30+
if (runtimeState === nextState) return false;
31+
const previous = runtimeState;
32+
runtimeState = nextState;
33+
notifyStateChange(previous, nextState, context);
34+
return true;
35+
}
36+
37+
function start(context = {}) {
38+
if (runtimeState === RUNTIME_STATES.RUNNING) return false;
39+
coreServices?.start?.({ ...context, runtimeLayer: api });
40+
transitionTo(RUNTIME_STATES.RUNNING, context);
41+
return true;
42+
}
43+
44+
function update(dtSeconds, context = {}) {
45+
if (runtimeState !== RUNTIME_STATES.RUNNING) return 0;
46+
const dt = Math.max(0, Number(dtSeconds) || 0);
47+
lastDtSeconds = dt;
48+
tickCount += 1;
49+
50+
const payload = { dtSeconds: dt, tick: tickCount, context };
51+
schedulerHooks.beforeUpdate.run(payload);
52+
coreServices?.update?.(dt, { ...context, runtimeLayer: api, tick: tickCount });
53+
schedulerHooks.afterUpdate.run(payload);
54+
return tickCount;
55+
}
56+
57+
function stop(context = {}) {
58+
if (runtimeState !== RUNTIME_STATES.RUNNING) return false;
59+
transitionTo(RUNTIME_STATES.STOPPED, context);
60+
coreServices?.stop?.({ ...context, runtimeLayer: api });
61+
return true;
62+
}
63+
64+
function getSnapshot() {
65+
return {
66+
state: runtimeState,
67+
tickCount,
68+
lastDtSeconds,
69+
hookCounts: schedulerHooks.snapshot(),
70+
serviceIds: coreServices?.listServiceIds?.() || [],
71+
};
72+
}
73+
74+
const api = {
75+
start,
76+
update,
77+
stop,
78+
getSnapshot,
79+
getState() {
80+
return runtimeState;
81+
},
82+
getService(serviceId) {
83+
return coreServices?.get?.(serviceId) || null;
84+
},
85+
listServiceIds() {
86+
return coreServices?.listServiceIds?.() || [];
87+
},
88+
onBeforeUpdate(handler) {
89+
return schedulerHooks.beforeUpdate.register(handler);
90+
},
91+
onAfterUpdate(handler) {
92+
return schedulerHooks.afterUpdate.register(handler);
93+
},
94+
onStateChange(handler) {
95+
return schedulerHooks.stateChange.register(handler);
96+
},
97+
states: RUNTIME_STATES,
98+
};
99+
100+
return api;
101+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase18SchedulerHooks.js
6+
*/
7+
function createHookChannel() {
8+
const handlers = new Set();
9+
10+
function register(handler) {
11+
if (typeof handler !== 'function') return () => {};
12+
handlers.add(handler);
13+
return () => handlers.delete(handler);
14+
}
15+
16+
function run(payload) {
17+
let executed = 0;
18+
for (const handler of handlers) {
19+
handler(payload);
20+
executed += 1;
21+
}
22+
return executed;
23+
}
24+
25+
function count() {
26+
return handlers.size;
27+
}
28+
29+
return {
30+
register,
31+
run,
32+
count,
33+
};
34+
}
35+
36+
export default function createPhase18SchedulerHooks() {
37+
const beforeUpdate = createHookChannel();
38+
const afterUpdate = createHookChannel();
39+
const stateChange = createHookChannel();
40+
41+
return {
42+
beforeUpdate,
43+
afterUpdate,
44+
stateChange,
45+
snapshot() {
46+
return {
47+
beforeUpdate: beforeUpdate.count(),
48+
afterUpdate: afterUpdate.count(),
49+
stateChange: stateChange.count(),
50+
};
51+
},
52+
};
53+
}

0 commit comments

Comments
 (0)