Skip to content

Commit c62b8fb

Browse files
author
DavidQ
committed
Combine the remaining Section-4 state/replay/timeline work into one low-PR pass
BUILD_PR_LEVEL_04_STATE_REPLAY_TIMELINE_COMBINED_PASS
1 parent 45b6973 commit c62b8fb

17 files changed

Lines changed: 438 additions & 39 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
1-
21
MODEL: GPT-5.4
32
REASONING: high
43

54
COMMAND:
6-
Create BUILD_PR_SHARED_RULE_PROMOTION_AND_TASK_SPLIT
5+
Create `BUILD_PR_LEVEL_04_STATE_REPLAY_TIMELINE_COMBINED_PASS` as one combined Section-4 PR.
6+
7+
Goal:
8+
Finish as much of State, Replay, Timeline, and Authoritative Flow as truthfully possible in one pass.
79

8-
1. Locate:
9-
- shared math is now a real active layer and should continue by exact-cluster extraction only
10-
- remaining number/string/id helpers still need exact-cluster normalization
10+
Target items to close in this PR if supported:
11+
- replay/timeline boundaries normalized
12+
- state contracts extracted/confirmed
13+
- replay model
14+
- timeline orchestration
15+
- authoritative state slices
1116

12-
2. Convert:
13-
RULE:
14-
- remove checkbox
15-
- move to rules section
17+
Required work:
18+
1. Treat the remaining open Section-4 items as one coherent state-flow lane.
19+
2. Complete/confirm the state contracts needed to support replay and timeline work.
20+
3. Normalize replay model and timeline orchestration together.
21+
4. Normalize replay/timeline boundaries as part of that same pass.
22+
5. Close any remaining authoritative-state-slice residue truthfully without reopening already-stable work.
23+
6. Reuse the already-established authoritative/passive, promotion-gate, selector, and rollback work instead of redoing it.
24+
7. Close as many Section-4 items as truthfully possible in this one PR.
25+
8. If anything remains open:
26+
- keep the residue very small
27+
- report exact blockers
28+
- leave it suitable for one residue-only PR
1629

17-
TASK:
18-
- keep as checklist item
30+
Roadmap:
31+
- update status markers only
32+
- do NOT rewrite roadmap text
1933

20-
3. Do NOT:
21-
- change wording
22-
- delete content
34+
Final packaging step is REQUIRED:
35+
- package ALL changed files into this exact repo-structured ZIP:
36+
`<project folder>/tmp/BUILD_PR_LEVEL_04_STATE_REPLAY_TIMELINE_COMBINED_PASS.zip`
2337

24-
OUTPUT:
25-
<project folder>/tmp/BUILD_PR_SHARED_RULE_PROMOTION_AND_TASK_SPLIT.zip
38+
Hard rules:
39+
- combine aggressively to reduce PR count
40+
- keep the changes coherent
41+
- no unrelated repo changes
42+
- no missing ZIP

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Separate rules vs tasks in shared section
1+
Combine the remaining Section-4 state/replay/timeline work into one low-PR pass
2+
BUILD_PR_LEVEL_04_STATE_REPLAY_TIMELINE_COMBINED_PASS

docs/dev/NEXT_COMMAND.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
NEXT_PR_TBD
1+
BUILD_PR_LEVEL_04_STATE_REPLAY_TIMELINE_RESIDUE_ONLY
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
Rule promoted, task retained
1+
- Added a combined PR for the remaining Section-4 state/replay/timeline lane
2+
- Bundles contracts, replay model, timeline orchestration, authoritative slices, and boundary normalization
3+
- Intended to finish most or all of Section 4 in one pass and leave only tiny residue if needed
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
No rules remain as checklist items
1+
- replay/timeline boundaries addressed
2+
- state contracts addressed
3+
- replay model addressed
4+
- timeline orchestration addressed
5+
- authoritative state slices addressed
6+
- any residue is explicit and minimal
7+
- roadmap updated by status markers only
8+
- output ZIP created at:
9+
<project folder>/tmp/BUILD_PR_LEVEL_04_STATE_REPLAY_TIMELINE_COMBINED_PASS.zip

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,18 +214,18 @@
214214
- [x] promotion-gate lane active
215215
- [x] final promotion gate implemented and applied
216216
- [x] authoritative/passive handoff finalized
217-
- [ ] replay/timeline boundaries normalized
218-
- [.] state contracts extracted/confirmed
217+
- [x] replay/timeline boundaries normalized
218+
- [x] state contracts extracted/confirmed
219219
- [x] public selectors stabilized
220220
- [x] observability for promotion/handoff completed
221221
- [x] long-run validation completed
222222
- [x] rollback safety / abort logic completed
223223

224224
### Subcomponents
225-
- [.] state contracts
226-
- [.] authoritative state slices
227-
- [ ] replay model
228-
- [ ] timeline orchestration
225+
- [x] state contracts
226+
- [x] authoritative state slices
227+
- [x] replay model
228+
- [x] timeline orchestration
229229
- [x] selectors/public readers
230230
- [x] promotion gating
231231
- [x] rollback safety / abort logic
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# BUILD_PR_LEVEL_04_STATE_REPLAY_TIMELINE_COMBINED_PASS
2+
3+
## Purpose
4+
Close the remaining Section-4 state-flow residue in one surgical pass by normalizing replay model + timeline orchestration boundaries, confirming shared contracts, and validating authoritative state slice behavior without reopening stable lanes.
5+
6+
## Implemented Scope
7+
- added shared replay contracts under `src/shared/contracts`
8+
- exposed replay contracts through shared contract/state barrels
9+
- extracted replay model creation/normalization into `src/engine/replay/ReplayModel.js`
10+
- added bounded replay timeline orchestration in `src/engine/replay/ReplayTimeline.js`
11+
- wired `ReplaySystem` to replay-model + timeline surfaces
12+
- added timeline-focused replay tests and runner registration
13+
- updated Section-4 roadmap markers (status only)
14+
15+
## Boundary Outcomes
16+
- replay model boundary: `src/engine/replay/ReplayModel.js`
17+
- timeline orchestration boundary: `src/engine/replay/ReplayTimeline.js`
18+
- replay runtime facade: `src/engine/replay/ReplaySystem.js`
19+
- state/replay contract surface: `src/shared/contracts/*` + `src/shared/state/contracts.js`
20+
21+
## Section-4 Status Result
22+
Completed in this pass:
23+
- replay/timeline boundaries normalized
24+
- state contracts extracted/confirmed
25+
- replay model
26+
- timeline orchestration
27+
- authoritative state slices (validated with focused authoritative tests)
28+
29+
Residual blockers:
30+
- none identified in the targeted Section-4 lane
31+
32+
## Validation Run
33+
### Node parse checks
34+
- `node --check` on all touched JS/MJS files: pass
35+
36+
### Focused state-flow checks
37+
- `tests/replay/ReplaySystem.test.mjs`: pass
38+
- `tests/replay/ReplayTimeline.test.mjs`: pass
39+
- `tests/world/WorldGameStateSystem.test.mjs`: pass
40+
- `tests/world/WorldGameStateAuthoritativeHandoff.test.mjs`: pass
41+
- `tests/world/WorldGameStateAuthoritativeScore.test.mjs`: pass
42+
43+
### Not run in this focused Node-only invocation
44+
- `tests/games/GravityWellReplay.test.mjs` depends on browser-style absolute `/src/...` imports in the game scene module and does not execute in the direct Node ESM call used here.
45+
46+
## Changed Files
47+
- `src/shared/contracts/replayContracts.js`
48+
- `src/shared/contracts/index.js`
49+
- `src/shared/state/contracts.js`
50+
- `src/engine/replay/ReplayModel.js`
51+
- `src/engine/replay/ReplayTimeline.js`
52+
- `src/engine/replay/ReplaySystem.js`
53+
- `src/engine/replay/index.js`
54+
- `tests/replay/ReplaySystem.test.mjs`
55+
- `tests/replay/ReplayTimeline.test.mjs`
56+
- `tests/run-tests.mjs`
57+
- `docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md`

src/engine/replay/ReplayModel.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/14/2026
5+
ReplayModel.js
6+
*/
7+
import {
8+
SHARED_REPLAY_MODEL_CONTRACT_VERSION,
9+
} from "../../shared/contracts/index.js";
10+
11+
function cloneOrNull(value) {
12+
return value === undefined || value === null ? null : structuredClone(value);
13+
}
14+
15+
function cloneFrames(frames) {
16+
if (!Array.isArray(frames)) {
17+
return [];
18+
}
19+
const out = [];
20+
for (let i = 0; i < frames.length; i += 1) {
21+
out.push(structuredClone(frames[i]));
22+
}
23+
return out;
24+
}
25+
26+
function createReplayModel({ metadata = null, initialState = null } = {}) {
27+
return {
28+
version: SHARED_REPLAY_MODEL_CONTRACT_VERSION,
29+
metadata: cloneOrNull(metadata),
30+
initialState: cloneOrNull(initialState),
31+
frames: [],
32+
finalState: null,
33+
};
34+
}
35+
36+
function normalizeReplayModel(input) {
37+
if (!input || typeof input !== "object") {
38+
return createReplayModel();
39+
}
40+
41+
return {
42+
version: Number.isFinite(Number(input.version))
43+
? Number(input.version)
44+
: SHARED_REPLAY_MODEL_CONTRACT_VERSION,
45+
metadata: cloneOrNull(input.metadata),
46+
initialState: cloneOrNull(input.initialState),
47+
frames: cloneFrames(input.frames),
48+
finalState: cloneOrNull(input.finalState),
49+
};
50+
}
51+
52+
function withFinalState(replay, finalState = null) {
53+
const normalized = normalizeReplayModel(replay);
54+
normalized.finalState = cloneOrNull(finalState);
55+
return normalized;
56+
}
57+
58+
export {
59+
createReplayModel,
60+
normalizeReplayModel,
61+
withFinalState,
62+
};

src/engine/replay/ReplaySystem.js

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,49 @@ David Quesenberry
44
03/22/2026
55
ReplaySystem.js
66
*/
7+
import { asNonNegativeInteger } from '../../shared/math/numberNormalization.js';
8+
import { createReplayModel, normalizeReplayModel, withFinalState } from './ReplayModel.js';
9+
import { ReplayTimeline } from './ReplayTimeline.js';
10+
711
export default class ReplaySystem {
8-
constructor() {
9-
this.replay = this.createReplay();
12+
constructor({ timelineWindowFrames } = {}) {
13+
this.replay = createReplayModel();
1014
this.frames = this.replay.frames;
1115
this.playbackIndex = 0;
1216
this.recording = false;
1317
this.playing = false;
18+
this.timeline = new ReplayTimeline({ maxFrames: timelineWindowFrames });
1419
}
1520

1621
createReplay({ metadata = null, initialState = null } = {}) {
17-
return {
18-
version: 1,
19-
metadata: metadata ? structuredClone(metadata) : null,
20-
initialState: initialState ? structuredClone(initialState) : null,
21-
frames: [],
22-
finalState: null,
23-
};
22+
return createReplayModel({ metadata, initialState });
23+
}
24+
25+
rebuildTimelineFromFrames() {
26+
this.timeline.loadFromSnapshots(this.frames);
2427
}
2528

2629
startRecording({ metadata = null, initialState = null } = {}) {
27-
this.replay = this.createReplay({ metadata, initialState });
30+
this.replay = createReplayModel({ metadata, initialState });
2831
this.frames = this.replay.frames;
32+
this.timeline.clear();
2933
this.recording = true;
3034
this.playing = false;
3135
this.playbackIndex = 0;
3236
}
3337

3438
recordFrame(frame) {
3539
if (this.recording) {
36-
this.frames.push(structuredClone(frame));
40+
const clonedFrame = structuredClone(frame);
41+
this.frames.push(clonedFrame);
42+
this.timeline.pushSnapshot(this.frames.length - 1, clonedFrame);
3743
}
3844
}
3945

4046
stopRecording({ finalState = null } = {}) {
4147
this.recording = false;
42-
this.replay.finalState = finalState ? structuredClone(finalState) : null;
48+
this.replay = withFinalState(this.replay, finalState);
49+
this.frames = this.replay.frames;
4350
return [...this.frames];
4451
}
4552

@@ -48,8 +55,9 @@ export default class ReplaySystem {
4855
}
4956

5057
loadReplay(replay) {
51-
this.replay = replay ? structuredClone(replay) : this.createReplay();
58+
this.replay = normalizeReplayModel(replay);
5259
this.frames = this.replay.frames;
60+
this.rebuildTimelineFromFrames();
5361
this.playbackIndex = 0;
5462
this.recording = false;
5563
this.playing = false;
@@ -72,6 +80,39 @@ export default class ReplaySystem {
7280
return true;
7381
}
7482

83+
getTimeline() {
84+
return this.timeline.toArray();
85+
}
86+
87+
getTimelineSnapshot(frameId) {
88+
return this.timeline.getSnapshot(frameId);
89+
}
90+
91+
getNearestTimelineSnapshot(frameId) {
92+
return this.timeline.getNearestSnapshot(frameId);
93+
}
94+
95+
replaceReplayFromFrame(frameId, frames = []) {
96+
const normalizedFrameId = asNonNegativeInteger(frameId, this.frames.length);
97+
const prefix = this.frames.slice(0, normalizedFrameId);
98+
const replacementFrames = Array.isArray(frames)
99+
? frames.map((frame) => structuredClone(frame))
100+
: [];
101+
const nextFrames = [...prefix, ...replacementFrames];
102+
103+
this.replay = normalizeReplayModel({
104+
...this.replay,
105+
frames: nextFrames
106+
});
107+
this.frames = this.replay.frames;
108+
this.timeline.replaceFromFrame(normalizedFrameId, replacementFrames);
109+
if (this.playbackIndex > this.frames.length) {
110+
this.playbackIndex = this.frames.length;
111+
}
112+
113+
return this.getReplay();
114+
}
115+
75116
nextFrame() {
76117
if (!this.playing || this.playbackIndex >= this.frames.length) {
77118
this.playing = false;

0 commit comments

Comments
 (0)