Skip to content

Commit 98f6c55

Browse files
author
DavidQ
committed
Establish Phase 19 core services skeleton including registry, lifecycle hooks, and baseline wiring for the next runtime layer
PR: BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES
1 parent 8911a5b commit 98f6c55

13 files changed

Lines changed: 443 additions & 13 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: Create Phase 19 foundation and package to <project folder>/tmp/BUILD_PR_LEVEL_19_2_PHASE19_FOUNDATION.zip
3+
COMMAND: Implement Phase 19 core services skeleton and wiring, package to <project folder>/tmp/BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES.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 foundation structure and minimal wiring
1+
Establish Phase 19 core services skeleton including registry, lifecycle hooks, and baseline wiring for the next runtime layer
22

3-
PR: BUILD_PR_LEVEL_19_2_PHASE19_FOUNDATION
3+
PR: BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
- [ ] structure created
2-
- [ ] no errors
1+
- [ ] services initialize
2+
- [ ] no runtime errors
3+
- [ ] baseline wiring loads
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES
2+
3+
## Purpose
4+
Implement a minimal Phase 19 core-services skeleton and wire it into the existing `1901` foundation sample.
5+
6+
## Source of Truth
7+
- `docs/pr/PLAN_PR_LEVEL_19_3_PHASE19_CORE_SERVICES.md`
8+
- `docs/pr/BUILD_PR_LEVEL_19_2_PHASE19_FOUNDATION.md`
9+
10+
## Exact Build Target
11+
1. Add a Phase 19 shared core-services skeleton under:
12+
- `samples/phase-19/shared/coreServices/`
13+
2. Include:
14+
- one service contract validator
15+
- one service registry with lifecycle hooks (`start`, `update`, `stop`)
16+
- one communication service (channel/event bus wrapper)
17+
- one baseline heartbeat service
18+
- one bootstrap factory that registers baseline services
19+
3. Wire sample `1901` to use the core-services skeleton:
20+
- initialize services in `main.js`
21+
- start/update/stop via scene lifecycle hooks
22+
- render minimal service status in sample panel
23+
4. Add targeted runtime validation:
24+
- core-service lifecycle + communication path
25+
- sample `1901` service-status rendering path
26+
27+
## Non-Goals
28+
- no engine core modifications
29+
- no gameplay/system feature implementation
30+
- no runtime-layer or integration-flow implementation
31+
- no additional Phase 19 samples
32+
- no roadmap status updates
33+
34+
## Validation
35+
- targeted runtime test for service registry lifecycle + communication path passes
36+
- sample `1901` still renders and surfaces heartbeat/service status without errors
37+
38+
## Packaging Rule
39+
Package only this PR's created/modified files into:
40+
`tmp/BUILD_PR_LEVEL_19_3_PHASE19_CORE_SERVICES.zip`
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# PLAN_PR_LEVEL_19_3_PHASE19_CORE_SERVICES
2+
3+
## Purpose
4+
Define and implement a minimal Phase 19 core-services skeleton and wire it into sample `1901`.
5+
6+
## Source of Truth
7+
- `docs/pr/BUILD_PR_LEVEL_19_2_PHASE19_FOUNDATION.md`
8+
- `docs/pr/PLAN_PR_LEVEL_19_2_PHASE19_FOUNDATION.md`
9+
10+
## Scope
11+
- add Phase 19 shared core-services modules under `samples/phase-19/shared/coreServices/`
12+
- include a service contract validator, service registry, channel service, and heartbeat service
13+
- add one bootstrap factory that registers baseline services
14+
- wire sample `1901` to start/update/stop core services and render minimal service status
15+
- add targeted runtime validation for core-service lifecycle + communication path
16+
17+
## Out of Scope
18+
- no runtime-layer implementation
19+
- no integration-flow abstraction
20+
- no additional Phase 19 samples
21+
- no roadmap status updates
22+
23+
## Exit Criteria
24+
- core-services skeleton exists and is wired into `1901`
25+
- lifecycle and communication path are validated with targeted test coverage
26+
- scoped delta is ready for repo-structured packaging

samples/phase-19/1901/Phase19FoundationScene.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,45 @@ 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() {
14+
constructor({ coreServices = null } = {}) {
1515
super();
1616
this.elapsed = 0;
17+
this.coreServices = coreServices;
18+
this.lastHeartbeatTick = 0;
19+
this.lastHeartbeatTime = 0;
20+
this.unsubscribeHeartbeat = null;
21+
}
22+
23+
enter(engine) {
24+
if (!this.coreServices) return;
25+
const channel = this.coreServices.get('phase19.channel');
26+
if (channel && typeof channel.subscribe === 'function') {
27+
this.unsubscribeHeartbeat = channel.subscribe('phase19.heartbeat', (payload) => {
28+
this.lastHeartbeatTick = Number(payload?.tick) || 0;
29+
this.lastHeartbeatTime = Number(payload?.t) || 0;
30+
});
31+
}
32+
this.coreServices.start({ engine, scene: this });
1733
}
1834

1935
update(dtSeconds) {
2036
this.elapsed += dtSeconds;
37+
this.coreServices?.update(dtSeconds, { scene: this });
38+
}
39+
40+
exit() {
41+
if (typeof this.unsubscribeHeartbeat === 'function') {
42+
this.unsubscribeHeartbeat();
43+
this.unsubscribeHeartbeat = null;
44+
}
45+
this.coreServices?.stop({ scene: this });
2146
}
2247

2348
render(renderer) {
2449
drawFrame(renderer, theme, [
25-
'Sample 1901 - Phase 19 Foundation',
26-
'Minimal Phase 19 scaffold with launcher wiring.',
27-
'No feature implementation in this foundation slice.',
50+
'Sample 1901 - Phase 19 Core Services',
51+
'Minimal Phase 19 core-services skeleton wired into foundation sample.',
52+
'No feature implementation in this core-services slice.',
2853
]);
2954

3055
renderer.drawRect(120, 212, 720, 200, '#0f172a');
@@ -38,12 +63,22 @@ export default class Phase19FoundationScene extends Scene {
3863
font: '16px monospace',
3964
});
4065

66+
const lifecycle = this.coreServices?.getLifecycleState?.() || {
67+
running: false,
68+
serviceCount: 0,
69+
};
70+
const channelSnapshot = this.coreServices?.get?.('phase19.channel')?.getSnapshot?.() || {
71+
publishedCount: 0,
72+
lastChannel: 'none',
73+
};
4174
drawPanel(renderer, 620, 34, 300, 140, 'Phase 19 Bootstrap', [
42-
'Status: initialized',
75+
'Status: initialized (core services)',
4376
'Folder: samples/phase-19',
4477
'Entry sample: 1901',
45-
'Scope: structure + wiring only',
46-
'Features: deferred',
78+
`Running: ${lifecycle.running ? 'yes' : 'no'}`,
79+
`Services: ${lifecycle.serviceCount}`,
80+
`Published: ${channelSnapshot.publishedCount} (${channelSnapshot.lastChannel})`,
81+
`Heartbeat tick: ${this.lastHeartbeatTick} @ ${this.lastHeartbeatTime.toFixed(2)}s`,
4782
]);
4883
}
4984
}

samples/phase-19/1901/main.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +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 createPhase19CoreServices from '/samples/phase-19/shared/coreServices/createPhase19CoreServices.js';
1011
import Phase19FoundationScene from './Phase19FoundationScene.js';
1112

1213
const theme = new Theme(ThemeTokens);
@@ -23,5 +24,6 @@ const engine = new Engine({
2324
input,
2425
});
2526

26-
engine.setScene(new Phase19FoundationScene());
27+
const coreServices = createPhase19CoreServices();
28+
engine.setScene(new Phase19FoundationScene({ coreServices }));
2729
engine.start();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase19ChannelService.js
6+
*/
7+
import EventBus from '/src/engine/events/EventBus.js';
8+
9+
export default function createPhase19ChannelService() {
10+
const bus = new EventBus();
11+
let running = false;
12+
let publishedCount = 0;
13+
let lastChannel = 'none';
14+
15+
function publish(channel, payload = {}) {
16+
if (!running || typeof channel !== 'string' || channel.length === 0) {
17+
return 0;
18+
}
19+
lastChannel = channel;
20+
publishedCount += 1;
21+
return bus.emit(channel, payload);
22+
}
23+
24+
function subscribe(channel, handler) {
25+
return bus.on(channel, handler);
26+
}
27+
28+
return {
29+
id: 'phase19.channel',
30+
publish,
31+
subscribe,
32+
getSnapshot() {
33+
return {
34+
running,
35+
publishedCount,
36+
lastChannel,
37+
};
38+
},
39+
onStart() {
40+
running = true;
41+
},
42+
onStop() {
43+
running = false;
44+
bus.clear();
45+
},
46+
};
47+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase19CoreServices.js
6+
*/
7+
import createPhase19ServiceRegistry from './createPhase19ServiceRegistry.js';
8+
import createPhase19ChannelService from './createPhase19ChannelService.js';
9+
import createPhase19HeartbeatService from './createPhase19HeartbeatService.js';
10+
11+
export default function createPhase19CoreServices() {
12+
const registry = createPhase19ServiceRegistry();
13+
registry.register(createPhase19ChannelService());
14+
registry.register(createPhase19HeartbeatService());
15+
return registry;
16+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase19HeartbeatService.js
6+
*/
7+
export default function createPhase19HeartbeatService({ intervalSeconds = 0.5 } = {}) {
8+
const safeInterval = Math.max(0.05, Number(intervalSeconds) || 0.5);
9+
let running = false;
10+
let elapsed = 0;
11+
let ticks = 0;
12+
let lastBeatSeconds = 0;
13+
14+
return {
15+
id: 'phase19.heartbeat',
16+
onStart() {
17+
running = true;
18+
elapsed = 0;
19+
ticks = 0;
20+
lastBeatSeconds = 0;
21+
},
22+
onUpdate(dtSeconds, context = {}) {
23+
if (!running) return;
24+
const dt = Math.max(0, Number(dtSeconds) || 0);
25+
elapsed += dt;
26+
while (elapsed >= safeInterval) {
27+
elapsed -= safeInterval;
28+
ticks += 1;
29+
lastBeatSeconds += safeInterval;
30+
const channel = context.getService?.('phase19.channel');
31+
if (channel && typeof channel.publish === 'function') {
32+
channel.publish('phase19.heartbeat', {
33+
tick: ticks,
34+
t: Number(lastBeatSeconds.toFixed(3)),
35+
});
36+
}
37+
}
38+
},
39+
onStop() {
40+
running = false;
41+
},
42+
getSnapshot() {
43+
return {
44+
running,
45+
intervalSeconds: safeInterval,
46+
ticks,
47+
lastBeatSeconds: Number(lastBeatSeconds.toFixed(3)),
48+
};
49+
},
50+
};
51+
}

0 commit comments

Comments
 (0)