feat: dashboard source coverage를 안전하게 활성화

엔진의 부대·예약턴·집계 변화와 mailbox writer를 저널에 연결하고 dashboard.global 및 인증 아이콘 source를 dependency vector에 포함한다. post-deploy coverage v1 활성화는 shared head seed와 CAS를 한 transaction으로 수행한다.
This commit is contained in:
2026-08-16 18:51:17 +00:00
parent 346f796cfc
commit 1a77bb3df1
16 changed files with 404 additions and 22 deletions
+33 -1
View File
@@ -463,6 +463,31 @@ export const createReadModelChangeJournal = (changes: RealtimeReadModelChanges):
return journal;
};
/**
* Conservative transitive dependency for the private dashboard projections.
* Context reads troop/leader-turn state and command options scan the complete
* general/city/nation sets, so entity-local heads alone are insufficient.
*/
export const hasDashboardSourceMutation = (
changes: TurnWorldChanges,
readModelChanges: RealtimeReadModelChanges
): boolean =>
readModelChanges.worldChanged ||
readModelChanges.reservedGeneralIds.length > 0 ||
changes.generals.length > 0 ||
changes.createdGenerals.length > 0 ||
changes.deletedGenerals.length > 0 ||
changes.lifecycleEvents.length > 0 ||
changes.cities.length > 0 ||
changes.nations.length > 0 ||
changes.createdNations.length > 0 ||
changes.deletedNations.length > 0 ||
changes.troops.length > 0 ||
changes.createdTroops.length > 0 ||
changes.deletedTroops.length > 0 ||
changes.diplomacy.length > 0 ||
changes.createdDiplomacy.length > 0;
export const excludeDeletedReservedTurnQueues = (
changes: ReservedTurnChanges,
deletedGeneralIds: readonly number[],
@@ -1463,13 +1488,15 @@ export const createDatabaseTurnHooks = async (
for (const snapshot of pendingYearbookSnapshots) {
await persistYearbookSnapshot(prisma, snapshot);
}
const persistedMessageMailboxes: number[] = [];
for (const finalization of pendingUnificationFinalizations) {
if (options?.profileName && finalization.profileName !== options.profileName) {
throw new Error(
`Unification profile mismatch: pending=${finalization.profileName}, daemon=${options.profileName}.`
);
}
await persistUnificationFinalization(prisma, finalization, world);
const result = await persistUnificationFinalization(prisma, finalization, world);
persistedMessageMailboxes.push(...result.messageMailboxes);
}
for (const message of messages) {
await sendMessage(
@@ -1506,6 +1533,7 @@ export const createDatabaseTurnHooks = async (
if (!id) {
throw new Error('Failed to persist turn message.');
}
persistedMessageMailboxes.push(draft.mailbox);
return id;
},
},
@@ -1555,6 +1583,10 @@ export const createDatabaseTurnHooks = async (
readModelChanges.worldChanged = true;
}
const journal = createReadModelChangeJournal(readModelChanges);
if (hasDashboardSourceMutation(changes, readModelChanges)) {
journal.mark('dashboard.global');
}
markIds(journal, 'messages.mailbox', uniqueSortedIds(persistedMessageMailboxes));
markIds(journal, 'access.general', accessScoreResetGeneralIds);
if (pendingNationBettingOpens.length > 0 || pendingNationBettingFinishes.length > 0) {
journal.mark('betting');