fix core domestic command persistence parity

This commit is contained in:
2026-07-26 20:34:36 +00:00
parent a7de5778ed
commit a1dacda59e
8 changed files with 173 additions and 44 deletions
@@ -127,6 +127,7 @@ export const projectCoreDatabaseSnapshot = (rows: {
belong: readNumber(row, 'belong', readNumber(meta, 'belong')),
permission: readString(row, 'permission') ?? readString(meta, 'permission') ?? 'normal',
maxBelong: readNumber(meta, 'max_belong'),
maxDomesticCritical: readNumber(meta, 'max_domestic_critical'),
betray: row.betray,
personality: row.personality ?? null,
specialDomestic: row.specialDomestic ?? null,
@@ -548,6 +548,7 @@ const projectWorld = (
belong: readNumber(general.meta, 'belong'),
permission: readString(general.meta, 'permission', 'normal'),
maxBelong: readNumber(general.meta, 'max_belong'),
maxDomesticCritical: readNumber(general.meta, 'max_domestic_critical'),
betray: readNumber(general.meta, 'betray'),
personality: general.role.personality,
specialDomestic: general.role.specialDomestic,
@@ -4,6 +4,27 @@ import path from 'node:path';
import type { CanonicalTurnCommandTrace, CanonicalTurnSnapshot, TurnSnapshotSelector } from './canonical.js';
const withProjectedGeneralMeta = (snapshot: CanonicalTurnSnapshot): CanonicalTurnSnapshot => ({
...snapshot,
generals: snapshot.generals.map((general) => {
const meta =
typeof general.meta === 'object' && general.meta !== null && !Array.isArray(general.meta)
? (general.meta as Record<string, unknown>)
: {};
const value = meta.max_domestic_critical;
return {
...general,
maxDomesticCritical: typeof value === 'number' && Number.isFinite(value) ? value : 0,
};
}),
});
const withProjectedTraceMeta = (trace: CanonicalTurnCommandTrace): CanonicalTurnCommandTrace => ({
...trace,
before: withProjectedGeneralMeta(trace.before),
after: withProjectedGeneralMeta(trace.after),
});
export const findTurnDifferentialWorkspaceRoot = (start: string): string | null => {
let current = path.resolve(start);
while (true) {
@@ -45,7 +66,7 @@ export const readReferenceDatabaseSnapshot = (
stdio: ['pipe', 'pipe', 'pipe'],
}
);
return JSON.parse(stdout) as CanonicalTurnSnapshot;
return withProjectedGeneralMeta(JSON.parse(stdout) as CanonicalTurnSnapshot);
};
export const runReferenceTurnCommandTrace = (workspaceRoot: string, fixturePath: string): CanonicalTurnCommandTrace => {
@@ -60,7 +81,7 @@ export const runReferenceTurnCommandTrace = (workspaceRoot: string, fixturePath:
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return JSON.parse(stdout) as CanonicalTurnCommandTrace;
return withProjectedTraceMeta(JSON.parse(stdout) as CanonicalTurnCommandTrace);
};
export const runReferenceTurnCommandTraceRequest = (
@@ -79,5 +100,5 @@ export const runReferenceTurnCommandTraceRequest = (
TURN_DIFFERENTIAL_STACK_DIR: stackDirectory,
},
});
return JSON.parse(stdout) as CanonicalTurnCommandTrace;
return withProjectedTraceMeta(JSON.parse(stdout) as CanonicalTurnCommandTrace);
};
@@ -1281,6 +1281,112 @@ integration('general domestic command boundary, value, and log parity', () => {
);
});
interface CoreDomesticBoundaryCase {
name: string;
action: 'che_농지개간' | 'che_상업투자' | 'che_주민선정' | 'che_정착장려' | 'che_기술연구';
actorPatch?: Record<string, unknown>;
fixturePatches?: FixturePatches;
hiddenSeed: string;
completed: boolean;
}
const coreDomesticBoundaryCases: CoreDomesticBoundaryCase[] = [
{
name: 'agriculture rejects a city at its exact capacity',
action: 'che_농지개간',
fixturePatches: { cities: { 3: { agriculture: 1_000, agricultureMax: 1_000 } } },
hiddenSeed: 'general-core-domestic-agriculture-capacity',
completed: false,
},
{
name: 'agriculture preserves the early capital front adjustment',
action: 'che_농지개간',
fixturePatches: {
world: { year: 183 },
cities: { 3: { frontState: 1, agriculture: 1_000, agricultureMax: 2_000 } },
},
hiddenSeed: 'turn-command-general-matrix-v1',
completed: true,
},
{
name: 'commerce preserves the non-capital front debuff and integer city storage',
action: 'che_상업투자',
fixturePatches: {
nations: { 1: { capitalCityId: 70 } },
cities: { 3: { frontState: 1, commerce: 1_000, commerceMax: 2_000 } },
},
hiddenSeed: 'turn-command-general-matrix-v1',
completed: true,
},
{
name: 'resident selection preserves fractional FLOAT storage and accumulates critical score by half',
action: 'che_주민선정',
actorPatch: { meta: { max_domestic_critical: 10 } },
fixturePatches: {
cities: { 3: { trust: 10 } },
},
hiddenSeed: 'turn-command-general-matrix-v1',
completed: true,
},
{
name: 'settlement critical success uses the shared half-score accumulator',
action: 'che_정착장려',
actorPatch: { meta: { max_domestic_critical: 10 } },
hiddenSeed: 'turn-command-general-matrix-v1',
completed: true,
},
{
name: 'technology critical success uses the shared half-score accumulator',
action: 'che_기술연구',
actorPatch: { meta: { max_domestic_critical: 10 } },
hiddenSeed: 'turn-command-general-matrix-v1',
completed: true,
},
{
name: 'commerce failure resets the current domestic critical accumulator',
action: 'che_상업투자',
actorPatch: { meta: { max_domestic_critical: 10 } },
hiddenSeed: 'general-failure-che_상업투자-2',
completed: true,
},
];
integration('core domestic command critical, front, and storage boundary parity', () => {
it.each(coreDomesticBoundaryCases)(
'$name',
async ({ action, actorPatch, fixturePatches, hiddenSeed, completed }) => {
const request = buildRequest(action, undefined, actorPatch, fixturePatches);
request.setup!.world!.hiddenSeed = hiddenSeed;
request.observe!.includeGlobalHistoryLogs = true;
const reference = runReferenceTurnCommandTraceRequest(
workspaceRoot!,
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
expect(reference.execution.outcome).toMatchObject({ completed });
expect(core.execution.outcome).toMatchObject({
requestedAction: action,
actionKey: completed ? action : '휴식',
usedFallback: !completed,
});
expect(core.rng).toEqual(reference.rng);
expect(
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
ignoredPathPatterns: ignoredLifecyclePaths,
})
).toEqual([]);
if (completed) {
expect(semanticLogSignatures(core.after.logs)).toEqual(
semanticLogSignatures(addedReferenceLogs(reference.before, reference.after.logs))
);
}
},
120_000
);
});
interface MilitaryPreparationBoundaryCase {
name: string;
action: 'che_모병' | 'che_징병' | 'che_장비매매' | 'che_소집해제' | 'cr_맹훈련';
@@ -2365,16 +2471,7 @@ const generalStatusTransitionBoundaryCases: GeneralStatusTransitionBoundaryCase[
integration('general resignation, retirement, and abdication boundary, state, and log parity', () => {
it.each(generalStatusTransitionBoundaryCases)(
'$name',
async ({
name,
action,
args,
actorPatch,
fixturePatches,
completed,
expectedActionKey,
compareLogs,
}) => {
async ({ name, action, args, actorPatch, fixturePatches, completed, expectedActionKey, compareLogs }) => {
const request = buildRequest(action, args, actorPatch, fixturePatches);
request.setup!.world!.hiddenSeed = `general-status-transition-${name}`;
request.observe!.includeGlobalHistoryLogs = true;
@@ -2550,16 +2647,7 @@ const generalFoundingRebellionBoundaryCases: GeneralFoundingRebellionBoundaryCas
integration('general uprising, rebellion, and founding boundary, state, RNG, and log parity', () => {
it.each(generalFoundingRebellionBoundaryCases)(
'$name',
async ({
name,
action,
args,
actorPatch,
fixturePatches,
completed,
expectedActionKey,
compareLogs,
}) => {
async ({ name, action, args, actorPatch, fixturePatches, completed, expectedActionKey, compareLogs }) => {
const request = buildRequest(action, args, actorPatch, fixturePatches);
request.setup!.world!.hiddenSeed = `general-founding-rebellion-${name}`;
if (action === 'che_거병') {