관리 daemon의 실행 commit SHA를 NPC 감사 결정에 기록
This commit is contained in:
@@ -99,3 +99,7 @@ VITE_BOARD_TIP_URL=
|
|||||||
VITE_BOARD_PATCH_URL=
|
VITE_BOARD_PATCH_URL=
|
||||||
VITE_OFFICIAL_CHAT_URL=
|
VITE_OFFICIAL_CHAT_URL=
|
||||||
VITE_CASUAL_CHAT_URL=
|
VITE_CASUAL_CHAT_URL=
|
||||||
|
|
||||||
|
# 수동 turn-daemon 기동 시 실행 산출물의 확정된 전체 commit SHA. 미확인이면 비워 둔다.
|
||||||
|
# Gateway 관리 daemon은 프로필 buildCommitSha를 자동 전달하므로 수동 설정하지 않는다.
|
||||||
|
TURN_BUILD_COMMIT_SHA=
|
||||||
|
|||||||
@@ -2274,7 +2274,7 @@ integration('game API security over HTTP transport', () => {
|
|||||||
month: 1,
|
month: 1,
|
||||||
tick: 4_320_000_000n,
|
tick: 4_320_000_000n,
|
||||||
stepCount: 129,
|
stepCount: 129,
|
||||||
summary: decisionSummary,
|
summary: { ...decisionSummary, codeVersion: index ? null : 'a'.repeat(40) },
|
||||||
hash: id,
|
hash: id,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
@@ -2309,7 +2309,7 @@ integration('game API security over HTTP transport', () => {
|
|||||||
result: {
|
result: {
|
||||||
data: {
|
data: {
|
||||||
coverage: 'PROCEDURES_ONLY',
|
coverage: 'PROCEDURES_ONLY',
|
||||||
items: [{ id: decisionIds[0], tick: '4320000000' }],
|
items: [{ id: decisionIds[0], tick: '4320000000', summary: { codeVersion: 'a'.repeat(40) } }],
|
||||||
nextCursor: { tick: '4320000000', id: decisionIds[0] },
|
nextCursor: { tick: '4320000000', id: decisionIds[0] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2323,7 +2323,9 @@ integration('game API security over HTTP transport', () => {
|
|||||||
cursor: { tick: '4320000000', id: decisionIds[0] },
|
cursor: { tick: '4320000000', id: decisionIds[0] },
|
||||||
})
|
})
|
||||||
).body
|
).body
|
||||||
).toMatchObject({ result: { data: { items: [{ id: decisionIds[1] }], nextCursor: null } } });
|
).toMatchObject({
|
||||||
|
result: { data: { items: [{ id: decisionIds[1], summary: { codeVersion: null } }], nextCursor: null } },
|
||||||
|
});
|
||||||
expect((await get('decisionHistory', admin, { ...decisionInput, phase: 'nation' })).body).toMatchObject({
|
expect((await get('decisionHistory', admin, { ...decisionInput, phase: 'nation' })).body).toMatchObject({
|
||||||
result: { data: { items: [{ id: decisionIds[1] }] } },
|
result: { data: { items: [{ id: decisionIds[1] }] } },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,3 +32,9 @@ export interface PendingAuditDecision {
|
|||||||
|
|
||||||
export const auditDecisionIdentity = (serverId: string, generalId: number, tick: number, revision: number) =>
|
export const auditDecisionIdentity = (serverId: string, generalId: number, tick: number, revision: number) =>
|
||||||
auditPolicyHash([serverId, generalId, tick, revision]);
|
auditPolicyHash([serverId, generalId, tick, revision]);
|
||||||
|
|
||||||
|
// 확정된 전체 SHA만 받는다. branch/tag/축약 SHA나 미상 값을 현재 checkout에서 추정하지 않는다.
|
||||||
|
export const normalizeAuditCodeVersion = (value: string | undefined): string | undefined => {
|
||||||
|
const sha = value?.trim().toLowerCase();
|
||||||
|
return sha && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha) && !/^0+$/.test(sha) ? sha : undefined;
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { TurnDaemonLeaseUnavailableError } from '../lifecycle/databaseTurnDaemon
|
|||||||
export interface TurnDaemonCliOptions {
|
export interface TurnDaemonCliOptions {
|
||||||
profile?: string;
|
profile?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
|
auditCodeVersion?: string;
|
||||||
scenario?: string;
|
scenario?: string;
|
||||||
databaseUrl?: string;
|
databaseUrl?: string;
|
||||||
gatewayDatabaseUrl?: string;
|
gatewayDatabaseUrl?: string;
|
||||||
@@ -84,6 +85,7 @@ export const runTurnDaemonCli = async (options: TurnDaemonCliOptions = {}): Prom
|
|||||||
createTurnDaemonRuntime({
|
createTurnDaemonRuntime({
|
||||||
profile,
|
profile,
|
||||||
profileName,
|
profileName,
|
||||||
|
auditCodeVersion: options.auditCodeVersion ?? env.TURN_BUILD_COMMIT_SHA,
|
||||||
databaseUrl,
|
databaseUrl,
|
||||||
gatewayDatabaseUrl,
|
gatewayDatabaseUrl,
|
||||||
defaultBudget: budget,
|
defaultBudget: budget,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { auditDecisionIdentity, type PendingAuditDecision } from '../playAudit/decision.js';
|
import { auditDecisionIdentity, normalizeAuditCodeVersion, type PendingAuditDecision } from '../playAudit/decision.js';
|
||||||
import { auditPolicyHash, AUDIT_POLICY_AREAS } from '../playAudit/policy.js';
|
import { auditPolicyHash, AUDIT_POLICY_AREAS } from '../playAudit/policy.js';
|
||||||
import type { AiDecisionTraceEvent } from './ai/generalAi/trace.js';
|
import type { AiDecisionTraceEvent } from './ai/generalAi/trace.js';
|
||||||
import type { AiDecisionTraceObserver } from './ai/generalAi/trace.js';
|
import type { AiDecisionTraceObserver } from './ai/generalAi/trace.js';
|
||||||
@@ -934,6 +934,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
actionDurationNs: bigint;
|
actionDurationNs: bigint;
|
||||||
}) => void;
|
}) => void;
|
||||||
}): Promise<GeneralTurnHandler> => {
|
}): Promise<GeneralTurnHandler> => {
|
||||||
|
const auditCodeVersion = normalizeAuditCodeVersion(options.auditCodeVersion);
|
||||||
const env = options.commandEnv ?? buildCommandEnv(options.scenarioConfig, options.unitSet);
|
const env = options.commandEnv ?? buildCommandEnv(options.scenarioConfig, options.unitSet);
|
||||||
const itemRegistry = createItemModuleRegistry(await loadItemModules([...ITEM_KEYS]));
|
const itemRegistry = createItemModuleRegistry(await loadItemModules([...ITEM_KEYS]));
|
||||||
const uniqueConfig = resolveUniqueConfig(asRecord(options.scenarioConfig.const));
|
const uniqueConfig = resolveUniqueConfig(asRecord(options.scenarioConfig.const));
|
||||||
@@ -1143,7 +1144,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
coverage: 'PROCEDURES',
|
coverage: 'PROCEDURES',
|
||||||
clockRevision: revision,
|
clockRevision: revision,
|
||||||
codeVersion: options.auditCodeVersion ?? null,
|
codeVersion: auditCodeVersion ?? null,
|
||||||
policyRefs: decisionPolicyRefs.get(phase) ?? {},
|
policyRefs: decisionPolicyRefs.get(phase) ?? {},
|
||||||
requestedAction: first.reservedAction,
|
requestedAction: first.reservedAction,
|
||||||
selectedAction: last.action,
|
selectedAction: last.action,
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ import { applyRuntimeGameSettings } from './runtimeGameSettings.js';
|
|||||||
export interface TurnDaemonRuntimeOptions {
|
export interface TurnDaemonRuntimeOptions {
|
||||||
profile: string;
|
profile: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
|
auditCodeVersion?: string;
|
||||||
databaseUrl: string;
|
databaseUrl: string;
|
||||||
gatewayDatabaseUrl?: string;
|
gatewayDatabaseUrl?: string;
|
||||||
defaultBudget?: TurnRunBudget;
|
defaultBudget?: TurnRunBudget;
|
||||||
@@ -779,6 +780,7 @@ const createTurnDaemonRuntimeWithLease = async (
|
|||||||
options.generalTurnHandler ??
|
options.generalTurnHandler ??
|
||||||
(await createReservedTurnHandler({
|
(await createReservedTurnHandler({
|
||||||
reservedTurns: reservedTurnStoreHandle!.store,
|
reservedTurns: reservedTurnStoreHandle!.store,
|
||||||
|
auditCodeVersion: options.auditCodeVersion,
|
||||||
scenarioConfig: snapshot.scenarioConfig,
|
scenarioConfig: snapshot.scenarioConfig,
|
||||||
scenarioMeta: snapshot.scenarioMeta,
|
scenarioMeta: snapshot.scenarioMeta,
|
||||||
map: snapshot.map,
|
map: snapshot.map,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { normalizeAuditCodeVersion } from '../src/playAudit/decision.js';
|
||||||
|
const runtimeFactory = vi.hoisted(() => vi.fn());
|
||||||
|
vi.mock('../src/turn/turnDaemon.js', () => ({ createTurnDaemonRuntime: runtimeFactory }));
|
||||||
|
vi.mock('../src/turn/turnDaemonMemoryReporter.js', () => ({
|
||||||
|
createTurnDaemonMemoryReporter: () => ({ report: vi.fn(), stop: vi.fn() }),
|
||||||
|
}));
|
||||||
|
import { runTurnDaemonCli } from '../src/turn/cli.js';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
runtimeFactory.mockReset();
|
||||||
|
});
|
||||||
|
describe('audit runtime build identity', () => {
|
||||||
|
it.each([undefined, '', 'main', 'abcd1234', '0'.repeat(40), 'a'.repeat(41), 'g'.repeat(40)])(
|
||||||
|
'leaves unconfirmed SHA %s unknown',
|
||||||
|
(value) => {
|
||||||
|
expect(normalizeAuditCodeVersion(value)).toBeUndefined();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
it.each([40, 64])('normalizes an exact %s digit SHA', (length) => {
|
||||||
|
expect(normalizeAuditCodeVersion(` ${'A'.repeat(length)} `)).toBe('a'.repeat(length));
|
||||||
|
});
|
||||||
|
it.each([undefined, 'a'.repeat(40)])('passes the startup build identity to the runtime', async (sha) => {
|
||||||
|
vi.spyOn(process, 'on').mockReturnValue(process);
|
||||||
|
vi.spyOn(console, 'info').mockImplementation(() => {});
|
||||||
|
const close = vi.fn();
|
||||||
|
runtimeFactory.mockResolvedValue({
|
||||||
|
lifecycle: { start: vi.fn(), stop: vi.fn() },
|
||||||
|
close,
|
||||||
|
});
|
||||||
|
await runTurnDaemonCli({
|
||||||
|
profile: 'che',
|
||||||
|
profileName: 'che:fixture',
|
||||||
|
tickMinutes: 10,
|
||||||
|
databaseUrl: 'postgresql://fixture/game',
|
||||||
|
gatewayDatabaseUrl: 'postgresql://fixture/gateway',
|
||||||
|
env: sha ? { TURN_BUILD_COMMIT_SHA: sha } : {},
|
||||||
|
});
|
||||||
|
expect(runtimeFactory).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ auditCodeVersion: sha, profileName: 'che:fixture' })
|
||||||
|
);
|
||||||
|
expect(close).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -73,6 +73,7 @@ export type TurnTestHarnessOptions = {
|
|||||||
};
|
};
|
||||||
worldRef?: { current: InMemoryTurnWorld | null };
|
worldRef?: { current: InMemoryTurnWorld | null };
|
||||||
onDecisionTrace?: Parameters<typeof createReservedTurnHandler>[0]['onDecisionTrace'];
|
onDecisionTrace?: Parameters<typeof createReservedTurnHandler>[0]['onDecisionTrace'];
|
||||||
|
auditCodeVersion?: string;
|
||||||
onActionResolved?: Parameters<typeof createReservedTurnHandler>[0]['onActionResolved'];
|
onActionResolved?: Parameters<typeof createReservedTurnHandler>[0]['onActionResolved'];
|
||||||
onActionProfiled?: Parameters<typeof createReservedTurnHandler>[0]['onActionProfiled'];
|
onActionProfiled?: Parameters<typeof createReservedTurnHandler>[0]['onActionProfiled'];
|
||||||
commandRngFactory?: Parameters<typeof createReservedTurnHandler>[0]['commandRngFactory'];
|
commandRngFactory?: Parameters<typeof createReservedTurnHandler>[0]['commandRngFactory'];
|
||||||
@@ -115,6 +116,7 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
|
|||||||
unitSet: options.snapshot.unitSet,
|
unitSet: options.snapshot.unitSet,
|
||||||
getWorld: () => worldRef.current,
|
getWorld: () => worldRef.current,
|
||||||
onDecisionTrace: options.onDecisionTrace,
|
onDecisionTrace: options.onDecisionTrace,
|
||||||
|
auditCodeVersion: options.auditCodeVersion,
|
||||||
onActionResolved: options.onActionResolved,
|
onActionResolved: options.onActionResolved,
|
||||||
onActionProfiled: options.onActionProfiled,
|
onActionProfiled: options.onActionProfiled,
|
||||||
commandRngFactory: options.commandRngFactory,
|
commandRngFactory: options.commandRngFactory,
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ describe('NPC 선전포고·개전·점령 흐름 테스트', () => {
|
|||||||
const decisionTrace: AiDecisionTraceEvent[] = [];
|
const decisionTrace: AiDecisionTraceEvent[] = [];
|
||||||
const savedDecisions: PendingAuditDecision[] = [];
|
const savedDecisions: PendingAuditDecision[] = [];
|
||||||
const { runUntil } = await createTurnTestHarness({
|
const { runUntil } = await createTurnTestHarness({
|
||||||
|
auditCodeVersion: 'a'.repeat(40),
|
||||||
onDecisionTrace: auditEnabled ? (event) => decisionTrace.push(event) : undefined,
|
onDecisionTrace: auditEnabled ? (event) => decisionTrace.push(event) : undefined,
|
||||||
wrapGeneralTurnHandler: (handler) => ({ execute: (context) => {
|
wrapGeneralTurnHandler: (handler) => ({ execute: (context) => {
|
||||||
const result = handler.execute(context);
|
const result = handler.execute(context);
|
||||||
@@ -447,6 +448,7 @@ describe('NPC 선전포고·개전·점령 흐름 테스트', () => {
|
|||||||
expect(dispatchCount).toBeGreaterThan(0);
|
expect(dispatchCount).toBeGreaterThan(0);
|
||||||
if (auditEnabled) {
|
if (auditEnabled) {
|
||||||
expect(savedDecisions.length).toBeGreaterThan(0);
|
expect(savedDecisions.length).toBeGreaterThan(0);
|
||||||
|
expect(savedDecisions.every((row) => row.summary.codeVersion === 'a'.repeat(40))).toBe(true);
|
||||||
expect(new Set(savedDecisions.map((row) => row.id)).size).toBe(savedDecisions.length);
|
expect(new Set(savedDecisions.map((row) => row.id)).size).toBe(savedDecisions.length);
|
||||||
expect(savedDecisions.some((row) => row.phase === 'nation' && row.summary.executedAction === 'che_선전포고')).toBe(true);
|
expect(savedDecisions.some((row) => row.phase === 'nation' && row.summary.executedAction === 'che_선전포고')).toBe(true);
|
||||||
expect(savedDecisions.some((row) => row.phase === 'general' && row.summary.executedAction === 'che_출병')).toBe(true);
|
expect(savedDecisions.some((row) => row.phase === 'general' && row.summary.executedAction === 'che_출병')).toBe(true);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ integration('decision persistence and bounded retention', () => {
|
|||||||
});
|
});
|
||||||
it('rolls back gameplay/header/chunks on insert failure, retries and rejects divergent replay', async () => {
|
it('rolls back gameplay/header/chunks on insert failure, retries and rejects divergent replay', async () => {
|
||||||
const decision = draft('decision-one');
|
const decision = draft('decision-one');
|
||||||
|
decision.summary.codeVersion = 'a'.repeat(40);
|
||||||
await db.$executeRawUnsafe(
|
await db.$executeRawUnsafe(
|
||||||
`CREATE OR REPLACE FUNCTION decision_fixture_failure() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.ordinal=1 THEN RAISE EXCEPTION 'decision chunk failure'; END IF; RETURN NEW; END $$`
|
`CREATE OR REPLACE FUNCTION decision_fixture_failure() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.ordinal=1 THEN RAISE EXCEPTION 'decision chunk failure'; END IF; RETURN NEW; END $$`
|
||||||
);
|
);
|
||||||
@@ -61,7 +62,18 @@ integration('decision persistence and bounded retention', () => {
|
|||||||
});
|
});
|
||||||
await db.$transaction((tx) => persistAuditDecisions(tx, [decision]));
|
await db.$transaction((tx) => persistAuditDecisions(tx, [decision]));
|
||||||
const header = await db.playAuditDecision.findUniqueOrThrow({ where: { id: decision.id } });
|
const header = await db.playAuditDecision.findUniqueOrThrow({ where: { id: decision.id } });
|
||||||
expect(header).toMatchObject({ tick: 4_320_000_000n, stepCount: 302 });
|
expect(header).toMatchObject({
|
||||||
|
tick: 4_320_000_000n,
|
||||||
|
stepCount: 302,
|
||||||
|
summary: { codeVersion: 'a'.repeat(40) },
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
db.$transaction((tx) =>
|
||||||
|
persistAuditDecisions(tx, [
|
||||||
|
{ ...decision, summary: { ...decision.summary, codeVersion: 'b'.repeat(40) } },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
).rejects.toThrow('replay conflict');
|
||||||
const chunks = await db.playAuditDecisionChunk.findMany({
|
const chunks = await db.playAuditDecisionChunk.findMany({
|
||||||
where: { decisionId: decision.id },
|
where: { decisionId: decision.id },
|
||||||
orderBy: { ordinal: 'asc' },
|
orderBy: { ordinal: 'asc' },
|
||||||
|
|||||||
@@ -600,6 +600,8 @@ export const buildProcessDefinitions = (
|
|||||||
...(turnDaemonNodeOptions ? { NODE_OPTIONS: turnDaemonNodeOptions } : {}),
|
...(turnDaemonNodeOptions ? { NODE_OPTIONS: turnDaemonNodeOptions } : {}),
|
||||||
POSTGRES_POOL_MAX: managedPostgresPoolMax(baseEnv, 'TURN_DAEMON_POSTGRES_POOL_MAX', 2),
|
POSTGRES_POOL_MAX: managedPostgresPoolMax(baseEnv, 'TURN_DAEMON_POSTGRES_POOL_MAX', 2),
|
||||||
GAME_ENGINE_ROLE: 'turn-daemon',
|
GAME_ENGINE_ROLE: 'turn-daemon',
|
||||||
|
// 실행 worktree를 결정한 동일 프로필의 SHA를 전달한다. 부모 환경의 다른 버전은 상속하지 않는다.
|
||||||
|
TURN_BUILD_COMMIT_SHA: profile.buildCommitSha ?? '',
|
||||||
TURN_PROFILE: profile.profile,
|
TURN_PROFILE: profile.profile,
|
||||||
PROFILE: profile.profile,
|
PROFILE: profile.profile,
|
||||||
SCENARIO: profile.currentScenario ?? 'default',
|
SCENARIO: profile.currentScenario ?? 'default',
|
||||||
|
|||||||
@@ -184,6 +184,17 @@ describe('buildProcessDefinitions', () => {
|
|||||||
gatewayInternalApiUrl: 'http://127.0.0.1:13000',
|
gatewayInternalApiUrl: 'http://127.0.0.1:13000',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
it('does not inherit a different profile build SHA', () => {
|
||||||
|
const config = { ...processConfig, baseEnv: { TURN_BUILD_COMMIT_SHA: 'f'.repeat(40) } };
|
||||||
|
expect(buildProcessDefinitions(buildProfile(), config).daemon.env.TURN_BUILD_COMMIT_SHA).toBe(
|
||||||
|
buildProfile().buildCommitSha
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
buildProcessDefinitions({ ...buildProfile(), buildCommitSha: undefined }, config).daemon.env
|
||||||
|
.TURN_BUILD_COMMIT_SHA
|
||||||
|
).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
it('runs a built profile from its commit worktree', () => {
|
it('runs a built profile from its commit worktree', () => {
|
||||||
const buildWorkspace = '/srv/sammo/worktrees/0123456789abcdef';
|
const buildWorkspace = '/srv/sammo/worktrees/0123456789abcdef';
|
||||||
const definitions = buildProcessDefinitions(buildProfile(buildWorkspace), processConfig);
|
const definitions = buildProcessDefinitions(buildProfile(buildWorkspace), processConfig);
|
||||||
@@ -217,6 +228,7 @@ describe('buildProcessDefinitions', () => {
|
|||||||
expect(definitions.daemon.cwd).toBe(path.join(buildWorkspace, 'app', 'game-engine'));
|
expect(definitions.daemon.cwd).toBe(path.join(buildWorkspace, 'app', 'game-engine'));
|
||||||
expect(definitions.daemon.script).toBe(path.join(buildWorkspace, 'app', 'game-engine', 'dist', 'index.js'));
|
expect(definitions.daemon.script).toBe(path.join(buildWorkspace, 'app', 'game-engine', 'dist', 'index.js'));
|
||||||
expect(definitions.daemon.env.POSTGRES_POOL_MAX).toBe('2');
|
expect(definitions.daemon.env.POSTGRES_POOL_MAX).toBe('2');
|
||||||
|
expect(definitions.daemon.env.TURN_BUILD_COMMIT_SHA).toBe(buildProfile().buildCommitSha);
|
||||||
expect(definitions.auction).toMatchObject({
|
expect(definitions.auction).toMatchObject({
|
||||||
cwd: path.join(buildWorkspace, 'app', 'game-api'),
|
cwd: path.join(buildWorkspace, 'app', 'game-api'),
|
||||||
script: path.join(buildWorkspace, 'app', 'game-api', 'dist', 'index.js'),
|
script: path.join(buildWorkspace, 'app', 'game-api', 'dist', 'index.js'),
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ migration58은 기존 장수 인덱스를 `(server, general, year, month, tick,
|
|||||||
reservedTurnHandler가 기수 identity가 있는 새 AI 실행을 phase별로 모으고 실제 요청/선택/
|
reservedTurnHandler가 기수 identity가 있는 새 AI 실행을 phase별로 모으고 실제 요청/선택/
|
||||||
실행·성공/대체 결과를 함께 반환한다. 실행 tick은 해당 장수 기준, ID는 serverId·장수·tick·
|
실행·성공/대체 결과를 함께 반환한다. 실행 tick은 해당 장수 기준, ID는 serverId·장수·tick·
|
||||||
clock revision·phase로 결정한다. 정책 head 참조는 시작 시 확보하며 없는 값은 채우지 않는다.
|
clock revision·phase로 결정한다. 정책 head 참조는 시작 시 확보하며 없는 값은 채우지 않는다.
|
||||||
현재 codeVersion 주입과 유효 정책 합성 상세, 내부 후보 조건은 남아 있어 coverage는
|
Gateway의 프로필 buildCommitSha→daemon 환경 TURN_BUILD_COMMIT_SHA→CLI→runtime→handler로
|
||||||
`PROCEDURES`다. 수동 턴 중 AI를 사용하지 않은 경우 결정 행을 만들지 않는다.
|
실행 코드 버전을 전달한다. 전체40/64자리 SHA만 인정하며 누락/잘못된 값은 null이다.
|
||||||
|
handler 생성 시 한 번 정규화하므로 턴마다 Git/DB를 읽지 않는다. 유효 정책 합성 상세와
|
||||||
|
내부 후보 조건은 남아 있어 coverage는 `PROCEDURES`다. 수동 턴 중 AI를 사용하지 않은 경우 결정 행을 만들지 않는다.
|
||||||
|
|
||||||
GeneralTurnResult→world pending→capture/restore/peek/ack→기존 fenced DB flush를 연결했다.
|
GeneralTurnResult→world pending→capture/restore/peek/ack→기존 fenced DB flush를 연결했다.
|
||||||
요약 hash와 실행/phase unique로 같은 재시도는 중복 없이 통과하고 다른 payload는 실패한다.
|
요약 hash와 실행/phase unique로 같은 재시도는 중복 없이 통과하고 다른 payload는 실패한다.
|
||||||
@@ -63,7 +65,7 @@ GeneralAI와 예약 실행 handler에 선택적 `onDecisionTrace` 관측 경계
|
|||||||
초기 관측 단계에서는 default daemon을 켜지 않았다. 이후 아래 저장 경계에서 현재 기수의 새 실행을 수집하도록 연결했다.
|
초기 관측 단계에서는 default daemon을 켜지 않았다. 이후 아래 저장 경계에서 현재 기수의 새 실행을 수집하도록 연결했다.
|
||||||
이는 R5의 관측 기반일 뿐 완료가 아니다. 불변 결정 ID·기존 정책 참조·실행 결과·
|
이는 R5의 관측 기반일 뿐 완료가 아니다. 불변 결정 ID·기존 정책 참조·실행 결과·
|
||||||
pending/rollback·migration·정리·목록/상세 API와 GUI는 위 절에서 연결했다.
|
pending/rollback·migration·정리·목록/상세 API와 GUI는 위 절에서 연결했다.
|
||||||
후보/조건별 실제 관측값, 합성 유효 정책과 code version 연결은 남는다.
|
후보/조건별 실제 관측값, 합성 유효 정책의 실제 관측은 남는다. 코드 버전 전달은 위 저장 절에 연결했다.
|
||||||
|
|
||||||
### 전달 전 DB tick 정밀도 보완
|
### 전달 전 DB tick 정밀도 보완
|
||||||
|
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ Gateway의 새 진입/권한 catalog도 사용하려면 같은 commit의 Gateway
|
|||||||
- NPC/유저 자동턴의 개인·수뇌 절차, 정책 차단, RNG utility 결과와 최종 실행 결과는
|
- NPC/유저 자동턴의 개인·수뇌 절차, 정책 차단, RNG utility 결과와 최종 실행 결과는
|
||||||
migration 이후 새 실행부터 저장한다. 후보 내부 조건 전체는 아직 없으며 `PROCEDURES`
|
migration 이후 새 실행부터 저장한다. 후보 내부 조건 전체는 아직 없으며 `PROCEDURES`
|
||||||
coverage로 구분한다. 장수 상세의 **NPC 결정 기록 조회**에서 선택 월의 목록과 순서별 상세를 읽는다. 과거 결정은 역산하지 않는다.
|
coverage로 구분한다. 장수 상세의 **NPC 결정 기록 조회**에서 선택 월의 목록과 순서별 상세를 읽는다. 과거 결정은 역산하지 않는다.
|
||||||
- 결정은 당시 확보된 정책 참조를 보존하며 **당시 정책 참조**에서 해당 불변 버전을 바로 조회한다. 합성된 유효 정책 상세와 코드 버전 연결은
|
- 결정은 당시 확보된 정책 참조를 보존하며 **당시 정책 참조**에서 해당 불변 버전을 바로 조회한다. 합성된 유효 정책 상세는 아직 미완성이다. Gateway 관리 daemon은 프로필의 buildCommitSha를 새 실행에 기록한다. 수동 daemon은 실행 산출물의 전체 SHA를 TURN_BUILD_COMMIT_SHA로 전달할 수 있다. 미지정/잘못된 SHA의 실행과 기존 null 기록은 현재 버전으로 메우지 않는다.
|
||||||
아직 미완성이다. 코드 버전이 주입되지 않은 실행은 null로 남기며 현재 버전으로 메우지 않는다.
|
|
||||||
- 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진
|
- 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진
|
||||||
버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다.
|
버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다.
|
||||||
- 일부 국가 생성/소멸 사건은 actor/request가 null이다. 원인을 현재 주체로 추정하지 않는다.
|
- 일부 국가 생성/소멸 사건은 actor/request가 null이다. 원인을 현재 주체로 추정하지 않는다.
|
||||||
|
|||||||
Reference in New Issue
Block a user