Skip to content

Commit 233c677

Browse files
author
DavidQ
committed
Introduce Phase 19 runtime layer including update loop, scheduling hooks, and integration with core services
PR: BUILD_PR_LEVEL_19_4_PHASE19_RUNTIME_LAYER
1 parent 98f6c55 commit 233c677

11 files changed

Lines changed: 361 additions & 24 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 19 core services skeleton and wiring, package to <project folder>/tmp/BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES.zip
3+
COMMAND: Implement Phase 19 runtime layer scaffolding and package to <project folder>/tmp/BUILD_PR_LEVEL_19_4_PHASE19_RUNTIME_LAYER.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Establish Phase 19 core services skeleton including registry, lifecycle hooks, and baseline wiring for the next runtime layer
1+
Introduce Phase 19 runtime layer including update loop, scheduling hooks, and integration with core services
22

3-
PR: BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES
3+
PR: BUILD_PR_LEVEL_19_4_PHASE19_RUNTIME_LAYER
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
- [ ] services initialize
2-
- [ ] no runtime errors
3-
- [ ] baseline wiring loads
1+
- [ ] runtime initializes
2+
- [ ] loop runs
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# BUILD_PR_LEVEL_19_4_PHASE19_RUNTIME_LAYER
2+
3+
## Purpose
4+
Implement a minimal Phase 19 runtime-layer scaffold and integrate it with the existing Phase 19 core-services skeleton.
5+
6+
## Source of Truth
7+
- `docs/pr/PLAN_PR_LEVEL_19_4_PHASE19_RUNTIME_LAYER.md`
8+
- `docs/pr/BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES.md`
9+
10+
## Exact Build Target
11+
1. Add runtime-layer scaffolding under:
12+
- `samples/phase-19/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 19 core services:
18+
- runtime layer starts/updates/stops service registry
19+
- `samples/phase-19/1901` uses runtime layer instead of directly driving services
20+
4. Add targeted runtime validation for:
21+
- runtime state transitions + scheduler hooks + service integration
22+
- sample `1901` runtime/service status rendering path
23+
24+
## Non-Goals
25+
- no engine-core changes
26+
- no gameplay/system feature logic
27+
- no integration-flow implementation
28+
- no additional Phase 19 sample entries
29+
- no roadmap status updates
30+
31+
## Validation
32+
- targeted runtime test for runtime state transitions + scheduler hooks + service integration passes
33+
- existing Phase 19 core-service skeleton test still passes
34+
- sample `1901` renders runtime/service status without errors
35+
36+
## Packaging Rule
37+
Package only this PR's created/modified files into:
38+
`tmp/BUILD_PR_LEVEL_19_4_PHASE19_RUNTIME_LAYER.zip`
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# PLAN_PR_LEVEL_19_4_PHASE19_RUNTIME_LAYER
2+
3+
## Purpose
4+
Implement a minimal Phase 19 runtime-layer scaffold and wire it into sample `1901`.
5+
6+
## Source of Truth
7+
- `docs/pr/BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES.md`
8+
- `docs/pr/PLAN_PR_LEVEL_19_3_PHASE19_CORE_SERVICES.md`
9+
10+
## Scope
11+
- add runtime-layer scaffolding under `samples/phase-19/shared/runtimeLayer/`
12+
- include runtime loop orchestration, scheduler hooks, and explicit runtime state transitions
13+
- integrate runtime layer with Phase 19 core services (`start`, `update`, `stop`)
14+
- rewire sample `1901` to use runtime layer instead of directly driving core services
15+
- add targeted runtime validation for transitions/hooks/service integration
16+
17+
## Out of Scope
18+
- no integration-flow abstraction
19+
- no gameplay/feature systems
20+
- no additional Phase 19 samples
21+
- no roadmap status updates
22+
23+
## Exit Criteria
24+
- runtime layer exists and drives core services through `1901`
25+
- targeted runtime validation passes
26+
- scoped delta is ready for repo-structured packaging

samples/phase-19/1901/Phase19FoundationScene.js

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

1313
export default class Phase19FoundationScene 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('phase19.channel');
26+
if (!this.runtimeLayer) return;
27+
this.unsubscribeRuntimeState = this.runtimeLayer.onStateChange(({ previous, next }) => {
28+
this.lastRuntimeTransition = `${previous} -> ${next}`;
29+
});
30+
const channel = this.runtimeLayer.getService('phase19.channel');
2631
if (channel && typeof channel.subscribe === 'function') {
2732
this.unsubscribeHeartbeat = channel.subscribe('phase19.heartbeat', (payload) => {
2833
this.lastHeartbeatTick = Number(payload?.tick) || 0;
2934
this.lastHeartbeatTime = Number(payload?.t) || 0;
3035
});
3136
}
32-
this.coreServices.start({ engine, scene: this });
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) {
@@ -63,20 +72,22 @@ export default class Phase19FoundationScene extends Scene {
6372
font: '16px monospace',
6473
});
6574

66-
const lifecycle = this.coreServices?.getLifecycleState?.() || {
67-
running: false,
68-
serviceCount: 0,
75+
const runtimeSnapshot = this.runtimeLayer?.getSnapshot?.() || {
76+
state: 'idle',
77+
tickCount: 0,
78+
serviceIds: [],
6979
};
70-
const channelSnapshot = this.coreServices?.get?.('phase19.channel')?.getSnapshot?.() || {
80+
const channelSnapshot = this.runtimeLayer?.getService?.('phase19.channel')?.getSnapshot?.() || {
7181
publishedCount: 0,
7282
lastChannel: 'none',
7383
};
74-
drawPanel(renderer, 620, 34, 300, 140, 'Phase 19 Bootstrap', [
75-
'Status: initialized (core services)',
84+
drawPanel(renderer, 620, 34, 300, 160, 'Phase 19 Bootstrap', [
85+
'Status: initialized (runtime layer)',
7686
'Folder: samples/phase-19',
7787
'Entry sample: 1901',
78-
`Running: ${lifecycle.running ? 'yes' : 'no'}`,
79-
`Services: ${lifecycle.serviceCount}`,
88+
`Runtime: ${runtimeSnapshot.state} | tick ${runtimeSnapshot.tickCount}`,
89+
`Transition: ${this.lastRuntimeTransition}`,
90+
`Services: ${runtimeSnapshot.serviceIds.length}`,
8091
`Published: ${channelSnapshot.publishedCount} (${channelSnapshot.lastChannel})`,
8192
`Heartbeat tick: ${this.lastHeartbeatTick} @ ${this.lastHeartbeatTime.toFixed(2)}s`,
8293
]);

samples/phase-19/1901/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 createPhase19CoreServices from '/samples/phase-19/shared/coreServices/createPhase19CoreServices.js';
11+
import createPhase19RuntimeLayer from '/samples/phase-19/shared/runtimeLayer/createPhase19RuntimeLayer.js';
1112
import Phase19FoundationScene from './Phase19FoundationScene.js';
1213

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

2728
const coreServices = createPhase19CoreServices();
28-
engine.setScene(new Phase19FoundationScene({ coreServices }));
29+
const runtimeLayer = createPhase19RuntimeLayer({ coreServices });
30+
engine.setScene(new Phase19FoundationScene({ 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+
createPhase19RuntimeLayer.js
6+
*/
7+
import createPhase19SchedulerHooks from './createPhase19SchedulerHooks.js';
8+
9+
const RUNTIME_STATES = {
10+
IDLE: 'idle',
11+
RUNNING: 'running',
12+
STOPPED: 'stopped',
13+
};
14+
15+
export default function createPhase19RuntimeLayer({ coreServices } = {}) {
16+
const schedulerHooks = createPhase19SchedulerHooks();
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?.('phase19.channel');
24+
if (channel && typeof channel.publish === 'function') {
25+
channel.publish('phase19.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+
createPhase19SchedulerHooks.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 createPhase19SchedulerHooks() {
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+
}

tests/runtime/Phase19CoreServicesSkeleton.test.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Phase19CoreServicesSkeleton.test.mjs
66
*/
77
import assert from 'node:assert/strict';
88
import createPhase19CoreServices from '../../samples/phase-19/shared/coreServices/createPhase19CoreServices.js';
9+
import createPhase19RuntimeLayer from '../../samples/phase-19/shared/runtimeLayer/createPhase19RuntimeLayer.js';
910
import Phase19FoundationScene from '../../samples/phase-19/1901/Phase19FoundationScene.js';
1011

1112
function createRendererProbe(width = 960, height = 540) {
@@ -57,7 +58,8 @@ function assertCoreServiceLifecycleAndCommunication() {
5758

5859
function assertPhase19SampleWiring() {
5960
const coreServices = createPhase19CoreServices();
60-
const scene = new Phase19FoundationScene({ coreServices });
61+
const runtimeLayer = createPhase19RuntimeLayer({ coreServices });
62+
const scene = new Phase19FoundationScene({ runtimeLayer });
6163

6264
scene.enter({});
6365
scene.update(0.55);
@@ -75,7 +77,7 @@ function assertPhase19SampleWiring() {
7577
assert.equal(
7678
coreServices.getLifecycleState().running,
7779
false,
78-
'Scene exit should stop core services.'
80+
'Scene exit should stop core services through runtime layer.'
7981
);
8082
}
8183

0 commit comments

Comments
 (0)