Skip to content

Commit b84e7bf

Browse files
author
DavidQ
committed
Phase 18 integration pass
1 parent d82cc9e commit b84e7bf

10 files changed

Lines changed: 219 additions & 33 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 runtime layer scaffolding and integrate with core services, package to <project folder>/tmp/BUILD_PR_LEVEL_18_4_PHASE18_RUNTIME_LAYER.zip
3+
COMMAND: Integrate Phase 18 services and runtime layer, validate flow, package to <project folder>/tmp/BUILD_PR_LEVEL_18_5_PHASE18_INTEGRATION_PASS.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 runtime layer scaffolding
1+
Phase 18 integration pass
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
- [ ] runtime initializes
2-
- [ ] loop executes
1+
- [ ] integration works
32
- [ ] no errors
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# BUILD_PR_LEVEL_18_5_PHASE18_INTEGRATION_PASS
2+
3+
## Purpose
4+
Integrate Phase 18 core services and runtime layer through a single flow entry point and validate end-to-end interaction behavior.
5+
6+
## Source of Truth
7+
- `docs/pr/PLAN_PR_LEVEL_18_5_PHASE18_INTEGRATION_PASS.md`
8+
- `docs/pr/BUILD_PR_LEVEL_18_3_PHASE18_CORE_SERVICES.md`
9+
- `docs/pr/BUILD_PR_LEVEL_18_4_PHASE18_RUNTIME_LAYER.md`
10+
11+
## Exact Build Target
12+
1. Add a Phase 18 integration-flow module that composes:
13+
- core services registry
14+
- runtime layer
15+
- flow-level event subscriptions and snapshots
16+
2. Rewire sample `1801` to use the integration-flow module instead of directly composing runtime/core services.
17+
3. Keep runtime and service modules unchanged except as needed for integration wiring.
18+
4. Add targeted runtime integration validation for:
19+
- start/update/stop flow path
20+
- runtime state event flow
21+
- heartbeat flow through integration snapshot
22+
23+
## Non-Goals
24+
- no engine-core changes
25+
- no gameplay feature implementation
26+
- no additional Phase 18 sample entries
27+
- no roadmap status changes
28+
29+
## Validation
30+
- targeted integration flow test passes
31+
- existing runtime/core service targeted tests still pass
32+
- sample `1801` renders integration flow status without errors
33+
34+
## Packaging Rule
35+
Package only this PR's created/modified files into:
36+
`tmp/BUILD_PR_LEVEL_18_5_PHASE18_INTEGRATION_PASS.zip`
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# PLAN_PR_LEVEL_18_5_PHASE18_INTEGRATION_PASS
2+
3+
Purpose:
4+
Integrate Phase 18 layers into cohesive flow.
5+
6+
Scope:
7+
- connect services + runtime
8+
- validate data flow

samples/phase-18/1801/Phase18FoundationScene.js

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,40 @@ 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({ runtimeLayer = null } = {}) {
14+
constructor({ phase18Flow = null } = {}) {
1515
super();
1616
this.elapsed = 0;
17-
this.runtimeLayer = runtimeLayer;
17+
this.phase18Flow = phase18Flow;
1818
this.lastHeartbeatTick = 0;
1919
this.lastHeartbeatTime = 0;
2020
this.lastRuntimeTransition = 'idle';
21-
this.unsubscribeHeartbeat = null;
2221
this.unsubscribeRuntimeState = null;
2322
}
2423

2524
enter(engine) {
26-
if (!this.runtimeLayer) return;
27-
const channel = this.runtimeLayer.getService('phase18.channel');
28-
if (channel && typeof channel.subscribe === 'function') {
29-
this.unsubscribeHeartbeat = channel.subscribe('phase18.heartbeat', (payload) => {
30-
this.lastHeartbeatTick = Number(payload?.tick) || 0;
31-
this.lastHeartbeatTime = Number(payload?.t) || 0;
32-
});
33-
}
34-
this.unsubscribeRuntimeState = this.runtimeLayer.onStateChange(({ previous, next }) => {
25+
if (!this.phase18Flow) return;
26+
this.unsubscribeRuntimeState = this.phase18Flow.onStateChange(({ previous, next }) => {
3527
this.lastRuntimeTransition = `${previous} -> ${next}`;
3628
});
37-
this.runtimeLayer.start({ engine, scene: this });
29+
this.phase18Flow.start({ engine, scene: this });
3830
}
3931

4032
update(dtSeconds) {
4133
this.elapsed += dtSeconds;
42-
this.runtimeLayer?.update(dtSeconds, { scene: this });
34+
this.phase18Flow?.update(dtSeconds, { scene: this });
35+
const snapshot = this.phase18Flow?.getSnapshot?.();
36+
if (snapshot?.flow) {
37+
this.lastHeartbeatTick = Number(snapshot.flow.lastHeartbeatTick) || 0;
38+
this.lastHeartbeatTime = Number(snapshot.flow.lastHeartbeatSeconds) || 0;
39+
}
4340
}
4441

4542
exit() {
46-
if (typeof this.unsubscribeHeartbeat === 'function') {
47-
this.unsubscribeHeartbeat();
48-
this.unsubscribeHeartbeat = null;
49-
}
5043
if (typeof this.unsubscribeRuntimeState === 'function') {
5144
this.unsubscribeRuntimeState();
5245
this.unsubscribeRuntimeState = null;
5346
}
54-
this.runtimeLayer?.stop({ scene: this });
47+
this.phase18Flow?.stop({ scene: this });
5548
}
5649

5750
render(renderer) {
@@ -72,17 +65,29 @@ export default class Phase18FoundationScene extends Scene {
7265
font: '16px monospace',
7366
});
7467

75-
const runtimeSnapshot = this.runtimeLayer?.getSnapshot?.() || {
68+
const phase18Snapshot = this.phase18Flow?.getSnapshot?.() || {
69+
runtime: {
70+
state: 'idle',
71+
tickCount: 0,
72+
serviceIds: [],
73+
},
74+
flow: {
75+
runtimeStateEvents: 0,
76+
},
77+
};
78+
const runtimeSnapshot = phase18Snapshot.runtime || {
7679
state: 'idle',
7780
tickCount: 0,
7881
serviceIds: [],
7982
};
83+
const flowSnapshot = phase18Snapshot.flow || { runtimeStateEvents: 0 };
8084
drawPanel(renderer, 620, 34, 300, 160, 'Phase 18 Bootstrap', [
8185
'Status: initialized',
8286
'Folder: samples/phase-18',
8387
'Entry sample: 1801',
8488
`Runtime: ${runtimeSnapshot.state} | tick ${runtimeSnapshot.tickCount}`,
8589
`Transition: ${this.lastRuntimeTransition}`,
90+
`Flow events: ${flowSnapshot.runtimeStateEvents}`,
8691
`Services: ${runtimeSnapshot.serviceIds.length}`,
8792
`Heartbeat tick: ${this.lastHeartbeatTick} @ ${this.lastHeartbeatTime.toFixed(2)}s`,
8893
]);

samples/phase-18/1801/main.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ main.js
77
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';
10-
import createPhase18CoreServices from '/samples/phase-18/shared/coreServices/createPhase18CoreServices.js';
11-
import createPhase18RuntimeLayer from '/samples/phase-18/shared/runtimeLayer/createPhase18RuntimeLayer.js';
10+
import createPhase18IntegrationFlow from '/samples/phase-18/shared/integration/createPhase18IntegrationFlow.js';
1211
import Phase18FoundationScene from './Phase18FoundationScene.js';
1312

1413
const theme = new Theme(ThemeTokens);
@@ -25,7 +24,6 @@ const engine = new Engine({
2524
input,
2625
});
2726

28-
const coreServices = createPhase18CoreServices();
29-
const runtimeLayer = createPhase18RuntimeLayer({ coreServices });
30-
engine.setScene(new Phase18FoundationScene({ runtimeLayer }));
27+
const phase18Flow = createPhase18IntegrationFlow();
28+
engine.setScene(new Phase18FoundationScene({ phase18Flow }));
3129
engine.start();
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase18IntegrationFlow.js
6+
*/
7+
import createPhase18CoreServices from '../coreServices/createPhase18CoreServices.js';
8+
import createPhase18RuntimeLayer from '../runtimeLayer/createPhase18RuntimeLayer.js';
9+
10+
export default function createPhase18IntegrationFlow({
11+
coreServices = createPhase18CoreServices(),
12+
runtimeLayer = createPhase18RuntimeLayer({ coreServices }),
13+
} = {}) {
14+
let heartbeatEvents = 0;
15+
let runtimeStateEvents = 0;
16+
let lastHeartbeatTick = 0;
17+
let lastHeartbeatSeconds = 0;
18+
let lastRuntimeState = 'idle';
19+
let heartbeatUnsubscribe = null;
20+
let runtimeStateUnsubscribe = null;
21+
22+
function attachChannels() {
23+
if (heartbeatUnsubscribe || runtimeStateUnsubscribe) return;
24+
const channel = runtimeLayer.getService('phase18.channel');
25+
if (!channel || typeof channel.subscribe !== 'function') return;
26+
27+
heartbeatUnsubscribe = channel.subscribe('phase18.heartbeat', (payload) => {
28+
heartbeatEvents += 1;
29+
lastHeartbeatTick = Number(payload?.tick) || 0;
30+
lastHeartbeatSeconds = Number(payload?.t) || 0;
31+
});
32+
runtimeStateUnsubscribe = channel.subscribe('phase18.runtime.state', (payload) => {
33+
runtimeStateEvents += 1;
34+
lastRuntimeState = String(payload?.next || lastRuntimeState);
35+
});
36+
}
37+
38+
function detachChannels() {
39+
if (typeof heartbeatUnsubscribe === 'function') {
40+
heartbeatUnsubscribe();
41+
}
42+
if (typeof runtimeStateUnsubscribe === 'function') {
43+
runtimeStateUnsubscribe();
44+
}
45+
heartbeatUnsubscribe = null;
46+
runtimeStateUnsubscribe = null;
47+
}
48+
49+
function start(context = {}) {
50+
attachChannels();
51+
return runtimeLayer.start(context);
52+
}
53+
54+
function update(dtSeconds, context = {}) {
55+
return runtimeLayer.update(dtSeconds, context);
56+
}
57+
58+
function stop(context = {}) {
59+
const stopped = runtimeLayer.stop(context);
60+
detachChannels();
61+
return stopped;
62+
}
63+
64+
function getSnapshot() {
65+
return {
66+
runtime: runtimeLayer.getSnapshot(),
67+
flow: {
68+
heartbeatEvents,
69+
runtimeStateEvents,
70+
lastHeartbeatTick,
71+
lastHeartbeatSeconds,
72+
lastRuntimeState,
73+
subscriptionsActive: Boolean(heartbeatUnsubscribe || runtimeStateUnsubscribe),
74+
},
75+
};
76+
}
77+
78+
return {
79+
start,
80+
update,
81+
stop,
82+
getSnapshot,
83+
getService: runtimeLayer.getService,
84+
getState: runtimeLayer.getState,
85+
states: runtimeLayer.states,
86+
onBeforeUpdate: runtimeLayer.onBeforeUpdate,
87+
onAfterUpdate: runtimeLayer.onAfterUpdate,
88+
onStateChange: runtimeLayer.onStateChange,
89+
getCoreServices() {
90+
return coreServices;
91+
},
92+
getRuntimeLayer() {
93+
return runtimeLayer;
94+
},
95+
};
96+
}

tests/runtime/Phase18CoreServicesSkeleton.test.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Phase18CoreServicesSkeleton.test.mjs
77
import assert from 'node:assert/strict';
88
import createPhase18CoreServices from '../../samples/phase-18/shared/coreServices/createPhase18CoreServices.js';
99
import createPhase18RuntimeLayer from '../../samples/phase-18/shared/runtimeLayer/createPhase18RuntimeLayer.js';
10+
import createPhase18IntegrationFlow from '../../samples/phase-18/shared/integration/createPhase18IntegrationFlow.js';
1011
import Phase18FoundationScene from '../../samples/phase-18/1801/Phase18FoundationScene.js';
1112

1213
function createRendererProbe(width = 960, height = 540) {
@@ -57,9 +58,8 @@ function assertCoreServiceLifecycleAndCommunication() {
5758
}
5859

5960
function assertPhase18SampleWiring() {
60-
const services = createPhase18CoreServices();
61-
const runtimeLayer = createPhase18RuntimeLayer({ coreServices: services });
62-
const scene = new Phase18FoundationScene({ runtimeLayer });
61+
const phase18Flow = createPhase18IntegrationFlow();
62+
const scene = new Phase18FoundationScene({ phase18Flow });
6363

6464
scene.enter({});
6565
scene.update(0.55);
@@ -74,7 +74,11 @@ function assertPhase18SampleWiring() {
7474
);
7575

7676
scene.exit();
77-
assert.equal(services.getLifecycleState().running, false, 'Scene exit should stop core services through runtime integration.');
77+
assert.equal(
78+
phase18Flow.getCoreServices().getLifecycleState().running,
79+
false,
80+
'Scene exit should stop core services through integration flow.'
81+
);
7882
}
7983

8084
export function run() {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
Phase18IntegrationFlowPass.test.mjs
6+
*/
7+
import assert from 'node:assert/strict';
8+
import createPhase18IntegrationFlow from '../../samples/phase-18/shared/integration/createPhase18IntegrationFlow.js';
9+
10+
function assertIntegrationFlowLifecycleAndDataFlow() {
11+
const flow = createPhase18IntegrationFlow();
12+
13+
assert.equal(flow.getState(), flow.states.IDLE, 'Integration flow should start in idle state.');
14+
assert.equal(flow.start({ source: 'integration-test' }), true, 'Integration flow should start once.');
15+
16+
flow.update(0.25, { source: 'integration-test' });
17+
flow.update(0.25, { source: 'integration-test' });
18+
flow.update(0.5, { source: 'integration-test' });
19+
20+
const runningSnapshot = flow.getSnapshot();
21+
assert.equal(runningSnapshot.runtime.state, flow.states.RUNNING, 'Runtime state should be running after start.');
22+
assert.equal(runningSnapshot.runtime.tickCount >= 3, true, 'Runtime tick count should advance through updates.');
23+
assert.equal(runningSnapshot.flow.heartbeatEvents >= 1, true, 'Heartbeat events should flow through integration layer.');
24+
assert.equal(runningSnapshot.flow.runtimeStateEvents >= 1, true, 'Runtime state events should flow through integration layer.');
25+
assert.equal(runningSnapshot.flow.lastRuntimeState, flow.states.RUNNING, 'Last runtime state should reflect running during active updates.');
26+
27+
assert.equal(flow.stop({ source: 'integration-test' }), true, 'Integration flow should stop cleanly.');
28+
const stoppedSnapshot = flow.getSnapshot();
29+
assert.equal(stoppedSnapshot.runtime.state, flow.states.STOPPED, 'Runtime state should be stopped after stop.');
30+
assert.equal(stoppedSnapshot.flow.subscriptionsActive, false, 'Integration flow subscriptions should detach after stop.');
31+
assert.equal(
32+
flow.getCoreServices().getLifecycleState().running,
33+
false,
34+
'Core services should be stopped when integration flow stops.'
35+
);
36+
}
37+
38+
export function run() {
39+
assertIntegrationFlowLifecycleAndDataFlow();
40+
}

0 commit comments

Comments
 (0)