Skip to content

Commit cdb1a2e

Browse files
committed
PR_26174_ALFA_003-game-hub-journey-bootstrap
1 parent a44b72f commit cdb1a2e

11 files changed

Lines changed: 498 additions & 48 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Branch Validation: PASS
2+
3+
PASS - Current branch: pr/26174-ALFA-003-game-hub-journey-bootstrap.
4+
PASS - Stack base: pr/26174-ALFA-002-game-hub-project-intake-display.
5+
PASS - Changes are scoped to Game Journey bootstrap service/repository wiring, mock persistence schema, impacted tests, and required reports.
6+
PASS - No merge to main performed.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Manual Validation Notes: PASS
2+
3+
PASS - Confirmed createGame response includes journeyBootstrap bucket records with noteKey and itemKey.
4+
PASS - Confirmed Game Journey displays all required starter bucket notes for the created Game Hub project.
5+
PASS - Confirmed source idea Journey item remains present after bootstrap.
6+
PASS - Confirmed bootstrap records are produced by the repository/Local API layer, not browser arrays.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Requirement Checklist: PASS
2+
3+
PASS - Journey bootstrap happens through API/service contract.
4+
PASS - Server/API owns authoritative journey record keys.
5+
PASS - Buckets are created in exact required order: Idea, Design, Graphics, Audio, Objects, Worlds, Interface, Controls, Rules, Progression, Play Test, Publish, Share.
6+
PASS - Initial progress placeholders are created for starter buckets.
7+
PASS - No browser-owned journey arrays were added.
8+
PASS - No silent fallbacks were added; bootstrap contract failure throws through Local API.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Validation Lane: PASS
2+
3+
Targeted Playwright impacted lane:
4+
PASS - npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
5+
6+
Notes:
7+
- Full workspace smoke was not run; targeted impacted Playwright validation was used per request.
8+
- User-facing terminology remains Game Hub / Game Journey.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# PR_26174_ALFA_003-game-hub-journey-bootstrap
2+
3+
## Purpose
4+
5+
Create starter Game Journey records when an Idea Board idea becomes a Game Hub project.
6+
7+
## Summary
8+
9+
- Added Local API/service contract bootstrap after Game Hub project creation.
10+
- Added server-owned Game Journey starter buckets in the required order with authoritative note and item keys.
11+
- Added placeholder Journey items for each starter bucket and persisted bucket order in the mock database schema.
12+
- Extended impacted Playwright coverage to verify bootstrap response keys, bucket order, Journey display, and Local API persistence.
13+
14+
## Validation
15+
16+
PASS - `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
src/dev-runtime/persistence/mock-db-store.js
2+
src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js
3+
src/dev-runtime/server/local-api-router.mjs
14
tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
25
docs_build/dev/reports/codex_review.diff
36
docs_build/dev/reports/codex_changed_files.txt
4-
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display.md
5-
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-branch-validation.txt
6-
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-requirement-checklist.txt
7-
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-validation-lane.txt
8-
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-manual-validation-notes.txt
7+
docs_build/dev/reports/PR_26174_ALFA_003-game-hub-journey-bootstrap.md
8+
docs_build/dev/reports/PR_26174_ALFA_003-game-hub-journey-bootstrap-branch-validation.txt
9+
docs_build/dev/reports/PR_26174_ALFA_003-game-hub-journey-bootstrap-requirement-checklist.txt
10+
docs_build/dev/reports/PR_26174_ALFA_003-game-hub-journey-bootstrap-validation-lane.txt
11+
docs_build/dev/reports/PR_26174_ALFA_003-game-hub-journey-bootstrap-manual-validation-notes.txt

docs_build/dev/reports/codex_review.diff

Lines changed: 280 additions & 37 deletions
Large diffs are not rendered by default.

src/dev-runtime/persistence/mock-db-store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const MOCK_DB_TABLE_SCHEMAS = Object.freeze({
125125
input_custom_action_records: Object.freeze(["key", "id", "gameId", "label", "recordOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
126126
game_journey_note_types: Object.freeze(["key", "typeSlug", "name", "seeded", "userExtensible", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
127127
game_journey_completion_metrics: Object.freeze(["key", "bucketKey", "bucketOrder", "bucketName", "friendlyDescription", "requiredForMvp", "canSkip", "plannedCount", "completedCount", "active", "status", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
128-
game_journey_notes: Object.freeze(["key", "slug", "gameKey", "ownerKey", "name", "typeKey", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
128+
game_journey_notes: Object.freeze(["key", "slug", "gameKey", "ownerKey", "name", "typeKey", "bucketOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
129129
game_journey_templates: Object.freeze(["key", "templateSlug", "originalMeaning", "systemGuidance", "linkedToolContexts", "version", "isActive", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
130130
game_journey_items: Object.freeze(["key", "gameKey", "noteKey", "status", "title", "userDetails", "templateKey", "linkedRecordType", "linkedRecordId", "indent", "order", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
131131
game_journey_activity: Object.freeze(["key", "gameKey", "noteKey", "message", "createdAt", "updatedAt", "createdBy", "updatedBy"]),

src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,23 @@ const GENERATED_ULID_SEQUENCE = Object.freeze({
101101
const RECOMMENDED_TARGET_LINKED_RECORD_TYPE = "recommended-target";
102102
const RECOMMENDED_TARGET_NOTE_KEY = GAME_JOURNEY_KEYS.notes.designPass;
103103
const SOURCE_IDEA_LINKED_RECORD_TYPE = "source-idea-note";
104+
const JOURNEY_BOOTSTRAP_LINKED_RECORD_TYPE = "journey-bootstrap-bucket";
105+
106+
export const GAME_JOURNEY_BOOTSTRAP_BUCKETS = Object.freeze([
107+
"Idea",
108+
"Design",
109+
"Graphics",
110+
"Audio",
111+
"Objects",
112+
"Worlds",
113+
"Interface",
114+
"Controls",
115+
"Rules",
116+
"Progression",
117+
"Play Test",
118+
"Publish",
119+
"Share",
120+
]);
104121

105122
export const GAME_JOURNEY_STATUSES = [
106123
{
@@ -708,6 +725,106 @@ export function createGameJourneyMockRepository(options = {}) {
708725
return idea ? `Source Idea: ${idea}` : "Source Idea";
709726
}
710727

728+
function noteTypeKeyForBootstrapBucket(bucketName) {
729+
const slug = slugSegment(bucketName, "task");
730+
const matchingType = tables.game_journey_note_types.find((type) => type.typeSlug === slug);
731+
return matchingType?.key || GAME_JOURNEY_KEYS.noteTypes.task;
732+
}
733+
734+
function ensureJourneyBootstrapBuckets(activeGame) {
735+
if (!activeGame) {
736+
return {
737+
buckets: [],
738+
createdItems: 0,
739+
createdNotes: 0,
740+
};
741+
}
742+
743+
const ownerKey = safeCurrentUserKey();
744+
const timestampValue = new Date().toISOString();
745+
let createdNotes = 0;
746+
let createdItems = 0;
747+
const bucketSummaries = [];
748+
GAME_JOURNEY_BOOTSTRAP_BUCKETS.forEach((bucketName, index) => {
749+
const bucketOrder = index + 1;
750+
const bucketSlug = slugSegment(bucketName, "bucket");
751+
const noteSlug = `journey-bucket-${slugSegment(activeGame.id || activeGame.key)}-${String(bucketOrder).padStart(2, "0")}-${bucketSlug}`;
752+
let note = tables.game_journey_notes.find(
753+
(candidate) => candidate.gameKey === activeGame.key && candidate.slug === noteSlug,
754+
);
755+
756+
if (!note) {
757+
note = {
758+
key: makeUlid(nextNoteNumber),
759+
slug: noteSlug,
760+
gameKey: activeGame.key,
761+
ownerKey,
762+
name: bucketName,
763+
typeKey: noteTypeKeyForBootstrapBucket(bucketName),
764+
bucketOrder,
765+
createdAt: timestampValue,
766+
updatedAt: timestampValue,
767+
createdBy: ownerKey,
768+
updatedBy: ownerKey,
769+
};
770+
nextNoteNumber += 1;
771+
tables.game_journey_notes.push(note);
772+
createdNotes += 1;
773+
}
774+
775+
const linkedRecordId = `${slugSegment(activeGame.id || activeGame.key)}:${String(bucketOrder).padStart(2, "0")}:${bucketSlug}`;
776+
let item = getItemsForNote(note.key).find(
777+
(candidate) =>
778+
candidate.linkedRecordType === JOURNEY_BOOTSTRAP_LINKED_RECORD_TYPE &&
779+
candidate.linkedRecordId === linkedRecordId,
780+
);
781+
782+
if (!item) {
783+
item = {
784+
key: makeUlid(nextItemNumber),
785+
gameKey: activeGame.key,
786+
noteKey: note.key,
787+
status: "not-started",
788+
title: `${bucketName} progress placeholder`,
789+
userDetails: "",
790+
createdBy: ownerKey,
791+
updatedBy: ownerKey,
792+
templateKey: "",
793+
linkedRecordType: JOURNEY_BOOTSTRAP_LINKED_RECORD_TYPE,
794+
linkedRecordId,
795+
indent: 0,
796+
order: 1,
797+
createdAt: timestampValue,
798+
updatedAt: timestampValue,
799+
};
800+
nextItemNumber += 1;
801+
tables.game_journey_items.push(item);
802+
createdItems += 1;
803+
}
804+
805+
bucketSummaries.push({
806+
bucketName,
807+
itemKey: item.key,
808+
noteKey: note.key,
809+
order: bucketOrder,
810+
});
811+
});
812+
813+
if (createdNotes || createdItems) {
814+
const firstBucket = bucketSummaries[0];
815+
selectedNoteKey = firstBucket?.noteKey || selectedNoteKey;
816+
selectedItemKey = firstBucket?.itemKey || selectedItemKey;
817+
addActivity(activeGame.key, firstBucket?.noteKey || "", `Created ${GAME_JOURNEY_BOOTSTRAP_BUCKETS.length} Game Journey starter buckets.`, ownerKey);
818+
persistTables();
819+
}
820+
821+
return {
822+
buckets: bucketSummaries,
823+
createdItems,
824+
createdNotes,
825+
};
826+
}
827+
711828
function ensureSourceIdeaJourneyItems(activeGame) {
712829
const sourceIdea = activeGame?.sourceIdea && typeof activeGame.sourceIdea === "object"
713830
? activeGame.sourceIdea
@@ -1039,7 +1156,11 @@ export function createGameJourneyMockRepository(options = {}) {
10391156
.filter(currentUserCanSeeNote)
10401157
.filter((note) => noteMatchesFilter(note, filterId))
10411158
.map((note) => hydrateNote(note, filterId))
1042-
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1159+
.sort((left, right) => {
1160+
const leftOrder = Number.isFinite(Number(left.bucketOrder)) ? Number(left.bucketOrder) : Number.POSITIVE_INFINITY;
1161+
const rightOrder = Number.isFinite(Number(right.bucketOrder)) ? Number(right.bucketOrder) : Number.POSITIVE_INFINITY;
1162+
return leftOrder - rightOrder || right.updatedAt.localeCompare(left.updatedAt);
1163+
});
10431164
}
10441165

10451166
function addNote({ name, typeKey } = {}) {
@@ -1500,11 +1621,29 @@ export function createGameJourneyMockRepository(options = {}) {
15001621
gameId === GAME_JOURNEY_KEYS.game ? GAME_JOURNEY_ROUTE_GAME_ALIAS : gameId;
15011622
const openedGame = gameWorkspaceRepository.openGame(workspaceGameId);
15021623
if (openedGame) {
1503-
ensureSourceIdeaJourneyItems(getActiveGame());
1624+
bootstrapGameJourneyForGame(getActiveGame());
15041625
}
15051626
return openedGame;
15061627
}
15071628

1629+
function bootstrapGameJourneyForGame(game = getActiveGame()) {
1630+
const activeGame = game?.key ? game : game ? { ...game, key: journeyGameKey(game) } : getActiveGame();
1631+
if (!activeGame) {
1632+
return {
1633+
buckets: [],
1634+
createdItems: 0,
1635+
createdNotes: 0,
1636+
sourceIdeaItems: [],
1637+
};
1638+
}
1639+
const bucketResult = ensureJourneyBootstrapBuckets(activeGame);
1640+
const sourceIdeaItems = ensureSourceIdeaJourneyItems(activeGame);
1641+
return {
1642+
...bucketResult,
1643+
sourceIdeaItems,
1644+
};
1645+
}
1646+
15081647
return {
15091648
getTables: async () => clone({
15101649
game_journey_completion_metrics: await completionMetricsStore.listMetrics(),
@@ -1519,6 +1658,7 @@ export function createGameJourneyMockRepository(options = {}) {
15191658
getSystemUser: () => getMockDbSystemUser(),
15201659
getActiveGame,
15211660
openGame,
1661+
bootstrapGameJourneyForGame,
15221662
clearActiveGame: () => gameWorkspaceRepository.clearTestData(),
15231663
listNoteTypes: () => clone(tables.game_journey_note_types),
15241664
addNoteType,

src/dev-runtime/server/local-api-router.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5306,6 +5306,13 @@ LIMIT 1;
53065306
}
53075307
const result = await method(...args);
53085308
assertRepositoryMethodResult(repositoryId, methodName, result);
5309+
if (repository === this.gameWorkspaceRepository && methodName === "createGame") {
5310+
const journeyBootstrap = this.gameJourneyRepository.bootstrapGameJourneyForGame(result);
5311+
if (!journeyBootstrap || !Array.isArray(journeyBootstrap.buckets)) {
5312+
throw repositoryMethodError("Game Journey bootstrap did not return starter bucket records. Restore the Local API/service contract.");
5313+
}
5314+
result.journeyBootstrap = journeyBootstrap;
5315+
}
53095316
const methodPersistsThroughToolStore =
53105317
repository === this.gameJourneyRepository && GAME_JOURNEY_TOOL_STORE_METHODS.has(methodName);
53115318
if (repositoryMethodRequiresPersistence(methodName) && !methodPersistsThroughToolStore) {

0 commit comments

Comments
 (0)