중복 문서를 통합하고 완료된 구현 계획을 정리한다

This commit is contained in:
2026-09-29 09:08:28 +00:00
parent 08c3d0e936
commit cd6ded4970
9 changed files with 285 additions and 1295 deletions
-1
View File
@@ -40,7 +40,6 @@ export default defineConfig({
{ text: '독자별 읽기 순서', link: '/developer/' },
{ text: '기초 구조 안내', link: '/developer/first-steps' },
{ text: '경험자를 위한 시스템 읽기', link: '/developer/system-walkthrough' },
{ text: '구조 문서 찾아보기', link: '/developer/reference-map' },
{ text: '아키텍처 개요', link: '/architecture/overview' },
{ text: '런타임 아키텍처', link: '/architecture/runtime' },
{ text: '관리자 콘솔', link: '/admin-console' },
-716
View File
@@ -1,716 +0,0 @@
> 2026-09-26 정책 변경: 아래 단계별 검증 기록의 감사 실패 시 gameplay rollback은 당시 계약이다.
> 현재는 [기록·실패 계약](./play-audit.md#_6-1-공통-식별자와-내구성)에 따라 감사 savepoint만
> 취소하고 게임을 계속한다. 이력 누락·업데이트 경계의 단절을 허용하며 기존 원장은 수정하지 않는다.
# 플레이 감사 구현 기록과 수집 inventory
[확정 설계](play-audit.md)의 P1~P6 구현 기록이다. 현재 월별 상태·국가 시계열,
장수/도시 상세와 로그, 외교 이력, 정책 버전과 프로필별 관리자 진입을 사용할 수 있다.
NPC 결정 trace와 조사 A~F의 완성, 전체 종료 경계 및 COST gate는 남아 있다.
2026-09-16 사용자는 주간 한도 약20%를 남기고 사용 가능한 기능과 DB migration을
정리·통합·push하는 중간 전달을 요청했다. 전체 설계 완료와 이 전달을 구분한다.
실제 적용 방법과 제한은 [운영 안내](../play-audit-operations.md)를 따른다.
## 현재 구현
### NPC 저장 비용 probe
`app/game-engine/test/playAuditDecisionCost.integration.test.ts`는 별도
`PLAY_AUDIT_COST_DATABASE_URL`이 있을 때만 실행한다. schema는 정확히
`play_audit_cost_decision_fixture`여야 하며 그 schema의 결정/chunk를 비운 뒤 측정한다.
운영 URL을 사용하지 않는다. 정식 game migration을 먼저 적용하고 다음 명령을 실행한다.
```sh
pnpm --filter @sammo-ts/game-engine test playAuditDecisionCost.integration.test.ts --no-file-parallelism
```
201개 결정×302step을 실제 persistence로 저장해 200개 batch 경계를 넘긴다.
201header/603chunk/60,702step을 검사하고 JSON bytes, pg_column_size 합계,
테이블·TOAST 포함 할당과 index bytes, 저장 transaction 시간, warm 목록30회
p50/p95와 EXPLAIN ANALYZE BUFFERS를 `/tmp/play-audit-decision-cost.json`에 기록한다.
비밀·payload 본문·DB URL은 출력하지 않는다. 반복 실행에서는 이전 할당 공간이 남을 수 있다.
2026-09-16 격리 PG 결과는 JSON12,271,836B, header행101,304B/chunk행666,924B,
저장677.56ms, 목록51건 p50 0.76ms/p95 1.01ms였다. 월별 장수 index scan으로51행을
읽었다. 반복 RNG 합성 fixture는 압축률이 높으므로 평균 운영 trace 크기의 근거가 아니다.
추가 query event 계측에서는 동일201결정의 저장에 INSERT6회(헤더2/chunk4),
SELECT2회(hash 확인), COMMIT1회가 관측됐다. BEGIN 등 driver 내부 통신은 event에
나타나지 않으므로 네트워크 왕복 전체로 해석하지 않는다. test는 INSERT/SELECT 수를
assert하며 SQL/params 원문은 저장하지 않는다. 관측60,702단계마다 SQL을 쓰지 않고 batch당
저장·hash 조회를 확인한 범위다. WAL·retained heap, 실제 scenario 계측 전후와 전체
COST gate는 여전히 남는다.
### 정책·외교 사건의 요청 처리 상태
`playAudit.requestState`는 현재 기수의 정책 버전 또는 외교 사건 ID만 받는다.
참조된 requestId와 inputSequence가 모두 실제 input_event와 일치할 때 현재 상태,
처리 시도 횟수·접수/처리 tick·시계 버전·시각과 결과/오류 존재 여부를 반환한다.
기준 기록의 연결 없음, 불완전 참조, 삭제된 요청과 순번 불일치를 구분한다.
임의 requestId 검색, payload/result/error 원문·계정·lease owner 조회는 제공하지 않는다.
권한 검사 뒤 읽기 transaction 안에서 world, 사건 한 행, unique request_id 한 행을
최대3회 읽는다. SQL은 필요한 scalar와 존재 여부만 투영하며 쓰기·COUNT·전역 검색이 없다.
정책·외교 상세의 공통 component에서 버튼을 눌러 조회하며 재시도는 이 요청만 반복한다.
다른 버전으로 바꾸면 결과를 지우고 진행 중 응답을 무시한다. 자동 조회/polling은 없다.
R7 E의 연결 기반 일부이며 전체 실패·시도별 이력, 실제 mutation 횟수와 replay가 아니다.
F의 알려진 버그 조사 preset은 아직 구현하지 않았다. schema migration58은 그대로다.
### 당시 합성 정책 관측
DECISION_START.effectivePolicy(schemaVersion1)는 생성자에서 이미 합성된
General/Nation 정책 객체의 명시 필드를 복사한다. 서버→국가 설정, 사용자 자동턴 옵션,
NPC 상태와 기술력/병종/시나리오 기반 파생 기본값이 반영된 실제 priority/flags,
20개 국가 수치와 전투/지원/내정 부대 편성을 보존한다. 원문 meta 전체를 복사하지 않는다.
can()/조건/RNG를 재평가하지 않고 후보당 SQL도 추가하지 않는다. 결정당 상세에1회씩
기록하며 목록 summary에는 넣지 않는다. 기존 chunk/batch/transaction과 hash 계약을 따른다.
API는 허용 필드로 투영하고 UI는 기존 정책 한글 label을 재사용한다. details를 펼쳐도
추가 query가 없으며 이전 DECISION_START에 field가 없으면 미수집으로 표시한다.
현재 정책으로 과거를 채우지 않는다. runtime의 동적 지급 상한·개별 후보 수치·별도
자동화 권한의 판정 입력은 여전히 후속 관측이다. 초기 비용 probe의 반복 RNG fixture는
이 신규 정책 payload를 포함하지 않으므로 그 bytes를 완성된 결정의 평균으로 쓰지 않는다.
### NPC 결정 조회 API와 화면
`playAudit.decisionHistory/decisionDetail`은 같은 프로필 감사 권한·현재 기수 경계를 따른다.
장수별 조회는 선택 월과 phase, `(tick,id)` 내림차순 cursor를 사용한다. 월을 지정하지
않으면 현재 기수·장수·phase의 가장 최근 결정이 있는 월을 인덱스로 1건 찾는다.
명시한 과거 월은 비어 있어도 다른 월로 바꾸지 않는다. API는 실제 조회 `month`,
현재 게임 `currentMonth`, `selection`을 구분하며 더보기는 최초 조회 월에 고정한다.
화면에는 턴 실행 후 저장되는 시점, 최근 기록 월/현재 월, 이전·다음 월과 최근 결정
조회 버튼을 제공한다. 월말 표본과 달리 NPC 결정은 매 턴의 game flush에 저장된다.
목록은50건 기본/200건 상한으로 요약만 읽고, 상세는 명시 선택 시128 event chunk를
기본1개/최대4개 읽는다. header의 stepCount로 다음 chunk를 판단해 추가 본문이나 COUNT를
읽지 않는다. summary/step은 허용 필드만 투영하며 seed/raw metadata를 반환하지 않는다.
같은 tick의 개인·수뇌 결정 및 사망 장수의 현재 기수 기록도 조회한다.
migration58은 기존 장수 인덱스를 `(server, general, year, month, tick, id)`로 교체한다.
월 조건 밖의 기수 기록을 훑지 않으며 인덱스 개수는 늘리지 않는다. UI는 기존 장수 상세와
버튼·표 스타일을 재사용한다. 열기/상세 선택/더 보기는 명시적으로 수행하고 polling하지
않는다. 결정 URL 복원과 상세 재시도는 상위 장수 목록을 다시 읽지 않는다.
절차 coverage와 미수집 코드 버전을 표시하며 전체 후보 조건 관측은 남는다.
당시 정책 참조는 공용 `AuditPolicyVersion`으로 연결했다. 기존 정책 이력의 버전 표시·
필드 한국어 이름·실패 재시도를 재사용하며 클릭 시 버전1건만 읽는다. 결정의 참조 ID에
포함되지 않은 URL policy는 해당 결정의 정책으로 읽거나 표시하지 않는다.
국가의 저장 설정과 NPC별 합성 유효 값은 구분하며 현재 설정으로 보충하지 않는다.
### NPC 실행 단계와 대체 명령
선택 이후 `runAction`이 실제 수행한 인자/조건/재사용 대기/실행 문맥 검사를
`EXECUTION_ATTEMPT`로 상세 chunk에 기록한다. depth별 요청·해석·실행 명령과 결과,
대안 명령, 준비 term/total을 남긴다. 검사 함수를 다시 호출하지 않으며 실제로 건너뛴
검사는 추가하지 않는다. 준비·블럭으로 명령을 실행하지 않았으면 executedAction은 null이다.
원래 resolve 결과의 alternative를 따라간 순서와 공유 RNG를 그대로 유지한다.
저장 순번은 선택 관측과 실행 시도를 하나의 증가 순서로 매긴다. 기존 외부 AI observer의
원래 sequence는 변경하지 않는다. `DECISION_END`는 선택 종료이며 새 저장 기록은 실행
시도 뒤 종료한다. `executionCoverage=ATTEMPTS`로 기존 선택만 있는 기록과 구분하고,
이 flag가 있는데 실행 시도가 없으면 persistence가 거부한다. 과거 hash/행을 재작성하지 않는다.
요약의 대체 여부에는 이전 단계의 대안/휴식도 반영한다. 실제 게임 실행 결과 객체는 바꾸지 않는다.
API는 검사/결과의 허용 필드만 읽고 UI는 순서와 한국어 검사명을 표시한다. 목록에 상세를
추가하지 않는다. 체크 배열은 시도당 최대4개(블럭은1개), 기존 대안 depth 한도5를 유지한다.
전체 월간 부하 gate와 throw/rollback 실행의 별도 실패 원장, 내부 후보 조건은 후속이다.
### NPC 결정 저장·복구 기반
새 migration57은 `play_audit_decision` 요약과 `play_audit_decision_chunk` 상세를 분리한다.
reservedTurnHandler가 기수 identity가 있는 새 AI 실행을 phase별로 모으고 실제 요청/선택/
실행·성공/대체 결과를 함께 반환한다. 실행 tick은 해당 장수 기준, ID는 serverId·장수·tick·
clock revision·phase로 결정한다. 정책 head 참조는 시작 시 확보하며 없는 값은 채우지 않는다.
Gateway의 프로필 buildCommitSha→daemon 환경 TURN_BUILD_COMMIT_SHA→CLI→runtime→handler로
실행 코드 버전을 전달한다. 전체40/64자리 SHA만 인정하며 누락/잘못된 값은 null이다.
handler 생성 시 한 번 정규화하므로 턴마다 Git/DB를 읽지 않는다. 실행 직전 동적 정책 파생값과
내부 후보 조건은 남아 있어 coverage는 `PROCEDURES`다. 수동 턴 중 AI를 사용하지 않은 경우 결정 행을 만들지 않는다.
GeneralTurnResult→world pending→capture/restore/peek/ack→기존 fenced DB flush를 연결했다.
요약 hash와 실행/phase unique로 같은 재시도는 중복 없이 통과하고 다른 payload는 실패한다.
상세는128개 event씩 나누며 최대200행씩 batch insert한다. 목록을 위해 전체 trace를 하나의
header JSON으로 저장하지 않고 후보별 SQL도 추가하지 않는다. 실패하면 게임 상태와 모두
rollback되고 commit 뒤에만 pending prefix를 비운다. 실패한 실행의 별도 진단 원장은 남는다.
정리는 이전 기수 header를 잠그고200개 chunk씩 삭제한 후 header를 제거한다. FK RESTRICT로
무제한 cascade를 막으며 현재 기수는 기존 schema lock/identity 재검사로 보호한다.
빈57·기존56→57·재실행 no-op,302 event/3chunk 복원, 삽입 실패/재시도/충돌 및 실제
DB hooks와 정리 회귀를 검증했다. 전체 비용 gate는 후속이며 전용 API/GUI는 위 조회 절에 연결했다.
### NPC 판단 관측 기반
GeneralAI와 예약 실행 handler에 선택적 `onDecisionTrace` 관측 경계를 추가했다.
공유 AI의 수뇌→개인 결정 순서에 동일 sequence를 유지하며 시작/최종 선택/오류,
우선순위 절차 진입·결과, 정책/자동화/handler 부재 skip, 명령 후보 validation 결과와
실제로 호출한 RandUtil 결과를 관측한다. 예약 우선 반환 뒤의 절차는 만들어내지 않는다.
메서드를 재호출하지 않으며 RandUtil 내부 helper는 중복 사건으로 기록하지 않는다.
원문 seed/meta/debug 객체를 복제하지 않고 난수 결과의 객체는 ID만 투영한다. 투영할 수
없는 값은 `unprojected`로 명시하며 완전한 후보 상세라고 주장하지 않는다.
고정 seed3종에서16개 RNG utility 호출의 반환값·객체 identity·다음 RNG 결과가 같고,
실제 NPC 선전포고→개전→점령 fixture에서도 수집 on/off 회귀가 통과했다.
초기 관측 단계에서는 default daemon을 켜지 않았다. 이후 아래 저장 경계에서 현재 기수의 새 실행을 수집하도록 연결했다.
이는 R5의 관측 기반일 뿐 완료가 아니다. 불변 결정 ID·기존 정책 참조·실행 결과·
pending/rollback·migration·정리·목록/상세 API와 GUI는 위 절에서 연결했다.
후보/조건별 실제 관측값과 실행 직전 동적 파생값은 남는다. 코드 버전 전달은 위 저장 절에 연결했다.
### 전달 전 DB tick 정밀도 보완
월 표본과 정책의 기존 INTEGER tick은 1개월36,000,000 기준 약60개월에 넘친다.
`20260916060000_widen_play_audit_ticks`로 두 열을 BIGINT로 확장하고 writer의 안전한
정수 검증/변환, 월 표본 상세의 문자열 응답을 연결했다. pending payload와 hash 생성은
기존 number 의미를 유지해 이미 저장한 hash를 바꾸지 않는다. 릴리스 manifest도 이
migration head를 가리킨다. 실제 PG의55→56/빈56/no-op·기존 값/null/hash 보존과 큰 tick
저장 및 실제 HTTP의 월 표본/정책 상세 tick 문자열을 검증했다.
### 기본 조회 화면
장수 이름 부분 검색과 장수 번호 양방향 정렬을 현재/월말 모두 지원한다. 최대64자,
대소문자 구분, LIKE wildcard 문자 escape를 동일하게 적용한다. 입력 중에는 요청하지
않고 조회 버튼으로 URL에 적용하며, 더 보기와 새로고침도 같은 필터·정렬을 유지한다.
도시→모든 주둔 장수 연결에서는 이름 조건도 해제한다. 과거 검색은 sampleId로 먼저
좁힌 뒤 당시 JSON 이름을 검사하며 현재 이름을 참조하지 않는다. 역순은 ID `< cursor`로
페이지를 잇는다. 자원·능력별 정렬은 아래 2026-09-26 보완에서 복합 cursor로 추가했다.
실제 PG120개월×1,000명 fixture에서 선택 월 PK1000행, 국가 추가 시50행으로 후보를
좁혔다. 이 한 fixture의 실행계획은 전체 COST gate나 운영 p95 증거를 대신하지 않는다.
프로필 game frontend의 `/play-audit`는 장수가 없는 감사 계정도 직접 접근한다.
`capabilities`가 허용된 뒤 coverage와 국가 목록을 읽고 선택한 조회만 요청한다.
권한 거부 시 다른 감사 자료를 미리 가져오지 않는다. URL에 탭·국가·도시·표본 월·기간을
보존하며 도시의 주둔 장수 연결은 당시 월을 유지하고 국가 필터를 해제한다.
장수·도시 목록은 50개씩 명시적으로 더 읽는다. 느린 이전 응답은 후속 조회를 덮지 않는다.
국가 목록은 현재 또는 한 월의 이름/ID/color만 반환한다. 현재 목록은 해당 세 필드만
SELECT하며 과거 목록은 한 표본의 국가 JSON을 51행까지 읽고 allowlist projection한다.
기본 50·최대 200과 ID cursor를 사용하고 기수 전체의 국가를 DISTINCT 스캔하지 않는다.
멸망국은 해당 월 기준 목록으로 선택한다. 국가 시계열 API의 기본 범위는 최근 6개월, 화면의 기본 범위는 최근 12개월·월별이며
지표·집단 전환은 이미 받은 집계에서 계산해 추가 요청을 하지 않는다.
PanelCard, legacy-button, legacy-sort-select를 재사용한다. Chart.js로 국고 금·쌀,
총 병사수, 실제 수입, 집단별 5병종 평균 숙련도의 큰 그래프를 표시하고 수치 표를 함께 제공한다.
결측값은 선을 끊고 표시하며 최근 6/12/24개월 바로가기와 월/반기 조회를 제공한다. stock 마지막 표본 월, 월별 수집 여부·국가 존재·정산
완전성을 펼쳐볼 수 있고 null은 `자료 없음`이다. 국가 보유 금쌀/기술/세율,
수입·지급, 집단 인원·보유 총량/평균·5병종 평균 숙련 지표를 제공한다.
이 화면은 Core 신규 UX다. 최대 폭 1920px, 1200px 이상에서 탐색/분석/대상 상세의
3열 구조다. 701~1199px에서는 상세가 분석 아래에, 700px 이하는 한 열에 표시된다.
국가는 검색 가능한 목록에서 즉시 선택하며 장수·도시 클릭은 상세 영역에 focus를 옮긴다.
390px 모바일에서 문서 가로 넘침 없음,
넓은 표만 내부 수평 스크롤, 공통 14px 기본 typography와 명시적 focus/disabled가 계약이다.
월말/FINAL 장수·도시 projection과 도시 지도, 자원·능력별 정렬을 제공한다.
전투 통계는 후속 구현으로 남는다. 로그와 현재 예약 조회는 아래 구현을 따른다.
따라서 기본 화면 추가만으로 R1~R3/P2를 완료 처리하지 않는다.
Gateway 서버 관리의 프로필 카드에는 `admin.playAudit.read` capability의 해당 전체
profile scope가 있을 때만 진입 버튼을 표시한다. 기존 `auth.issueGameSession` 발급과
game session transfer를 사용하고 Gateway가 감사 데이터를 대신 읽지 않는다.
기존 로비의 URL 구성/세션 전달을 `utils/gameEntry.ts`로 추출해 공유한다.
새 감사 진입은 동일 origin의 sessionStorage 전달만 허용하며, 실패하면 현재 화면에
재시도 가능한 오류를 표시한다. 기존 로비의 query fallback은 동작 변경 없이 유지하되
새 감사 경로에는 적용하지 않는다. 서로 다른 origin의 관리자 진입은 지원하지 않는다.
`nationSnapshot`은 같은 권한/기수 범위에서 한 월말 또는 FINAL header와 그 국가의
복합 PK 행 하나만 읽는다. 최종 국가 화면은 이 API만 사용하며 월말 시계열을 동시에
요청하지 않는다. 국가 보유량·집단 통계와 해당 월 수집 시점까지 관측한 정산을 표시한다.
FINAL의 관측값을 월말/반기 합계에 추가하지 않는다. 표본 없음과 해당 국가 없음도 구분한다.
최종 수집 자체의 모든 게임 종료 경로 연결은 P1의 남은 lifecycle 검증을 따른다.
`generalDetail/cityDetail`은 선택 엔티티와 관련 국가·도시의 PK만 조회한다.
현재/과거 DTO를 분리하고 과거 이름·위치 이름도 같은 표본에서 읽는다. 현재 행으로
과거의 누락을 메우지 않는다. 목록에서 이름을 누르면 URL의 `general/cityRecord`로
상세를 연다. 상세 열기/닫기는 목록 조회 조건에서 제외하여 목록을 다시 읽지 않는다.
도시 상세의 주둔 연결도 적용된 월을 유지한다.
`generalTurns`는 현재 예약 전용 별도 조회이며 과거 시점을 입력받지 않는다.
상세의 버튼을 눌렀을 때만 기본50/최대200, `turnIdx` cursor로 읽는다. 정상30 slot을
넘은 잘못된 값도 숨기지 않는다. 인자는 읽기 전용 `argumentJson` 텍스트로 반환한다.
범용 JSON의 재귀 타입을 UI에 그대로 전달하지 않으면서 값은 생략하지 않는다.
예약 조회 실패는 장수 상세를 지우지 않는다. 과거 예약 변경은 이후 사건 원장이 담당하며
현재 큐에서 복원한 것처럼 표시하지 않는다. 전투 통계는 남는다. 지도·검색/정렬은 후속 보완으로 구현했다.
`app/game-engine/src/playAudit/snapshot.ts`는 기존 메모리 엔티티에서 명시적으로
허용한 장수·도시 필드와 국가별 자원·숙련 집계를 만든다. 입력 iterable을 각각
한 번 순회하며 국가마다 장수 목록을 다시 검색하지 않는다. 장수의 stats/role/items도
복사하여 이후 개명·이동·장비 변경으로 과거 표본이 변하지 않게 한다. 임의 meta,
triggerState, credential과 전체 world는 복사하지 않는다.
장수 분류는 human(`npcState < 2`), npc(`>= 2`, 5 제외), troopNpc(5)이다.
빈 집단은 합계 0, 평균 null이다. nation 0도 입력에 있으면 일반 국가와 별도로
집계한다. 장수의 국가와 도시 소유국을 일치시키지 않으므로 외국 주둔을 보존한다.
정산은 당월에 실제 관측한 `income/paid`만 별도 입력으로 받는다. 수집 완료 월에
정산이 없으면 0, 도입 월처럼 완전 수집을 증명하지 못한 기간은 null이다.
`prev_income_gold/rice`는 과거 정산 metadata이므로 집계하지 않는다. 정산 원장의
국가 전후값·적용 세율·보정액은 이후 원장 구현에서 보존해야 하며 이 projection만으로
R1을 완료했다고 판단하지 않는다.
### 실제 초기 달력의 조회 범위
감사 응답의 `startYear/startMonth`는 유효한 `world.meta.initYear/initMonth`를 우선한다.
동기화 개방은 `scenarioMeta.startYear`의 전년도에 시작할 수 있으므로 시나리오 규칙 연도를
조회 하한으로 고정하지 않는다. 두 metadata가 없거나 유효하지 않으면 시나리오 시작 연도와
현재 연도 중 이른 연도의 1월을 호환 fallback으로 사용한다. 이것은 최초 수집 증거가 아니며
자료 존재는 월 header로 별도 확인한다. 불변 기수 식별자 필터도 계속 적용한다.
월말/최종 상세, 장수 로그, 국가 시계열의 범위 검증과 기본 최근 6개월 기간은 같은 연월
하한을 사용한다. UI도 시작 연도의 최소 월과 현재 연도의 최대 월을 제한한다. 이미 읽던
world metadata로 계산하며 추가 DB 조회·쓰기나 시나리오/AI 규칙 변경은 없다.
PREOPEN은 wall-clock 대기 상태이며, 검증하는 것은 공식 개방 때의 논리 게임 달력이다.
## 외교 문서 상태 이벤트 저장 기반
`diplomacy.sendLetter/respondLetter/rollbackLetter/destroyLetter`의 기존 입력 원장
transaction에 제안·교체·승인·거절·회수·파기 요청·파기를 연결했다. 기존 SELECT와
UPDATE 반환값에서 변경 전후 allowlist를 만들고, 원장 잠금 SELECT의 sequence를
재사용한다. 감사 실패는 문서/알림과 함께 rollback하며 실패한 입력 원장은 남는다.
성공 재요청은 기존 결과를 반환해 이벤트를 중복 저장하지 않는다.
- migration55의 `play_audit_diplomacy_event`는 기수, 방향 있는 국가쌍, 실행/로컬 순번,
DB sequence, 처리 당시 달력/tick/revision, actor와 작은 상태 전후 값을 저장한다.
DB sequence와 입력 접수 sequence는 다른 개념이며 숫자 간격은 허용한다.
- 본문은 기존 `diplomacy_letter`를 ID/hash로 참조한다. 작성 본문·작성자·작성 시각·
국가쌍·prevId의 UPDATE를 DB trigger로 거부하고, 기존 새 문서 작성 경로를 유지한다.
상태·서명·aux 갱신은 허용한다. 기존 문서의 과거 상태를 소급 생성하지 않는다.
- 문서 쓰기 요청당 작은 world/clock SELECT 1회, RUNNING realtime이면 readiness
SELECT 1회, 이벤트 200개당 bulk INSERT와 ID/hash 확인 SELECT 각 1회가 추가된다.
원문 SELECT와 본문 복사는 추가하지 않는다. 기존 알림 wall time 조회와 결합해
줄일 여지는 남으며, SQL/WAL 비용 gate의 실측 완료를 뜻하지 않는다.
- reset 뒤 이전 이벤트는 기존 retention 경로에서 ID 최대200개씩 정리한다.
과거 tick/revision은 시계 이동 대상이 아닌 KEEP 이력으로 등록한다.
- 기존 `rollbackLetter`는 회수다. 현재 복구 mutation은 없어 복구 이력을 꾸며내지 않는다.
실제 PostgreSQL에서 문서 8개 시나리오, unit 7건, retention 4건, 신규55개/
증분54→55/재실행 migration을 검증했다. 외교 상태의 API 즉시 응답·엔진 월간/턴
변경, 도입 당시 기준, 감사 조회 API/UI는 후속 구현이며 **R4 전체 완료가 아니다**.
일반 외교 알림은 WALL_TIME이고 제의 처리와 tombstone은 구분한다. 오래된 알림
테스트의 게임 tick 가정을 현행 envelope 계약에 맞췄다.
### 즉시 외교 응답의 관계 전이
`messages.respond`의 별도 `executeInputEvent` 경로에도 인증된 입력 context를 전달한다.
불가침 체결·불가침 파기·종전 수락에서 이미 잠근 양방향 관계 행을 before로 사용하고,
각 diplomacy UPDATE의 반환값을 after로 기록한다. state/term/dead/isDead/isShowing만
투영하며 임의 meta를 복사하지 않는다. 같은 값의 재적용은 상태 전이에 포함하지 않는다.
추가 상태/clock/입력 SELECT는 없고, 두 방향의 전이를 한 bulk INSERT와 ID/hash
확인 SELECT로 저장한다. 원장·관계·로그·알림과 같은 transaction이다. API commit 뒤
엔진 메모리 동기화가 실패해도 재요청은 원장 결과를 재사용해 감사 이력을 중복 쓰지
않는다. 엔진 동기화에서 같은 사건을 다시 수집하지 않는다.
실제 PG에서 세 응답의 양방향 before/after, 처리 순서, RESOLVED 제의 상태와 동기화
실패 후 재요청을 검증했다. 엔진 transport만 fixture 응답이므로 엔진 runtime 동기화
완료의 증거는 아니다. 거절/실패/무변경은 관계 전이와 구분할 시도 원장 구현에 남겼다.
엔진 턴 변화와 기준 수집, 외교 조회 화면은 아직 남았다.
### 엔진 월간 외교 전이
`createMonthlyDiplomacyHandler`가 이미 가진 before/after에서 state/term/dead의 실제
변화만 directed event로 모은다. 사건 순서는 국가 ID 쌍으로 고정하고 실행 identity는
기수/달력/clock revision을 사용한다. 자연 월간 실행에 actor나 입력 원장 ID를 만들지
않는다. 기본 TRADE matrix 보충 자체와 무변경 행은 기록하지 않는다.
pending queue는 world capture/restore/peek/ack에 포함하고, 기존 fenced DB transaction
안에서 bulk200 INSERT와 hash 확인을 수행한다. 실패 시 상태와 이력은 함께 rollback,
queue는 재시도까지 유지하며 commit 후에만 제거한다. 기존 계산/RNG/로그 순서는
그대로고 추가 상태 SELECT는 없다. 기존 before 목록을 정렬한 메모리 사본만 추가한다.
실제 PG에서 개전·기간 감소·사상자 처리·불가침 만료·종전의 기존 결과/로그를 유지하며,
메모리 checkpoint 복구, 감사 INSERT 실패 rollback, 재시도/중복 방지를 검증했다.
엔진 개별 명령 전이와 초기 외교 기준, 조회 API/UI 및 전체 비용 실측은 남았다.
### 예약 턴 명령의 외교 전이
`createReservedTurnHandler`는 각 실제 action의 실행 순번과 실행 전 장수 identity,
국가/개인 명령 구분, actionKey를 diplomacy patch에 운반한다. world가 patch를 실제
적용하는 순서대로 직전/직후 state/term/dead를 queue하므로 같은 턴의 중간 전이를
최종값으로 덮어쓰지 않는다. API 응답 동기화의 직접 world patch는 다시 기록하지 않는다.
실행 ID는 장수/실행 전 scheduled tick/clock revision이며 기수 ID와 함께 unique하다.
ordinal은 해당 턴의 patch 순서이고 무변경을 생략하면 간격이 생길 수 있다. 입력 접수
sequence를 예약 턴의 실행 ID로 오인하지 않는다. 메모리 Map을 직접 읽어 before
관측 때문에 기본 관계 생성 순서가 달라지지 않도록 했다. 상태 SELECT/RNG 호출은 없다.
연속 두 전이와 checkpoint 복구 fixture, 실제 NPC 선전포고의 양방향 사건,
감사 on/off에서 기존 개전·점령 진행을 검증했다. 완전한 RNG trace 동일성, 모든 명령의
실제 DB 재로드와 당시 NPC 정책/결정 trace 연결은 후속 gate다. 즉시 명령 executor와
특수 상태 변경을 포함한 최종 mutation inventory 및 초기 기준/조회 화면도 남았다.
### 외교 이력 조회 API
프로필 game-api의 `diplomacyHistory/diplomacyEvent`는 공통 감사 권한과 read-only
transaction을 재사용한다. 목록은 서로 다른 국가 두 개와 현재 기수 안의 기간을
필수로 받아 sequence 역순 기본50/최대200개만 반환한다. 국가쌍의 입력 순서는 무관하지만
각 사건의 방향은 유지한다. sequence/cursor/tick은 정밀도를 잃지 않는 문자열이다.
목록에는 본문·전후 값·요청 원장 정보를 싣지 않는다. 상세는 해당 기수 event 1개와
필요한 문서 1개만 읽고 원문 hash를 검증한다. AVAILABLE/NOT_APPLICABLE/
MISSING_REFERENCE/HASH_MISMATCH를 구분하고 원문 부재·불일치에서는 본문을 반환하지
않는다. 기록의 before/after를 보여 주며 현재 문서 상태로 덮어쓰지 않는다. actor와
상태는 allowlist로 투영하여 계정 ID·debug 등 내부 값을 제외한다.
coverage는 RECORDED_EVENTS_ONLY이며 최초 수집 전의 상태 완전성을 주장하지 않는다.
국가쌍/sequence index를 사용 가능한 형태지만 실제 큰 기수의 기간별 scan/EXPLAIN 비용
검증은 후속 gate다. 실제 HTTP+PG/Redis에서 권한·sanction·잘못된 범위/cursor,
본문 지연 조회/hash 상태·기수 전환의 접근 차단과 input_event 무증가를 검증했다.
외교 화면과 초기 기준 수집은 아직 남아 있다.
### 외교 감사 화면
CHE/HWE의 기존 `PlayAuditView` 필터·PanelCard에 외교 이력을 추가했다. 국가/상대 국가,
기간과 선택 event를 URL에 보존하고 기존 정책 이력과 같은 표·상세·pagination 흐름을
사용한다. 상세 선택·닫기·오류 재시도는 국가/이력 목록을 다시 읽지 않는다. 필터 편집은
조회 적용 전 요청하지 않는다. 본문은 선택한 상세 응답에서만 읽는다.
원문 hash를 검증한 뒤 기존 `purifyDiplomacyHtml`로 만든 briefHtml/detailHtml만 렌더링한다.
원문 HTML은 별도 details 안에서 escaped text로 보여준다. 이 때문에 선택 문서의
응답에는 원문/정제본이 함께 오지만 추가 DB 조회나 중복 저장은 없다. 해시 불일치와
참조 부재에는 본문을 표시하지 않는다. 이전 문서 번호와 당시 actor/상태/실행 연결을
보이고, 무소속은 외교 국가 선택에서 제외한다.
실제 HTTP에서 script 포함 원문의 보존/표시용 정제를 확인하고, production bundle
Chromium의 CHE/HWE에서 desktop1280×720/mobile390×844, DPR1로 검증했다.
선택적 상세/재시도·deep link/reload·pagination·draft 적용과 표 내부 가로 스크롤,
원문 script 비실행을 검사한다. 외교 최초 기준과 최종 mutation inventory/전체 비용
검증은 남아 있으며 화면 추가만으로 R4 전체 완료를 판단하지 않는다.
### 외교 관계 최초 관측
`initializeAuditDiplomacy`는 복구된 clock과 이미 로드한 관계에서 실제 국가쌍의
방향별 state/term/dead를 한 번만 기록한다. 재야(0)는 제외하며 당시의 관측 값만
보존한다. 이전 상태·원인·actor는 null이고 과거 체결 시점을 추정하지 않는다.
`playAuditDiplomacy` 기수 표식과 사건을 readiness 전 기존 fenced flush에서 함께
저장한다. 관계가 없는 경우에도 표식은 flush하고, 정상 재시작은 다시 쓰지 않는다.
checkpoint rollback은 표식과 pending 사건을 함께 복원한다.
추가 DB 전체 SELECT는 없다. 초기 관계 목록의 짧은 필드만 기존 200행 batch writer로
저장하며 최초 저장량은 방향별 관계 수에 비례한다. 정상 재시작은 메모리 표식 검사만
수행한다. 기본 교역 관계도 최초에는 보존해 국가쌍 조회의 시작 값을 제공하지만,
월간 기본 matrix 생성은 계속 전이로 기록하지 않는다. 대규모 국가 수의 payload와
WAL 실측, 이후 신생국·소멸국 및 기존 문서의 도입 기준은 별도 검증이 남아 있다.
단위 검증은 재야 제외·빈 관계·잘못된 관측 시각·재호출·checkpoint 복원을 다룬다.
실제 격리 PostgreSQL startup 검증은 실패 rollback, PREOPEN 저장, 재시작의 동일 행과
표식 및 readiness 경계를 확인한다. UI는 이를 '관계 최초 관측'으로 구분한다.
### 기존 외교 문서 도입 기준
`databaseHooks.flushInitialAudit`는 문서 도입 표식이 없는 기수에만 seed와 같은 schema lock → lease → CLOCK →
GENERAL_ACCESS 순서의 transaction을 열고 clock authority와 기수 identity를 확인한다.
`documentBaseline.ts`가 문서를 id cursor 200건씩 읽어 기존 불변 원문 참조/hash와
관측 당시 상태·서명·이전 문서 번호를 저장한다. 원문 자체를 사건에 복제하지 않는다.
현재 유효 문서뿐 아니라 보유 중인 교체·종료 문서도 보존하며 과거 승낙 시점이나
actor는 만들지 않는다. 기존 API가 쓰던 상태 projection을 infra로 옮겨 함께 사용한다.
문서 기준 사건·world 표식과 대기 중인 초기 상태/정책/관계가 같은 fenced transaction에서
확정되고 commit 뒤에만 ack한다. 실패는 memory checkpoint를 복원한다. 문서가 0건이어도
표식을 저장하고 일반 재시작에서는 문서 SELECT를 수행하지 않는다. 본문 mutation API와
같은 CLOCK lock 아래서 읽으며, 조회 준비가 되기 전에 종료한다.
비용 재검토: 최초에 작은 identity SELECT 1회, 문서 SELECT `floor(L/200)+1`회와 기존
writer의 batch INSERT/hash 확인 SELECT를 사용한다. 원문은 해시 계산에 필요한 한 batch만
유지하며 매 턴 다시 읽지 않는다. 초기 실패 복원을 위한 world memory checkpoint 1개의
비용과 대규모 도입 transaction 시간·WAL은 전체 COST gate에서 측정해야 한다.
실제 PG fixture는 201건/2 batch와 INITIAL 저장 실패의 전체 rollback, 원문 hash와 상태,
재시작의 동일 event/표식을 확인한다. 화면에서는 '문서 최초 관측'으로 구분한다.
### 국가 생성·멸망 관계
국가 생성은 `addNation`(월간 NPC/이민족/즉시 명령)과 예약 턴의 직접 created.nations
반영 경로를 모두 수집한다. 기본 matrix를 만든 뒤 신생국과 연결된 방향별 관계만 가져오고,
한 번에 여러 국가를 생성해도 같은 관계를 중복 기록하지 않는다. 후자의 누락된 정책
기준 4영역 초기화도 같은 지점에 연결했다. 생성 시 조회 범위는 대상 국가 수×기존 국가 수며
전체 관계 matrix를 감사용으로 복제하지 않는다.
멸망은 후계자 없음/전투/월간 방랑 처리의 공통 removeNation에서 기존 삭제 순회가
제거하는 관계를 재사용한다. 생성은 before null, 제거는 after null이며 default 교역
생성을 월간 외교 상태 전이로 바꾸지 않는다. 기존 world 감사 순번은 `nation-relations:N`
실행 identity에만 쓰고 사건 안의 ordinal은 방향별 정렬 순서다. checkpoint 복원과 기존
fenced transaction/pending acknowledgement를 그대로 따른다. 원인 actor/request 연결은
관측되지 않으면 null이며 향후 조사 C/E context 연결 대상으로 남긴다. 예약 턴에서 생성·멸망한 관계는 전역 clock 대신 해당 장수의 실행 tick을 전달한다.
단위 검증은 같은 ID 생성·삭제·재생성, 중복 add/remove, rollback, 예약 턴 복수 신생국의
정책 기준/관계 중복 방지를 확인한다. 실제 PG는 제거 저장 실패의 nation/관계 rollback,
재시도, 삭제 뒤 재생성, 정책 4개 및 이력 중복 방지를 검증한다. CHE/HWE에서 생성·종료의
한쪽 상태 부재를 표시한다. 상대국이 없는 단독 신생국은 관계 사건이 없으며 독립 국가
생멸 원장은 향후 조사 C에 속한다.
## NPC·국방 정책 버전 저장 기반
`PlayAuditPolicy`는 현재 기수/국가/영역별 불변 revision과 이전 버전 ID를 보존한다.
영역은 국가 NPC 값, 국가 NPC 우선순위, 장수 NPC 우선순위, 국방(`war/scout/secretlimit`)이다.
공지·권유문·세율·지급률 등 나머지 국가 설정의 사건 기록은 자원/행위 원장 연결에서 남아 있다.
NPC 설정은 기존 allowlist의 저장 값만 복사하며 setter/time이나 임의 nation metadata를
복사하지 않는다. 누락(null)은 설정 상속이다. 개인별·server별 보정이 적용된 최종 AI 값인
것처럼 표시하지 않으며, 해당 관측값과 코드 버전은 이후 NPC trace가 담당한다.
daemon world 구성과 신규 `addNation`에서 네 기준 버전을 pending에 담는다. 기존 정상
포인터가 있으면 재시작 때 재생성하지 않는다. 포인터 ID는 기수·국가·영역·revision으로
검증하므로 다른 국가 metadata를 복사해도 기존 국가 이력을 이어받지 않는다.
DB runtime의 기준 수집은 clock recovery/synchronization 뒤 수행하고 readiness 전에
startup flush로 내구화한다. PREOPEN에서 명령이 없어도 정책 기준을 저장한다.
기존 NPC mutation의 검증·CAS와 국가 설정의 권한·횟수 제한을 통과한 뒤 변경 전후를 비교한다.
setter/time 변경과 동일 설정 저장에는 새 적용 버전을 만들지 않는다. 기존 CAS token 갱신,
write와 성공 응답 계약은 유지한다. 거부된 입력은 정책 적용 이력에 넣지 않으며 기존
input_event의 ok:false와 구분한다. OBSERVED_GAP은 저장된 포인터와 현재 설정 불일치를
발견했을 때의 기준 재관측이다. 실제 변경 전 값/actor/시각을 추정하지 않는다.
pending 정책·최신 포인터·전역 감사 ordinal은 world capture/restore와 acknowledgement에
포함한다. 정책 row, nation meta, world meta와 input_event 완료는 같은 transaction이다.
기존 input_event fence SELECT에 sequence/actor_user_id만 추가하여 추가 요청 조회를 피한다.
수행자 ID가 명령과 일치하지 않거나 요청이 있는데 input context가 없으면 저장을 거부한다.
actor는 당시 user/general/name/nation/officer/npc/permission만 보존하며 createdAt은 DB wall time이다.
향후 수뇌 DTO는 nation 소유권과 기존 resolver를 적용하고 userId/inputSequence/requestId 및
관리자 진단을 제외해야 한다. 수뇌 직책/가입 전 열람 정책·화면은 확정 설계대로 후속 범위다.
ID와 payload hash로 재시도를 검증한다.200행마다 createMany와 ID/hash 확인 SELECT를 사용하며
본문 전체를 재조회하지 않는다. 최초 기준은 국가당4행, 적용 변경은 해당 영역1행이다.
세계 전체 정책이나 장수별 정책을 매 턴 복사하지 않는다. 기존 NPC 기본값은 별도 pure module로
옮겨 mutation과 audit allowlist가 공유하고 기존 export 경로는 유지한다.
정책 schema version은1이며 migration은 기존 이력을 backfill하지 않는다. 이미 적용한 migration
checksum은 수정하지 않고 schema_version 필드는 별도 증분 migration으로 추가했다.
정리 worker는 이전 기수 정책 ID도 최대200개씩 삭제한다. policy version의 self-reference는
삭제 FK로 강제하지 않아 과거 비공개 기수를 key batch로 정리할 수 있다. 현재 기수 포인터는
같은 transaction으로 저장하고 조회 시 항상 현재 기수 범위를 검사해야 한다.
NPC 결정의 정책 참조와 거부/무변경 시도의 사건 연결은 아직 남았다.
`policyHistory`는 국가/영역/기간을 필수로 받고 기본50·최대200개의 버전 요약을
revision 내림차순 cursor로 반환한다. 목록에서는 전후 정책 본문과 요청 자료를 읽지 않는다.
`policyVersion`은 선택 ID 한 건의 현재 기수 범위를 검사하고 전후 설정·당시 직책·입력
sequence를 반환한다. BigInt는 문자열로 보존하며 목록/상세 모두 계정 ID를 노출하지 않는다.
world identity와 자료는 기존 RepeatableRead/timeout 계약을 공유한다. 별도 count/쓰기나
현재 정책을 읽어 과거를 채우는 조회는 없다. 기존 국가/영역/revision 및 기간 index를
재사용한다. 반환 상한은 DB scan 상한의 증명이 아니며 실행계획/부하 측정은 P6에 남는다.
`projectPolicyConfiguration`은 수뇌 공개에 재사용할 전후 값·당시 소속/직책만 남기고
계정·요청·tick/ordinal·관리자 진단을 제외한다. 현행 endpoint는 관리자 전용이다.
향후 자국 API에서 기존 국가 resolver의 인가를 별도 적용해야 하며 이 projection 자체가
인가를 대신하지 않는다. 수뇌용 route나 직책 권한을 이번 변경에서 새로 열지 않았다.
프로필 `/play-audit`의 정책 화면은 적용한 국가/영역/기간만 읽고, 상세 열기/이전 버전
이동/실패 재시도가 목록과 국가 목록을 다시 읽지 않도록 한다. URL로 선택 버전을 보존한다.
입력 중인 필터는 조회 버튼을 누르기 전 SQL 요청을 발생시키지 않는다. 최초 관측,
실제 변경, 관측 누락 이후 기준과 자료 없음의 의미를 구분한다. 기존 PanelCard와 제어
스타일을 사용하고 NPC 설정 화면의 한국어 필드 이름을 따른다. 값은 텍스트로 출력한다.
## 초기 도입 기준과 즉시 내구성
`initializeAuditCollection`은 기수별 첫 관측에서만 INITIAL 표본과 world의
`playAuditCollection` 시작 좌표/실제 관측 시각을 pending에 담는다. INITIAL은 기존
국가·도시·장수 projection과 batch 저장을 재사용하며 월말과 FINAL을 대체하지 않는다.
새 migration은 INITIAL kind를 허용하고 부분 unique index로 기수당 한 행만 허용한다.
재시작은 저장된 marker를 사용하며 초기 상태를 현재 값으로 갱신하지 않는다.
runtime은 기존 lease 획득·world load·clock 복구/동기화 후 정책/상태 기준을 수집한다.
기존 fenced persistence로 기준과 국가 포인터/world marker를 함께 commit한 뒤에만
clockReady를 공개한다. 별도 input_event를 만들지 않는다. 초기 저장 실패는 기존 startup
오류 경로로 lease/연결을 정리하고 준비 완료를 알리지 않는다. 정상 재시작은 pending이
없으면 추가 flush가 없다. pending 확인은 배열 길이만 검사하여 큰 snapshot을 복사하지 않는다.
초기 수집은 이미 로드된 world를 각 한 번 순회한다. 추가 전체 엔티티 SELECT 없이 기존
header/child batch transaction을 한 번 수행한다. readiness 전에 저장하므로 큰 기수의
startup latency/WAL/heap 비용은 P6에서 함께 측정해야 한다. 값이나 표본을 생략하는
방식으로 비용을 줄이지 않는다. 초기 read-model receipt는 첫 실제 명령에 섞지 않는다.
조회 API의 표본 kind와 coverage cursor에 INITIAL을 포함한다. 현재 world meta에서
`collectionStart`만 allowlist projection하여 추가 DB 조회 없이 시작 시점을 표시한다.
국가·장수·도시의 수집 시작 기준 조회는 같은 snapshot identity를 유지하며 국가 시계열에는
MONTH_END만 포함한다. INITIAL의 흐름을 정규 월/반기 합계에 더하지 않는다.
시나리오 개방 달력과 실제 상세 수집 시작은 서로 다른 값이다.
## 이전 기수 월별 표본 정리
새 daemon runtime은 실제 `serverId`를 고정해 이전 월별 감사 표본 정리를 시작한다.
기수 변경 직후 조회 차단은 기존 API identity 필터가 담당한다. 정리는 gameplay flush와
별도 transaction이며, seeder와 같은 schema advisory lock을 try-lock한 뒤 DB의 현재
identity를 재확인한다. 이전 runtime/누락 identity는 정리하지 않는다. 부모 표본의 row lock으로
child 추가와 빈 부모 삭제의 경쟁을 막고, 장수→도시→국가 child의 PK만 최대200개 읽어
그 행을 삭제한다. 모두 빈 뒤 header1개를 삭제하여 대량 cascade를 피한다.
batch는 SQL statement2초/transaction5초 상한이고 성공 후1초, lock경합/오류 후30초에
재시도한다. 동시에 두 batch를 수행하지 않는다. 기수가 바뀌거나 이전 자료가 없으면
worker가 끝나므로 평상시 idle polling은 없다. 프로세스 재기동은 DB의 남은 key부터 다시
시작한다. 종료는 진행 중 transaction을 기다린 뒤 connector를 닫는다. 원문 DB 오류 대신
고정 경고를 운영 로그에 남기며 정리 실패로 gameplay 결과를 실패 처리하지 않는다.
현재 정리 대상은 구현된 PlayAuditMonth/General/City/Nation과 PlayAuditPolicy다. 기존 연감/계정 원장과
LogEntry 보존 정책은 바꾸지 않는다. 외교·정책·trace 테이블을 추가할 때 같은 수명주기와
key 단위 삭제를 연결해야 한다. Gateway RESET의 기존 process중지→seed commit→재기동
경로에서 시작한다. 예약 상태에서는 runtime이 없을 수 있어 seed commit 직후에도 batch
하나를 시도한다. 실패한 seed에는 정리가 실행되지 않고, 정리 실패는 seed 결과의 warning으로
남긴 뒤 이후 runtime이 재시도한다. RESERVED에서 남은 물리 정리는 PREOPEN 기동까지
연기되지만 새 identity로의 조회 차단은 즉시 적용된다. 취소 CANCELLED는 기존 정책상 API도
중지되므로 관리자 감사 접근과 최종 표본 수집의 별도 lifecycle 보완은 아직 남는다.
검증 fixture는 `PLAY_AUDIT_RETENTION_DATABASE_URL`의 `_retention_fixture` 전용 schema를
요구한다. schema에 정식 migration을 적용한 뒤 `playAuditRetention.integration.test.ts`를
실행한다. 삭제·trigger rollback fixture이므로 다른 통합 suite의 DB를 공유하지 않는다.
conditional registry는 external_fixture로 분류하며 일반 core DB URL에 자동 연결하지 않는다.
실제 DB에서401장수/201도시/1국가를 여러 batch로 정리하고 현재 기수를 보존했다.
추가 real daemon startup 검증은 기존 selectPool integration에 연결했다.
## 기존 장수 로그의 기수별 조회
`generalLogs`는 기존 `LogEntry`에서 현재 기수와 장수·기록 종류를 제한하고
ID 역순 cursor로 기본 50/최대 200행을 조회한다. 네 종류는 열전/개인 행동/
전투 결과/전투 상세다. 현재 장수가 사망했어도 보존된 로그를 조회할 수 있다.
과거 표본에서 열면 해당 게임 월 전체 기록을 조회하며 표본 순간까지의 기록인 것처럼
표시하지 않는다. `createdAt`은 게임 논리 시각일 수 있어 설치 wall time으로 필터하지 않는다.
별도 로그 복제 테이블 대신 nullable `LogEntry.serverId`를 추가했다. 엔진 공통 로그,
천통 내기 결과, 장수 선택/재선택, 외교 메시지 응답의 기존 저장 transaction에서
world meta의 실제 기수 ID를 함께 쓴다. 엔진과 장수 선택은 메모리에 있는 값을 사용한다.
외교 응답은 기존 world SELECT에 meta 필드를 추가한다(조회 횟수는 동일하지만 읽는 bytes는 증가).
별도 world SELECT나 로그 INSERT는 없다.
공백/누락 identity를 profile명으로 대신하지 않는다. 기존 및 레거시 이관 로그는
기수 귀속을 증명하지 못하므로 null 그대로 두며 API에서 제외하고 화면에 자료 범위를 표시한다.
설치 시 전체 backfill이나 로그 재복사는 수행하지 않는다.
현재 `(generalId, category, id)` index를 재사용한다. serverId/월은 잔여 조건이므로
오랜 기수의 희소한 월 조회에는 더 많은 index 행을 검사할 수 있다. 실행 5초 상한은
응답 실패를 자료 없음으로 숨기지 않는다. 큰 fixture의 EXPLAIN/지연 측정 후 복합 index의
추가 쓰기 비용과 비교하는 P6 gate는 남으며, 행 반환 상한을 스캔량 상한으로 보고하지 않는다.
`GeneralRecordPanels`에 선택 종류와 독립 오류/재시도 표시를 추가해 기존 표시를 재사용한다.
상세에서 버튼을 눌러야 로그를 읽고 종류별로 받은 페이지를 재사용한다. 엔티티/월이
바뀌면 캐시를 비우고 늦은 응답을 버린다. 동등한 월 객체가 재생성되어도 장수/도시 상세를
재조회하지 않도록 감시 대상을 ID·연·월·종류의 원시값으로 제한한다. 로그 원문은
기존 `formatLog` 허용 목록을 거쳐 렌더링한다.
## 월별 저장 구현
`playAudit/collection.ts`가 `beforeMonthChanged`에 이전 월 표본을 queue한다.
`incomeHandler`에서 이미 계산한 수입·급여를 관측하여 작은 국가별 월합계를
world meta에 함께 저장한다. 월중 재시작에도 집계가 유지되며, 수집 도입 월은
불완전으로 표시하고 다음 월부터 완전 수집한다. 기수 identity가 없는 설치에서는
profile명으로 대체하지 않는다. 해당 구간의 API coverage 안내는 아직 구현해야 한다.
`InMemoryTurnWorld`의 capture/restore/peek/acknowledge에 pending 표본을 포함하고
`databaseHooks.persistChanges`에서 gameplay와 같은 transaction으로 저장한다.
`unificationHandler`는 월말과 구분되는 FINAL 표본을 queue한다.
`PlayAuditMonth/Nation/City/General` 네 테이블에 명시적 projection을 보존한다.
국가·도시별 장수 검색 index를 두고 child insert를 200행씩 분할한다. 표본 ID와
payload hash가 같은 재시도는 중복 저장하지 않고, 내용이 다르면 transaction을 실패시킨다.
200은 초기 batch 설정이며 payload bytes/WAL/heap 실측을 통한 최종 선정은 남아 있다.
새 migration은 기존 행을 backfill하지 않는다. 전용 PostgreSQL에서 빈 설치 전체 적용,
기존 49 migration 이후 새 migration 증분 적용, 두 번째 deploy no-op을 검증했다.
업무 데이터와 감사 표본의 transaction rollback, 재시도/충돌, 월 경계의 전월 세율과
world meta reload도 확인했다. 이전 기수 차단/정리, 최종 표본 전체 종료 경로,
정산 전후값의 별도 사건 원장은 아직 남아 있다.
## 프로필 조회·권한 구현
`game-api/router/playAudit`에 `capabilities`, `coverage`, `generals`, `cities`를
추가했다. Gateway는 capability catalog만 제공하며 게임 자료를 대신 조회하지 않는다.
`admin.playAudit.read:<profileName>`는 `che:default`처럼 scenario까지 정확히 비교한다.
기존 resolver와 같은 명시적 전체 grant와 `superuser/admin.superuser`를 허용하고
일반 `admin`, Gateway 조치 감사 권한, 인게임 직책으로 접근을 추론하지 않는다.
공통 계정 권한 판정은 추가 `admin.playAudit.accounts`를 요구한다. 계정 조사 자체는 아직 없다.
정상 bootstrap 첫 계정은 기존 발급 경로의 명시적 `superuser` role을 사용한다.
역할 없는 레거시 첫 계정에 대한 Gateway의 DB 기반 관리자 fallback은 게임 token에
전달되지 않는다. 감사 API는 기존 게임 token의 명시적 role만 신뢰하며 Gateway의
첫 계정 판정을 game DB에서 재현하지 않는다. 해당 계정은 기존 Gateway 권한 관리로
감사 role을 부여할 수 있다. 이 경계는 일반 `admin`의 권한 확대로 해결하지 않는다.
모든 조회는 인증·기존 제재·token profile 검사 후 실행한다. 장수 보유, 접속 가중치,
input_event 쓰기를 요구하지 않는다. 현재 세계 identity와 자료를 같은 RepeatableRead
transaction으로 읽으며 대기는 2초, 실행은 5초로 제한한다. 응답에 `asOf`, tick과
현재 기수 identity를 포함하고 오래된 기수 ID를 client에게 입력받지 않는다.
장수/도시 목록은 기본 50·최대 200, ID cursor로 페이지를 읽는다. `at`이 있으면
현재 기수의 해당 월말/FINAL 표본에서 조회하고 미수집이면 `collected:false`다.
현재 일반 장수와 NPC/부대장 NPC, 국가·도시 필터를 지원한다. 저장·현재 자료 모두
DTO allowlist를 적용해 임의 meta를 응답하지 않는다. `coverage`도 월 header만
cursor 조회하며 원문·장수 목록을 같이 싣지 않는다. 아직 마감 월 캐시는 없다.
실제 Gateway HTTP에서 새 role 부여·범위 확대 거부·flush 호출·암호화 game token의
정확한 scope 전달을 확인했다. 별도 PostgreSQL/Redis와 실제 game HTTP에서는
no-general 허용, 무인증·일반 admin·다른 profile·제재 거부, 200 상한,
과거 이름/미수집, 새 기수 전환 후 이전 표본 차단과 flush 후 401을 확인했다.
`nationSeries`는 국가 1개의 월별 집계만 조회한다. 월/반기 해상도, 기간, 페이지 크기
50 기본/200 최대를 받고 긴 기수는 다음 기간 cursor로 이어 읽는다. 한 번에 읽는
월 header는 최대 1200개(200반기), 국가 집계도 그 범위의 해당 국가만 읽으며
병사수 집계가 없는 기존 표본만 해당 국가의 장수 원본을 DB에서 집계한다. 도시 원본이나
전체 trace는 읽지 않는다. API 기본 기간은 최근 6개월이고 반기는
1~~6월/7~~12월 경계로 묶는다. 보유·기술·집단 평균은 마지막 수집 표본과 그 시점을
반환하고 수입/급여만 기간 합산한다. 누락·국가 없음·불완전 정산의 흐름은 null,
관측한 정산 없음은 0이다. 기간 일부 요청은 from/to와 complete=false로 표시한다.
장수·도시 상세와 독립 로그, FINAL 별도 표시와 정책 조회는 기본 화면에 연결했다. 외교 이력도 연결했으며 NPC 결정과 조사 기능의 완성은 남았다.
## 초기 수집 지점과 쓰기 재검토
아래 표와 검증 진입점은 초기 구현 당시 inventory다. 이후 연결·검증 결과는 위의 현재 구현과 운영 안내를 기준으로 읽는다.
기준 Core commit은 `5ac961dfd17738dc4c39e6401f975296f39403a5`이다.
SQL/bytes는 아직 실측하지 않았으며 아래는 현재 소스에서 확인한 연결 지점과 구현 경계다.
| 자료 | 실제 source / 관측할 값 | 구현·비용 결정 | 남은 검증 |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| 월별 장수·도시·국가 | `turn/yearbookHandler.ts`의 `beforeMonthChanged`; `playAudit/snapshot.ts`의 필드 allowlist | 메모리 한 순회, 추가 SELECT 없이 수집. 상세는 도시/국가/장수로 페이지 조회 가능한 행에 batch 저장하며 큰 월 JSON 전체를 목록 조회하지 않음 | 단위/DB 연결 확인; 전체 종료 경로·coverage·비용 gate는 남음 |
| 세율 적용 수입·급여 | `turn/incomeHandler.ts`의 `applyIncome`, `incomeValue`, `current`, `next`, `ratio`, 장수별 `pay`; `turn/nationTaxRate.ts` | 이미 계산한 수치만 관측. 국가 수입과 실제 급여 합계 분리, 과거 metadata 재누적 금지. 정산 원장을 월집계 입력으로 재사용 | 정수화·최저 자원 보정, 원장/집계 원자 저장, 도입 월 coverage |
| 월별 내구성 | `turn/inMemoryWorld.ts`의 capture/restore, peek/acknowledge와 pending yearbook; `turn/databaseHooks.ts`의 `persistChanges` | 별도 audit pending을 같은 transaction과 savepoint에 포함. 기존 연감의 장기보존 테이블에 상세 감사를 넣지 않음 | 실패·중복·재시작, bounded 삭제 |
| 기수 identity | `scenario/scenarioSeeder.ts`의 `install.serverId`, `GameHistory` 충돌 검사 | profile명으로 대체하지 않음. 외부 install 입력을 만드는 지점과 RESET 전체 경로를 추가 추적한 뒤 수집 활성화 | 신규 identity 생성, 재시도, 기존 설치에 identity 누락 시 처리 |
| 외교 | game-api `router/diplomacy/index.ts`, engine 월간 외교 처리 | 불변 문서는 참조, 갱신되는 내용만 당시 버전 저장. 현재 상태 월복사만으로 사건을 대신하지 않음 | 모든 API/engine mutation별 inventory |
| NPC 정책 | `turn/worldCommandHandler.ts` → `turn/npcPolicyMutation.ts` | CAS 성공하고 실제 값이 달라진 경우에만 불변 버전. 무변경/거부는 적용 버전에서 제외 | NPC 결정의 버전 참조, 거부/무변경 시도 원장 |
| 권한 | Gateway `adminCapabilities.ts`, `adminAuth.ts`; game-api `trpc.ts` 인증·제재 middleware | scoped 감사 권한과 공통 계정 추가 권한 분리. `getMyGeneral` 요구 없이 서버에서 검사 | catalog/token/flush/HTTP matrix 전체 연결 |
월간 실행은 이전 월 snapshot → 달 변경 → 새달 `onMonthChanged` 순서다.
1월 금/7월 쌀 정산은 새로 진입한 월의 흐름으로 누적하고 그 월 마감에 집계한다.
입력 목록은 이미 로드된 world를 사용하며 기존 연감의 로그 SELECT를 새 감사 수집의
필수 입력으로 만들지 않는다.
## 검증 진입점
- 순수 projection: `app/game-engine/test/playAuditSnapshot.test.ts`.
- 월말·저장 경계: `monthlyBoundaryPrePersistence.integration.test.ts`.
- 수입: `monthlySemiAnnualPersistence.integration.test.ts`, `monthlyWarIncomePersistence.integration.test.ts`.
- 원자성: `inputEventAtomicity.test.ts`, `readModelChangeJournalPersistence.integration.test.ts`.
순수 fixture는 분모, 0/null, 소수 수입, 미수집, 외국 주둔, 과거 값의 독립성,
민감 meta 제외와 단일 순회를 검증한다. PostgreSQL SQL count/WAL/실행계획,
권한 HTTP, CHE/HWE Chromium과 전체 source inventory는 아직 남아 있다.
월 저장 검증: `playAuditCollection.test.ts`, `playAuditPersistence.integration.test.ts`와
확장한 `monthlyBoundaryPrePersistence.integration.test.ts`. 정확한 명령·결과는
상위 보고서 `2026-09-16-플레이-감사-월별-저장.md`에 기록한다.
## 2026-09-26 국가 그래프와 당월 정산
`nationSeries.currentSettlement`는 현재 월을 포함한 페이지에서만 같은 RepeatableRead
transaction의 `world_state.meta.playAuditFlows`를 읽는다. 국가·자원·연월과 유한 숫자를
확인하여 commit된 수입/지급을 반환한다. 1월 금, 7월 쌀 정산이 월말 표본을 기다리지 않고
노출되며, 당월 관측 없음·부분 관측·관측됨을 구별한다. 월말/반기 흐름 합계와 완전성은
기존 계약을 유지한다. gameplay 정산, RNG, flush 순서는 변경하지 않는다.
새 국가 표본은 집단별 `crew` 합계를 저장한다. 기존 JSON의 crew 누락은 null로 읽고,
시계열 조회 때 그 국가와 해당 표본 ID에 한정하여 PostgreSQL이 장수 표본을 한 번 집계한다.
저장된 집단 인원수와 원본 행 수가 일치할 때만 합계를 사용한다. 원본 누락은 null,
관측된 빈 집단은 0이며 live 장수로 과거를 채우지 않는다. 최대 1200개 표본에 한정된
추가 GROUP BY 조회이며, 새 집계가 있는 표본은 원본 재집계에서 제외한다.
차트는 [Chart.js line](https://www.chartjs.org/docs/latest/charts/line.html)과
[responsive](https://www.chartjs.org/docs/latest/configuration/responsive.html) 계약을 사용한다.
필요한 line 구성요소만 등록하고 resize·unmount를 처리하며 null 구간을 연결하지 않는다.
차트와 수치 표는 같은 응답을 사용하고 집단/지표 전환은 추가 API를 호출하지 않는다.
## 2026-09-26 원래 목표 재점검 보완
- `generals`는 금/쌀/병력/훈련/사기, 통솔/무력/지력, 경험/공헌, 병종 숙련도 5종의
서버 정렬을 현재와 월말/FINAL 모두 지원한다. 기본 ID 정렬은 기존 숫자 cursor를
유지하고 수치 정렬은 `{id, value}` cursor를 쓴다. 수치가 같으면 ID 오름차순,
NULL은 마지막이다. 검색·국가·도시·NPC 필터를 먼저 적용한 전체 집합을 정렬한다.
- SQL 식별자는 allowlist이고 검색값은 parameter이며 LIKE 특수문자는 literal 처리한다.
현재 조회는 ID/수치 조회와 projection 일괄 조회 2회, 과거는 표본 조회 1회다
(공통 world/header 조회 제외). 장수별 추가 조회나 저장 경로 변경은 없다.
- `cityMap({at?})`은 기존 감사 권한과 repeatable-read 경계에서 도시/국가 projection을
읽는다. 지도 화면을 선택할 때만 호출하며 최대 1024개 도시를 허용한다. 초과하면
오류로 목록 사용을 안내하고, 미수집 월은 빈 수집 상태로 반환한다.
- 기존 MapViewer를 사용하고 도시 클릭은 해당 시점 상세와 주둔 장수 목록으로 연결한다.
지도 소유/이름은 해당 표본을 사용하며 없는 표본을 무주 도시로 만들지 않는다.
좌표/배경은 현재 profile의 map layout이다. 물리적 지도 버전의 역사 복원은 아니다.
좌표가 없는 도시는 목록에서 확인하도록 ID를 안내한다. 지도는 모든 국가를 표시한다.
- 이 보완은 R2 수치 정렬과 R3 지도 탐색의 누락을 닫는다. 자원 이동 원장, 접속/계정 조사,
월중 전체 방문 이력, 취소 시즌 audit-only runtime, 전체 COST gate는 아직 완료하지 않았다.
원 설계 전체 P1–P6 완료로 해석하지 않는다.
+95 -75
View File
@@ -2,10 +2,10 @@
## 문서 상태와 사용법
**설계 기준: 2026-09-16. 제품 기능은 부분 구현 상태다.** 이 문서는 관리자 플레이
감사의 구현 goal과 완료 판정 기준이다. 문서 작성 완료는 기능 구현 완료가 아니다.
후속 작업은 아래 요구사항 ID, 단계와 증거 표를 유지하며 진행 상태를 갱신한다.
현재 구현 진행과 수집 지점은 [구현 inventory](play-audit-implementation.md)에 기록한다.
이 문서는 플레이 감사의 구조·현재 구현 경계·남은 요구사항을 관리하는 기준 문서다.
사용 방법과 DB 적용은 [운영 안내](../play-audit-operations.md)를 따른다.
날짜별 구현·측정 기록은 Git 이력과 상위 작업공간의 `report/`에서 확인한다.
아래 요구사항은 전체 목표이며 일부 구현만으로 완료를 의미하지 않는다.
사용자 결정으로 고정한 범위:
@@ -27,28 +27,50 @@
## 1. 현재 기반과 추가 작업
다음은 Core `f4aabec1fa13a0ae136b8ba96e8aa5dfa9a8e79e`의 정적 조사 결과다.
Ref 조사 checkout은 `ng_compare@2239ff667b3e841c9fed69b53539437a79203806`,
제품 기준선은 `devel@6c7f774fa1d49774a5924780516c88b8888cc1d7`이다.
후속 구현 시작 시 현재 소스와 다시 대조한다. 이 표는 live DB/runtime 검증이 아니다.
현재 소스의 책임은 다음과 같다. 경로는 Core root 기준이며 운영 적용 여부와는 별개다.
| 기반 | 확인한 source와 의미 | 감사 구현에서 보완할 점 |
| ----------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 관리자 조치 | `app/gateway-api/src/adminAudit.ts`, `AdminAuditEvent` | 기존 관리자 조치 원장은 유지하고 게임 플레이 기록과 구분 |
| 입력 원장 | `app/game-api/src/inputEventBoundary.ts`, `InputEvent` | API 요청 payload는 식별용 digest인 경로가 있음. 이를 실제 입력 내용으로 간주하지 않음 |
| 변경 알림 | `packages/common/src/realtime/changeJournal.ts` | entity/domain revision용이며 필드별 전후 값이나 원인 기록이 아님 |
| 턴 저장 | `app/game-engine/src/turn/inMemoryWorld.ts`, `databaseHooks.ts` | dirty state와 같은 transaction을 재사용하되 최종 dirty 상태만으로 개별 사건 순서를 복원하지 않음 |
| 월별 연감 | `yearbookHandler.ts`, `YearbookHistory` | 기존 지도·국가·로그와 겹치는 값은 재사용. 상세 장수·세율 적용 실제 수입은 별도 필요 |
| 국가 재정 | `incomeHandler.ts`, `monthlyNationStatsHandler.ts` | 이미 계산한 수입·지급·통계에서 수집. 감사용 재계산·추가 난수 소비 금지 |
| 장수·도시 | `nation/endpoints/getBattleCenter.ts`, `getSecretGeneralList.ts`, `world/index.ts` | 현재 조회를 활용하되 장수 없는 관리자와 과거 snapshot용 읽기 경계 추가 |
| 외교 | `Diplomacy`, `DiplomacyLetter`, `router/diplomacy/index.ts` | 현재 국가쌍 상태와 문서 체인만으로 변경 당시 상태가 모두 보존되지는 않음 |
| NPC 선택 | `ai/generalAi/core.ts`, `reservedTurnHandler.ts` | 일부 환경변수 stdout trace·action hook이 있으며 전체 과정의 내구성 기록은 없음 |
| NPC 정책 | `npcPolicyMutation.ts`, `router/npc/index.ts` | 현재 정책·마지막 변경 정보를 버전 이력과 연결 |
| 접속 | `GeneralAccessLog`, `TrafficPeriodGeneral` | 최신 활동·누적 통계이며 모든 가입·로그인·장수 생성 시도의 사건 원장이 아님 |
| 책임 | 구현 위치 | 경계 |
| ------------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
| 월말 상태·정산·집계 | `app/game-engine/src/playAudit/snapshot.ts`, `collection.ts` | 현재 정산과 확정 월말을 구분하며 미수집은 null |
| 외교·정책 사건 | 같은 디렉터리의 `diplomacy.ts`, `policy.ts`, `policyPersistence.ts` | 사건 순서와 당시 불변 정책 참조 보존 |
| NPC 결정 수집·저장 | `decision.ts`, `decisionPersistence.ts` | 실제 실행 경로만 관측하며 RNG를 추가 소비하지 않음 |
| 감사 실패 격리·보존 | `bestEffort.ts`, `retention.ts`, `retentionWorker.ts` | 6절의 savepoint·누락 표시·기수 분리 계약 |
| 읽기 API | `app/game-api/src/router/playAudit/` | profile 권한, 현재/과거 projection, 제한된 목록과 상세 |
| 화면 | `app/game-frontend/src/components/playAudit/` | 운영 안내의 현재 사용 범위 |
| DB 모델 | `packages/infra/prisma/game.prisma`, `gateway.prisma` | 정식 migration으로 추가하며 기존 원장과 구분 |
위 engine source의 생략한 prefix는 `app/game-engine/src/turn/`, game router는
`app/game-api/src/router/`다. Prisma 모델은 `packages/infra/prisma/game.prisma`와
`gateway.prisma`에 있다.
현재 장수 수치 정렬, 현재/과거 도시 지도, 요청 처리 조회까지 제공한다.
후보 내부 조건 전체, 상세 자원 이동 원장, 계정/IP HMAC 조사, 월중 전체 행동 이력,
취소 후 감사 전용 runtime과 전체 비용 gate는 미완성이다. R7-A~F는 이 후속 범위를
정의하므로 완료된 과거 계획으로 취급하지 않는다.
### 현재 읽기·수집의 세부 경계
- 국가 `currentSettlement`는 같은 RepeatableRead에서 읽은 현재 월의 확정 정산이다.
월말 snapshot과 합치지 않는다. 이전 월은 `beforeMonthChanged`에서 닫고 새해 금·7월 쌀
지급은 진입한 월의 flow에 누적한다. 과거 crew 집계가 없으면 일치하는 snapshot 표본만
제한적으로 집계하며 현재 장수 값으로 대체하지 않는다.
- 장수 목록은 현재/월말/FINAL의 금·쌀·병력·훈련·사기·능력·경험·공헌·숙련 정렬을
지원한다. SQL 정렬 필드는 허용 목록을 사용하며 동률은 ID 오름차순, null은 마지막이다.
- 도시 지도는 당시 소유·이름을 현재 지도 좌표·배경에 표시한다. 과거 지형 복원이 아니며
snapshot 누락을 중립 도시로 채우지 않는다. 1,024개 초과 도시는 명시적으로 거부한다.
- 결정 목록은 월을 생략하면 가장 최근 기록 월을 선택한다. 명시한 과거 월은 비어 있어도
유지하며 다음 페이지도 같은 월을 사용한다. 기본50건/최대200건이고 목록에서 전체 trace나
COUNT를 읽지 않는다. 상세는128항목 chunk를 기본1개/최대4개씩 읽는다.
- 결정 ID는 server/general/tick/revision/phase로 결정한다. header hash와 unique key로
중복·충돌을 구분하고128항목씩 chunk로 나눠200행 단위로 저장한다. pending 기록은
capture/restore/peek/ack 경계를 따른다. 감사 저장 실패 처리의 기준은 6.1절이다.
- `DECISION_START`의 schema1 `effectivePolicy`는 서버→국가·자동화·NPC 규칙을 합친
명시적 값이다. 우선순위·허용 여부·국가 정책20개 값·부대 편성을 저장하며 raw meta나
seed를 노출하지 않는다. 동적 후보 조건·지급 상한·별도 자동화 권한 판정은 포함하지 않는다.
- 실행 시도는 입력·조건·대기·문맥 검사와 대체 시도를 실제 순서대로 기록한다. 검사를
재실행하지 않으며 ATTEMPTS coverage로 이전 PROCEDURES 기록과 구분한다.
상세 중첩은 최대5단계, 검사 항목은 최대4개다. 수동 명령이 AI를 거치지 않으면 AI 결정은 없다.
- 실행 버전은 profile의 `buildCommitSha` 또는 `TURN_BUILD_COMMIT_SHA`의 전체40/64자리
SHA를 사용한다. 미지정·잘못된 값과 과거 기록은 null이며 현재 버전으로 채우지 않는다.
- `requestState`는 같은 기수의 정책 버전/외교 사건에서 참조한 `InputEvent`의 requestId와
inputSequence를 함께 확인한다. world·사건·요청 최대3회 읽기로 현재 상태·시도 횟수·처리
좌표·결과/오류 존재 여부를 반환한다. 원문 payload/error나 시도별 전체 이력은 제공하지 않는다.
Ref `hwe/_admin5.php`의 국가·평금쌀·병종 숙련 통계, `_admin7.php`의 전체
장수 로그, `_admin8.php`의 외교정보는 **계승할 정보·조사 목적**의 근거다.
@@ -56,6 +78,26 @@ Ref `hwe/_admin5.php`의 국가·평금쌀·병종 숙련 통계, `_admin7.php`
공통 계정 상관 조사와 알려진 버그 검색 프리셋은 **비교 불가인 Core 신규 기능**이다.
감사 기능을 추가하면서 Ref 계산이나 기존 일반 유저 권한을 변경하지 않는다.
### 최초 관측·기수·정리 경계
- 조회 달력의 하한은 유효한 `world.meta.initYear/initMonth`를 우선한다. 없으면 시나리오
시작 연도와 현재 연도 중 이른 연도의1월을 사용한다. 달력 하한은 수집 시작의 증거가
아니며 실제 자료 존재는 월 header로 확인한다. tick은 DB bigint 정밀도를 보존한다.
- 초기 정책·관계는 복구된 clock과 이미 로드된 상태를 관측하고 readiness 전에 저장한다.
재야 관계는 제외하며 원인·actor·과거 변경 시점을 추정하지 않는다. 정책 포인터는 기수와
국가 identity를 확인하고 CAS·권한 검사를 통과한 실제 변경만 버전으로 남긴다.
`OBSERVED_GAP`은 불일치의 재관측이지 과거 변경 이력의 복원이 아니다.
- `documentBaseline.ts`는 도입 표식이 없는 기수의 보유 문서를 ID cursor200건씩 읽는다.
원문은 불변 ID/hash로 참조하고 당시 상태·서명·이전 문서 번호만 보존한다. 빈 결과에도
도입 표식을 저장하며 정상 재시작은 다시 전체 문서를 읽지 않는다. 문서의 상태·서명과
달리 본문·작성자·작성 시각·국가쌍·prevId 변경은 DB trigger가 거부한다.
- 초기 저장은 schema→lease→CLOCK→GENERAL_ACCESS 잠금 순서와 기수·clock authority를
지킨다. commit 후 ack하며 감사 실패의 현재 처리 규칙은 6.1절을 따른다.
- RESET 직후 조회는 identity 필터로 차단한다. retention은 별도 transaction에서 schema
advisory lock과 현재 identity를 확인하고 부모 row를 잠근 뒤 자식 PK 최대200개씩 삭제한다.
FK RESTRICT와 빈 header 삭제로 대량 cascade·새 기수 삭제를 막는다. worker는 남은 key부터
재시작하며 이전 runtime·identity 누락에는 정리하지 않는다.
## 2. 화면·소유권·조회 계약
### 2.1 Profile 화면
@@ -81,8 +123,8 @@ base prefix를 사용하며 `/image/*`나 API URL을 root 배포 기준으로
### 2.2 API·권한
game-api가 `playAudit` 읽기 영역을 소유한다. 다음은 추가할 capability/API의 설계명이며
현재 존재하는 endpoint가 아니다. 입력은 공통으로 대상·기간·cursor를 받고 상세는 별도 조회한다.
game-api가 `playAudit` 읽기 영역을 소유한다. 아래 표는 요구사항 영역의 설계명이다. 실제 endpoint 이름과 제공 범위는
`app/game-api/src/router/playAudit/index.ts` 및 1절을 기준으로 한다. 입력은 공통으로 대상·기간·cursor를 받고 상세는 별도 조회한다.
| 조회 영역 | 최소 계약 |
| ----------------------------------- | ----------------------------------------------------------------------------------- |
@@ -424,38 +466,40 @@ NPC trace의 정책 참조와 사건 연결 외에 매 턴 전체 world를 seria
비용 표에 남긴다. 비용이 크면 중복·반복 query·직렬화·batch/index를 먼저 개선한다.
자료 샘플링·생략·보존기간 축소는 성능 최적화로 몰래 처리하지 않고 명시적 설계 변경으로 다룬다.
## 8. 단계·검증·goal 인계
### 7.4 NPC 결정 비용 probe
### 8.1 순차 구현 체크리스트
`app/game-engine/test/playAuditDecisionCost.integration.test.ts`는
`PLAY_AUDIT_COST_DATABASE_URL`이 가리키는 격리 PostgreSQL을 사용한다.
정식 migration을 적용한 `play_audit_cost_decision_fixture` schema가 필요하며
fixture 테이블을 비우므로 공유 DB에는 실행하지 않는다.
각 단계는 코드·mapping·fixture·보고서·관련 commit을 포함한다. 병렬 branch나
별도 goal을 자동 생성하지 않는다. 완료한 단계가 전체 구현 완료를 대신하지 않는다.
```bash
pnpm --filter @sammo-ts/game-engine test playAuditDecisionCost.integration.test.ts --no-file-parallelism
```
- [ ] P1. source/쓰기 inventory, 비용 표, 기수 identity와 수집 시작, 권한·API 계약,
migration·rollback·보존 정리 경계를 구현한다.
- [ ] P2. 월별 상태·국가 집계와 R1~R3 profile 화면을 구현한다.
- [ ] P3. R4/R6 외교·정책 버전과 조회, 수뇌 공개용 projection 경계를 구현한다.
- [ ] P4. R5 모든 NPC 경로 계측, 행위·자원 변화와 요청/실행 연결을 구현한다.
- [ ] P5. 조사 A~F, 공통 계정 adapter·30일 정리, 권한별 UI를 구현한다.
- [ ] P6. 아래 검증을 수행하고 비용·coverage·미검증 범위를 보고해 전체 완료를 판정한다.
201개 결정×302항목으로200행 batch 경계를 넘겨201개 header·603개 chunk·60,702항목을
검사한다. `/tmp/play-audit-decision-cost.json`에 JSON 크기, `pg_column_size`,
table/TOAST/index 크기, transaction 시간, warm30회 목록 p50/p95와 EXPLAIN을 남긴다.
URL·secret·원문 payload는 출력하지 않는다. 반복 RNG를 포함한 합성 자료라 압축률을
운영 평균으로 사용할 수 없고, WAL·heap 및 전체 COST gate를 대체하지 않는다.
### 8.2 요구사항별 증거
## 8. 요구사항별 검증
각 요구사항의 일부 구현과 검증은 구현 inventory/report에 기록한다. 아래 전체 합격 기준을
각 요구사항의 일부 구현과 검증은 이 문서의 1절과 날짜별 report에 기록한다. 아래 전체 합격 기준을
충족하기 전에는 요구사항 전체를 완료로 체크하지 않는다.
| ID | 합격 기준 | 필요한 증거 |
| ------- | ------------------------------------------------------------- | -------------------------------------------------------------------------- |
| R1 | 월/반기, 실제 정산, 집단 분모·0/null·부분 기간 정확 | 정산 fixture, PostgreSQL reload, 그래프와 표 값 비교 |
| R2 | 모든 국가·재야 장수 현재/과거 상세, 허용 로그의 독립 표시 | 권한 HTTP matrix, 사망/개명 fixture, Chromium |
| R3 | 당시 소유·내정·국가별 주둔과 병력/훈련/사기 | 월중 이동·점령·월말 fixture와 DB, 지도/장수 drill-down |
| R4 | 같은 달 복수 외교 전이와 당시 유효 문서 | API 즉시 처리·engine 만료/개전·취소/복구 DB fixture |
| R5 | 전체 개인/수뇌 AI 경로의 판정·선택·실행 연결 | handler inventory, 정책 차단·조기 반환·fallback·실패 trace, fixed seed A/B |
| R6 | 실제 적용 버전·actor·전후, CAS 거부 분리 | 충돌/무변경/직책 변경 fixture, 과거 결정의 정책 참조 |
| R7-A~F | 5절 각 질문을 정상·해당·근거 부족 사례로 조사 가능 | 도구별 API/DB fixture와 Chromium 탐색 artifact |
| AUTH | no-general 관리자 허용, 다른 profile/일반 유저/수뇌 비밀 차단 | 실제 HTTP token·scope·권한 revoke·cache matrix |
| DURABLE | gameplay/audit 원자성, 중복 방지·rollback·재시작·RESET 분리 | 실제 PostgreSQL migration 전체/증분/no-op, 실패·복구·정리 fixture |
| COST | 7절 필수 gate와 계측 전후 비용 검토 완료 | SQL count·실행계획·WAL/bytes·p95·heap 측정 보고 |
| ID | 합격 기준 | 필요한 증거 |
| ------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- |
| R1 | 월/반기, 실제 정산, 집단 분모·0/null·부분 기간 정확 | 정산 fixture, PostgreSQL reload, 그래프와 표 값 비교 |
| R2 | 모든 국가·재야 장수 현재/과거 상세, 허용 로그의 독립 표시 | 권한 HTTP matrix, 사망/개명 fixture, Chromium |
| R3 | 당시 소유·내정·국가별 주둔과 병력/훈련/사기 | 월중 이동·점령·월말 fixture와 DB, 지도/장수 drill-down |
| R4 | 같은 달 복수 외교 전이와 당시 유효 문서 | API 즉시 처리·engine 만료/개전·취소/복구 DB fixture |
| R5 | 전체 개인/수뇌 AI 경로의 판정·선택·실행 연결 | handler inventory, 정책 차단·조기 반환·fallback·실패 trace, fixed seed A/B |
| R6 | 실제 적용 버전·actor·전후, CAS 거부 분리 | 충돌/무변경/직책 변경 fixture, 과거 결정의 정책 참조 |
| R7-A~F | 5절 각 질문을 정상·해당·근거 부족 사례로 조사 가능 | 도구별 API/DB fixture와 Chromium 탐색 artifact |
| AUTH | no-general 관리자 허용, 다른 profile/일반 유저/수뇌 비밀 차단 | 실제 HTTP token·scope·권한 revoke·cache matrix |
| DURABLE | 감사 실패 격리·누락 표시, gameplay 실패 보존·중복 방지·기수 분리 | 실제 PostgreSQL migration 전체/증분/no-op, 실패·복구·정리 fixture |
| COST | 7절 필수 gate와 계측 전후 비용 검토 완료 | SQL count·실행계획·WAL/bytes·p95·heap 측정 보고 |
계측 전후 fixed seed 비교는 명령뿐 아니라 RNG 소비 순서, state, 기존 로그와
side effect까지 포함한다. 계승 계약에 영향을 준 경로는 Ref 차등을 추가하고 감사 UI 자체는
@@ -465,27 +509,3 @@ GUI는 같은 Chromium·viewport·DPR·zoom·font·fixture 조건에서 실제 `
`/hwe/play-audit`의 direct URL·refresh·asset/API·필터·기간 이동·뒤로가기·상세 열기와
desktop/mobile geometry를 확인한다. screenshot·DOM·computed style·측정 artifact를 남긴다.
실제 공개 HTTPS 검증은 배포가 별도 승인·수행된 경우에만 보고하며 fixture 결과와 구분한다.
### 8.3 후속 goal에 사용할 지시문
> `core2026/docs/design/play-audit.md`의 확정 계약을 기준으로 프로필별 플레이 감사를
> 구현한다. R1~~R7-A~~F와 AUTH/DURABLE/COST 전체를 완료하며 P1~P6를 순서대로 진행한다.
> 사용자 수정사항이 문서보다 우선한다. 현재 Git/source를 재확인하고 기존 dirty 작업을
> 보존한다. 기능마다 조사 근거, 수집 coverage, DB 비용 재검토와 실제 검증 artifact를
> 남긴다. 문서·일부 UI·unit test만으로 전체 goal을 완료하지 않는다. 관련 변경은 저장소별로
> commit하고 push·배포는 별도 요청 범위로 둔다. 자동 탐지/제재, 수뇌 화면, 지난 기수
> 장기보존이나 다른 frontend 신규 개발로 범위를 확대하지 않는다.
문서 수정은 결정 이유와 영향을 받는 요구사항·비용·검증을 함께 변경한다.
실행 일자·결과·미검증·commit은 상위 `report/`에 기록한다. 이 문서의 체크박스는
단순 계획·시도·의도로 완료 처리하지 않는다.
### 8.4 사용량을 고려한 중간 전달
2026-09-16 사용자는 주간 잔여 약42%에서 약20%를 남기고 현재 작업물을 마무리하여
사용 가능한 기능부터 이용할 수 있게 하며 DB 스키마도 미리 준비하도록 지시했다.
이에 따라 큰 미완성 기능을 추가로 벌이기보다 현재 수집·조회·권한의 회귀 검증,
정식 migration·운영 안내, 현재 main 통합과 push를 우선한다. 운영 적용은 대상 프로필을
확인한 범위에서 수행한다. 구현되지 않은 NPC trace/조사 도구나 미검증 COST gate를
완료로 표시하지 않는다. 후속 설계 범위는 삭제하지 않고 [운영 안내](../play-audit-operations.md)의
사용 가능/미완성 경계와 구현 inventory에 남긴다.
@@ -1,183 +0,0 @@
# Game clock reconciliation implementation plan
Baseline: `main@b91dcbcaaac5acd4c7349cd3ed0996c547f58756`
Branch: `test/game-clock-reconciliation-20260903`
이 문서는 위 기준선·branch에서 진행한 구현 계획의 기록입니다. 체크 표시는 당시
코드와 집중 검증이 있었다는 뜻이며, 현재 main·배포·운영 검증 상태를 나타내지 않습니다.
현재 동작은 [게임 시계](../architecture/game-clock.md)와
[재정렬 계약](../architecture/game-clock-reconciliation.md), 장애 처리는
[복구 안내](./game-clock-recovery.md)를 읽으세요.
## Milestone 1 - authority and inventory
- [x] Branded `GameTick`, `ObservedGameInstant`, `ScheduleInstant`,
`WallInstant`, and `ClockRevision` boundaries.
- [x] Explicit clock phase and monotonic RUNNING projection.
- [x] Exact alignment arithmetic preserving millisecond/sub-turn remainder.
- [x] Opening tick zero and PREOPEN executable floor in the shared seeder.
- [x] Schema columns for phase, revision, and deadline generation.
- [x] Suspension, participant-checksum, and Redis projection outbox tables.
- [x] Machine-readable DB/Redis/JSON participant inventory and architecture gate.
- [x] Turn flush lock prefix and phase/revision/generation fence.
- [x] Empty and upgraded database migration execution evidence.
## Milestone 2 - exact DB reconciliation
- [x] Suspension start command with DB wall time and idempotent source revision.
- [x] Exact resume plan transaction with deterministic participant lock order.
- [x] SHIFT adapters for cursor, generals, active auctions, message expiry, vote
end, select pool, and NPC selection windows.
- [x] KEEP checksum adapters for occurrences and accepted command coordinates.
- [x] Explicit `LEGACY_COMPLETE_TURNS` and bounded `CATCH_UP` policies.
- [x] Property tests for remaining distance, ordering, and history invariants.
- [x] 24-hour and 65m17.250s PostgreSQL integration evidence.
## Milestone 3 - revisioned Redis and workers
- [x] Projection outbox claimer/retry/recovery state machine.
- [x] Redis active revision and atomic due-pop script.
- [x] Auction OPEN/FINALIZING revision and generation fence.
- [x] Tournament durable tick dual-write and projection rebuild.
- [x] DB-commit/Redis-failure crash-restart test.
- [x] Readiness integration in Gateway/API process health.
## Milestone 4 - command and lifecycle workflows
- [x] All durable input events record accepted tick and accepted revision.
- [x] Processing converts accepted coordinates across revisions or fails closed.
- [x] Gateway pause/resume/open orchestration writes the DB clock phase.
- [x] Unification wait becomes a durable `UNIFICATION_WAIT` suspension.
- [x] Alignment, optional rate change, invader IDs/RNG, creation, first schedule,
outbox, verification, and RUNNING transition form one retry-safe workflow.
- [x] Multi-host drift and general-access/clock-operation deadlock tests.
## Milestone 5 - test-branch release gate
- [x] Full typecheck, architecture, lint, unit, build, and non-conditional
integration suites.
- [x] Dedicated PostgreSQL/Redis conditional integration suite with skip count
recorded.
- [x] Recovery runbook exercised from each incomplete status.
- [x] Admin status/readiness exposes revision, phase, participant checksums, and
incomplete outbox state.
- [ ] User-test deployment evidence is recorded separately from Git push.
- [x] All `FORBID` inventory entries are removed by typed migrations or proven
inactive preconditions.
## Evidence log
### 2026-09-03 - authority foundation
- `pnpm test:bootstrap`: dependency installation, Prisma generation, and package
preparation passed in the dedicated worktree.
- `CI=1 TURBO_CONCURRENCY=1 pnpm typecheck`: 21/21 tasks passed.
- `CI=1 TURBO_CONCURRENCY=1 pnpm test`: 12/12 package tasks passed. Conditional
suites remain classified separately and are not integration evidence.
- `CI=1 TURBO_CONCURRENCY=1 pnpm build`: 26/26 tasks passed.
- `TURBO_CONCURRENCY=1 pnpm lint`: passed with 36 pre-existing frontend
warnings and no errors.
- `pnpm check:architecture`: package boundaries passed; 21 authoritative clock
fields and 18 participants were registered.
- Migration SQL was generated, formatted, validated, and registered as the
release manifest head. All 43 migrations applied to the empty dedicated
PostgreSQL instance. A second schema upgraded from the previous head with an
existing manual `world_state` row and verified `MANUAL:1:1` plus all three
reconciliation tables.
- Dedicated PostgreSQL/Redis fixtures proved exact 24-hour and 65m17.250s gaps,
schedule distance/order preservation, occurrence checksums, live-lease
fencing, a single durable outbox, Redis target revision, and recovery after a
crash between Redis commit and DB finalization.
### 2026-09-03 - revisioned workers and input coordinates
- `CI=1 TURBO_CONCURRENCY=1 pnpm typecheck`: 21/21 tasks passed after the
worker/input contract changes.
- `CI=1 TURBO_CONCURRENCY=1 pnpm test`: 12/12 package tasks passed.
- Dedicated PostgreSQL runs passed `databaseCommandQueue.integration.test.ts`
6/6 and `runtimeClockShiftPersistence.integration.test.ts` 3/3 when executed
sequentially; the latter now clears each single-world fixture before the next
case. Dedicated Redis passed tournament source revision 4/4, including an
atomic stale-clock rejection. API input-event integration passed 13/13.
- The 24-hour/65m17.250s reconciliation suite passed 2/2 after the other DB
suites. Conditional files share a deliberately dedicated schema and are run
sequentially to prevent their fixture cleanup from racing another file.
### 2026-09-03 - Gateway lifecycle authority
- Runtime `PAUSE`/`STOP` starts a durable maintenance suspension before the
Gateway status and process reconciliation change. `RESUME` completes the DB
reconciliation and Redis outbox before the profile becomes `RUNNING`.
- Both an already-built overdue `RESERVED` profile and an overdue `PREOPEN`
profile promote the game DB from `PREOPEN@0` before the Gateway status changes
to `RUNNING`; the Redis clock phase is revision/generation fenced.
- `pnpm --filter @sammo-ts/gateway-api test` passed 313 tests with 35
environment-conditional skips. Gateway typecheck and target lint passed.
### 2026-09-03 - atomic unification wait
- A unification flush now commits the finalization, actionable prompts, and one
deterministic `UNIFICATION_WAIT` suspension together. A late archive failure
rolls the whole boundary back; retry creates one ledger.
- The suspended command queue admits only an invader decision tied to the
active ledger. The daemon-authorized command transaction applies a 36-hour
exact gap, preserves participant positions, changes the fixture rate from 10
to 20 minutes, creates one invader nation and ten deterministic generals with
future first turns, and writes one final-rate projection outbox.
- The dedicated PostgreSQL/Redis fixture reached `RUNNING@2/2` only after the
Redis projection. DB-wall versus a mocked 12-hour host drift and concurrent
general-access lock acquisition completed without drift or deadlock.
- `CI=1 TURBO_CONCURRENCY=1 pnpm typecheck` passed 21/21 tasks,
`CI=1 TURBO_CONCURRENCY=1 pnpm test` passed 12/12 package tasks,
`CI=1 TURBO_CONCURRENCY=1 pnpm build` passed 26/26 tasks, and workspace lint
passed. `pnpm check:architecture` registered 22 authoritative clock fields
and 18 participants.
- Dedicated sequential PostgreSQL/Redis runs passed clock reconciliation 3/3,
atomic unification 1/1, command queue 7/7, runtime clock persistence 3/3,
API input-event boundary 13/13, and tournament revision 4/4: 31/31 enabled
tests with zero skips. The queue and API suites now create and remove their
own clock fixtures so a completed file cannot leak phase or initialization
state into the next file.
- User-test deployment and public runtime evidence remain deliberately open:
Git push is not deployment, and no deployment was authorized in this work.
### 2026-09-03 - completion audit
- The invader response now reads the exact post-alignment world snapshot even
when the turn rate is unchanged. The HWE-shaped regression asserts all ten
first turns are in the aligned next-month window rather than already due.
- The ordinary flush fence now uses the same incomplete-row dual-read rule as
`worldLoader`: a legacy row is `MANUAL` until all four clock snapshot fields
exist. Input-event acceptance remains fail-closed until initialization.
- Auction OPEN/FINALIZING recovery fixtures and direct PROCESSING input-event
fixtures now carry explicit phase/revision/generation coordinates. The
optional Ref-only troop parity suite is registered only in Ref mode, so the
Core conditional gate reports only tests it actually ran.
- `pnpm check:architecture` passed with 22 authoritative fields and all 18
required DB/JSON participants. The gate now rejects duplicate or `FORBID`
participants, missing required participants, and unimplemented/duplicate
Redis participants.
- `CI=1 TURBO_CONCURRENCY=1 pnpm test:integration:conditional` passed 79 files
and 234 tests against isolated PostgreSQL/Redis schemas with zero skipped and
zero failed files.
- On the audited diff, `pnpm check:architecture` passed, full typecheck passed
21/21 tasks, workspace test passed 12/12 tasks, build passed 26/26 tasks, and
workspace lint completed without errors.
The required acceptance cases map to automated evidence as follows:
| Contract | Automated evidence |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| PREOPEN signed tick, tick-zero opening, executable floor; RUNNING rewind monotonicity | `packages/common/test/gameClock.test.ts`, `app/game-engine/test/scenarioSeeder.test.ts`, `app/gateway-api/test/orchestratorOperations.test.ts` |
| Exact 24-hour and 65m17.250s gaps; ordering, remaining distance, occurrence history | `app/game-engine/test/clockReconciliation.integration.test.ts`, `packages/common/test/gameClock.test.ts` |
| DB commit/Redis failure and incomplete-status recovery | `app/game-engine/test/clockReconciliation.integration.test.ts`, `app/game-engine/test/unificationFinalization.integration.test.ts` |
| Auction OPEN/FINALIZING and tournament revision races | `app/game-api/test/auctionWorker.integration.test.ts`, `app/game-api/test/tournamentStoreRevision.integration.test.ts` |
| Pending commands crossing a clock revision | `app/game-engine/test/databaseCommandQueue.integration.test.ts`, `app/game-api/test/inputEventBoundary.integration.test.ts` |
| Delayed opening | `app/gateway-api/test/orchestratorOperations.test.ts`, `app/game-engine/test/scenarioSeeder.test.ts` |
| 36-hour unification wait, rate change, deterministic retry, future invader turns | `app/game-engine/test/unificationFinalization.integration.test.ts`, `app/game-engine/test/unificationInvaderResume.test.ts` |
| DB wall despite host drift; general-access lock ordering | `app/game-engine/test/clockReconciliation.integration.test.ts`, `app/game-engine/test/profileSchemaAdvisoryLock.integration.test.ts` |
| Generated exact-gap ordering/remaining/history invariants | `packages/common/test/gameClock.test.ts` |
Deployment and public-runtime evidence remain outside this audit and are still
open pending explicit user-test deployment authorization.
+36 -32
View File
@@ -13,39 +13,43 @@
관리자 감사는 [운영 안내](../play-audit-operations.md)에서 현재 기능을,
[설계](../design/play-audit.md)에서 요구사항과 배경을 확인합니다.
| 작업 | 문서 | 코드 시작점 |
| --------------------- | --------------------------------------------------------------- | ------------------------------ |
| 전체 구조 | [아키텍처 개요](../architecture/overview.md) | `app/`, `packages/`, `tools/` |
| process·worker·daemon | [런타임 아키텍처](../architecture/runtime.md) | app server와 CLI |
| profile·Gateway 배포 | [릴리스 운영 매뉴얼](../release-operations.md) | Admin GUI와 release-controller |
| 파일 위치 | [파일 지도](./code-map.md) | router, handler, schema |
| package 의존 방향 | [패키지와 파일 경계](../architecture/package-boundaries.md) | `packages/*`, `app/*` |
| 명령·전투·효과 | [도메인과 조립](./domain-and-classes.md) | `packages/logic` |
| mutation·flush | [요청·턴·저장](./request-turn-persistence.md) | game API, game engine |
| action module | [행동 모듈 프로토콜](../architecture/action-module-protocol.md) | `actionModules/` |
| ref 비교 | [차등 검증](../architecture/turn-state-differential-testing.md) | `tools/integration-tests` |
## 현재 구조와 구현 계약
[구조 문서 찾아보기](./reference-map.md)에는 시계·재시도·실시간 갱신·측정·운영 자료를
질문별로 모았습니다. 과거 계획과 현재 계약의 구분도 여기서 확인할 수 있습니다.
| 질문 | 문서 |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 무엇이 어디서 실행되나요? | [개요](../architecture/overview.md), [런타임](../architecture/runtime.md) |
| 어느 파일부터 읽나요? | [파일 지도](./code-map.md), [패키지 경계](../architecture/package-boundaries.md) |
| 한 행동이 어떻게 계산되나요? | [도메인과 조립](./domain-and-classes.md), [행동 모듈](../architecture/action-module-protocol.md) |
| 언제 저장되고 실패는 어떻게 처리하나요? | [요청·턴·저장](./request-turn-persistence.md), [API 재시도](../architecture/api-input-event-replay.md) |
| 시간 정지·재개는 어떻게 되나요? | [게임 시계](../architecture/game-clock.md), [시간 도메인 목록](../architecture/time-domains.md), [재정렬 계약](../architecture/game-clock-reconciliation.md) |
| 설정을 여러 파일에서 조합하나요? | [시나리오 합성](../architecture/scenario-composition.md) |
| 화면에 변경이 어떻게 전달되나요? | [실시간 변경 원장](../architecture/realtime-change-journal.md) |
| 전투 시뮬레이터가 어디서 계산하나요? | [브라우저 Worker](../architecture/battle-simulator-browser-worker.md) |
| Ref와 무엇을 비교하나요? | [차등 검증](../architecture/turn-state-differential-testing.md) |
| TypeScript 버전이 왜 둘인가요? | [도구 체인 정책](../architecture/typescript-version.md) |
## 경계
## 변경 범위를 빠짐없이 확인하는 목록
- `packages/logic`은 계산과 규칙을 소유합니다.
- runtime I/O는 logic의 port를 app/infra adapter가 구현해 주입합니다.
- `app/game-engine`은 clock, queue, AI, 월간 순서, transaction과 flush를
소유합니다.
- `app/game-api`는 transport, 인증, input validation과 request acceptance를
소유합니다.
- `packages/infra`의 Prisma schema와 migration은 영속 구조의 기준입니다.
- `resources`는 scenario, map, unit set과 command profile을 구성합니다.
- actor와 resource owner는 session과 DB에서 서버가 결정합니다.
- `InputEvent`와 PostgreSQL transaction이 gameplay mutation의 내구성
경계입니다.
- Redis와 SSE는 session·worker·fan-out 경로이며 world commit의 대체 저장소가
아닙니다.
[엔진 호출 procedure 목록](../architecture/game-api-daemon-procedure-inventory.md)과
[직접 변경·journal 목록](../architecture/game-api-direct-mutation-journal-inventory.md)은
읽기 교재보다 변경 누락을 찾는 검토 자료입니다. 표의 조사 날짜·기준선과 현재
router·검사 코드를 함께 확인하세요. 새 API가 늘어도 과거 조사 숫자가 자동으로
현재 개수가 되지는 않습니다.
기능의 대응이 바뀌면 작업공간 루트 기준
`docs/ref-core2026-mapping.md`에 entry point,
호출 순서, 인증, DB mutation, RNG와 오류 경로를 갱신해 주세요.
공통 작업 규칙과 계약 분류는 제품 저장소 루트의 `AGENTS.md`를 따릅니다.
이 파일들은 핸드북 사이트 밖의 저장소 문서입니다.
시계 참여자 JSON·mutation evidence TSV는 기계가 사용하는 자료입니다. 형식과
검사 계약을 유지하며 해당 기능을 바꿀 때 갱신합니다.
## 측정·계획·운영 자료
| 자료 | 해석 범위 |
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [NPC 메모리 측정](../architecture/npc-lifecycle-memory-profile.md) | 지정 fixture의 메모리·생성/사망 부하. 운영 DB 처리량과 다름 |
| [NPC 천통 시간 측정](../architecture/npc-unification-timing-benchmark.md) | 인메모리 계산 시간. 실제 배포 성능 보장이 아님 |
| [시계 복구](./game-clock-recovery.md) | 상태·원장 확인과 같은 작업 재시도 |
| [릴리스 운영](../release-operations.md) | 실제 배포 작업과 준비 확인 |
| [관리자 콘솔](../admin-console.md), [플레이 감사 운영](../play-audit-operations.md) | 관리자 권한과 현재 조사 기능 |
| [테스트 정책](../testing-policy.md) | 실행 준비·검증 종류·skip의 해석 |
| [프론트엔드 CSS 구조](../frontend-css-architecture.md) | 화면 스타일의 소유권과 배치 계약 |
기능 설계의 목표, 코드에 있는 구현, 테스트로 확인한 범위, 실제 배포 상태는 서로
다릅니다. 문서의 “완료”는 함께 적힌 날짜와 검증 범위로 해석하세요.
-47
View File
@@ -1,47 +0,0 @@
# 구조 문서 찾아보기
[기초 안내](./first-steps.md)와 [경험자 안내](./system-walkthrough.md)가 학습 경로를,
아래 문서들은 세부 계약을 제공합니다. 목록·벤치마크·과거 계획을 처음부터 모두
읽기보다 현재 질문에 해당하는 문서를 고르세요.
## 현재 구조와 구현 계약
| 질문 | 문서 |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 무엇이 어디서 실행되나요? | [개요](../architecture/overview.md), [런타임](../architecture/runtime.md) |
| 어느 파일부터 읽나요? | [파일 지도](./code-map.md), [패키지 경계](../architecture/package-boundaries.md) |
| 한 행동이 어떻게 계산되나요? | [도메인과 조립](./domain-and-classes.md), [행동 모듈](../architecture/action-module-protocol.md) |
| 언제 저장되고 실패는 어떻게 처리하나요? | [요청·턴·저장](./request-turn-persistence.md), [API 재시도](../architecture/api-input-event-replay.md) |
| 시간 정지·재개는 어떻게 되나요? | [게임 시계](../architecture/game-clock.md), [시간 도메인 목록](../architecture/time-domains.md), [재정렬 계약](../architecture/game-clock-reconciliation.md) |
| 설정을 여러 파일에서 조합하나요? | [시나리오 합성](../architecture/scenario-composition.md) |
| 화면에 변경이 어떻게 전달되나요? | [실시간 변경 원장](../architecture/realtime-change-journal.md) |
| 전투 시뮬레이터가 어디서 계산하나요? | [브라우저 Worker](../architecture/battle-simulator-browser-worker.md) |
| Ref와 무엇을 비교하나요? | [차등 검증](../architecture/turn-state-differential-testing.md) |
| TypeScript 버전이 왜 둘인가요? | [도구 체인 정책](../architecture/typescript-version.md) |
## 변경 범위를 빠짐없이 확인하는 목록
[엔진 호출 procedure 목록](../architecture/game-api-daemon-procedure-inventory.md)과
[직접 변경·journal 목록](../architecture/game-api-direct-mutation-journal-inventory.md)은
읽기 교재보다 변경 누락을 찾는 검토 자료입니다. 표의 조사 날짜·기준선과 현재
router·검사 코드를 함께 확인하세요. 새 API가 늘어도 과거 조사 숫자가 자동으로
현재 개수가 되지는 않습니다.
시계 참여자 JSON·mutation evidence TSV는 기계가 사용하는 자료입니다. 형식과
검사 계약을 유지하며 해당 기능을 바꿀 때 갱신합니다.
## 측정·계획·운영 자료
| 자료 | 해석 범위 |
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [NPC 메모리 측정](../architecture/npc-lifecycle-memory-profile.md) | 지정 fixture의 메모리·생성/사망 부하. 운영 DB 처리량과 다름 |
| [NPC 천통 시간 측정](../architecture/npc-unification-timing-benchmark.md) | 인메모리 계산 시간. 실제 배포 성능 보장이 아님 |
| [시계 구현 계획 기록](./game-clock-reconciliation-plan.md) | 당시 branch의 milestone. 현재 운영 상태의 기준이 아님 |
| [시계 복구](./game-clock-recovery.md) | 상태·원장 확인과 같은 작업 재시도 |
| [릴리스 운영](../release-operations.md) | 실제 배포 작업과 준비 확인 |
| [관리자 콘솔](../admin-console.md), [플레이 감사 운영](../play-audit-operations.md) | 관리자 권한과 현재 조사 기능 |
| [테스트 정책](../testing-policy.md) | 실행 준비·검증 종류·skip의 해석 |
| [프론트엔드 CSS 구조](../frontend-css-architecture.md) | 화면 스타일의 소유권과 배치 계약 |
기능 설계의 목표, 코드에 있는 구현, 테스트로 확인한 범위, 실제 배포 상태는 서로
다릅니다. 문서의 “완료”는 함께 적힌 날짜와 검증 범위로 해석하세요.
+137 -1
View File
@@ -1,5 +1,8 @@
# Legacy MariaDB long-lived data migration
이 문서는 이관 범위·설정·CLI 실행·데이터 대응·전환 검증의 단일 기준입니다.
도구 위치는 `tools/legacy-db-migration`이며 명령은 Core 저장소 root에서 실행합니다.
## Scope and safety boundary
`tools/legacy-db-migration` is the only supported importer. It has no HTTP
@@ -20,7 +23,96 @@ the imported rows. A repeat import updates archive-owned rows but does not
replace a live Gateway account's password, reset status, login/display identity,
OAuth connection, roles, sanctions, consent, icon or login timestamps.
### Full and incremental execution
## Source restore
Restore each compressed table dump into a private MariaDB database before
running this tool. Do not expose that database on a public interface. The dump
directory is intentionally Git-ignored.
```sh
gzip -cd /path/to/db_dumps/root/member.sql.gz | mariadb root_dump
gzip -cd /path/to/db_dumps/che/ng_games.sql.gz | mariadb che_dump
```
Restore all tables defined by ref even though the CLI intentionally projects
only long-lived tables. This lets the dry-run verify the source inventory and
keeps the original dump as the recovery source.
Database URLs belong in a Git-ignored environment file or injected process
environment. They are deliberately not accepted as command-line flags.
## Ordered migration plan
Copy `tools/legacy-db-migration/migration-plan.example.json` to the Git-ignored
`tools/legacy-db-migration/migration-plan.json`.
Set its mode to 0600, then enter the MariaDB host, port, database and user for
Gateway and each game profile. A password can come from a separate mode-0600
file (recommended), an environment variable, or directly from the mode-0600
plan. Target PostgreSQL URLs remain in the named environment variables.
When the Gateway source contains a non-default member icon, `gateway.userIcons`
is mandatory. Mount Ref's `d_pic` directory read-only as `sourceDirectory`, and
mount the Core2026 sam-image upload secret as a mode-0600 `uploadSecretFile`.
The two URL fields normally point to `https://sam-image.hided.net` and its
`/icons` path. The importer never adds account images to the image Git tree.
An invalid historical file blocks the plan by default. After byte-level review,
its member number may be listed in `excludedMemberNumbers`; the exclusion is
accepted only while that exact member still has invalid image geometry/format.
A valid file or stale/missing member exclusion fails closed. An unchanged
invalid Ref selection is reset to the default icon instead of publishing bad
bytes; a newer Core selection is preserved.
```sh
mkdir -p tools/legacy-db-migration/secrets
chmod 700 tools/legacy-db-migration/secrets
cp tools/legacy-db-migration/migration-plan.example.json \
tools/legacy-db-migration/migration-plan.json
chmod 600 tools/legacy-db-migration/migration-plan.json
chmod 600 tools/legacy-db-migration/secrets/*
pnpm migrate:legacy -- check-plan \
--config tools/legacy-db-migration/migration-plan.json
pnpm migrate:legacy -- run-plan \
--config tools/legacy-db-migration/migration-plan.json --mode full
pnpm migrate:legacy -- run-plan \
--config tools/legacy-db-migration/migration-plan.json --mode full --apply
```
For profiles whose old file archive is available, add a `battleResults` block.
`directory` may be a local absolute/plan-relative path, or `sshHost` may select
the host from which the directory is read. The SSH target must be a configured
host alias; do not put credentials in the plan.
```json
"battleResults": {
"sshHost": "serv",
"directory": "/home/letrhee/web_symlinks/sam_hided_net/sam/che/logs/preserved"
}
```
`check-plan` opens every source and target without writing. Its stage JSON lists
every included item as `inventory`, including source, target, strategy and the
information transferred. Gateway preflight validates every local or already
uploaded custom icon and reports the source split without issuing a PUT. A
configured battle-result source also reports its
season/file/byte counts. `run-plan` also
preflights every stage before the first import, is a dry-run without `--apply`,
and stops at the first failed stage. Completed earlier stages remain committed;
rerunning is safe because the Gateway and each profile have independent locks,
transactions and run records. The JSON output never includes a connection URL
or password.
For a later delta, keep the same `sourceSet`, connection identity and source
databases, restore or expose the newer snapshot, then run:
```sh
pnpm migrate:legacy -- run-plan \
--config tools/legacy-db-migration/migration-plan.json --mode incremental
pnpm migrate:legacy -- run-plan \
--config tools/legacy-db-migration/migration-plan.json --mode incremental --apply
```
## Full and incremental execution
`run-plan` first connects to every configured MariaDB and PostgreSQL target and
checks that the checkpoint migrations exist. Only after all stages pass does it
@@ -51,6 +143,8 @@ The source of truth for eligibility is the checked ref schema, not every table
that happens to exist in a dump. Tables outside that schema remain only in the
recovery dump.
## Data mapping
### Gateway
| Legacy table | Target | Policy |
@@ -198,6 +292,29 @@ excluded. In particular, `general`, `city`, `nation`, their turn queues,
`ng_betting`, `reserved_open`, `select_pool`, `select_npc_token` and `plock`
must not be used to reconstruct a running season.
## Commands
```sh
LEGACY_ROOT_DATABASE_URL=... pnpm --filter @sammo-ts/legacy-db-migration migrate gateway
LEGACY_GAME_DATABASE_URL=... pnpm --filter @sammo-ts/legacy-db-migration migrate game --profile che
```
After reviewing the JSON counts and excluded-table reasons, add
`GATEWAY_DATABASE_URL` or `GAME_DATABASE_URL` and repeat with `--apply`.
For game archives, `GAME_DATABASE_URL` points at that profile's Core schema.
The importer writes completed-history data to the shared
`legacy_archive` PostgreSQL schema and writes only inheritance projections to
the selected current profile schema. Accepted profiles are
`che,kwe,pwe,twe,nya,pya,hwe`; run them separately against the same PostgreSQL
database.
The individual commands also accept `--mode incremental` and `--source-key`.
Use the ordered plan for production so every configured connection is checked
before the Gateway stage starts. The direct `gateway` command intentionally
fails closed when its source contains custom icons because it has no secure
structured icon-upload configuration; use `run-plan` for that source.
### Current-season comparison fixture
The archive exclusion above remains the production migration contract. The
@@ -222,6 +339,25 @@ through `CURRENT_SEASON_CAPTURE_USER_ID` and
`CURRENT_SEASON_CAPTURE_SOURCE_OWNER`. This mode must not be used for a live
season or as a substitute for the long-lived archive cutover procedure.
Start from a cloned Core database whose scenario, year and month already match
the Ref source. Dry-run verifies that contract and reports the planned counts:
```sh
LEGACY_GAME_DATABASE_URL=... GAME_DATABASE_URL=... \
pnpm --filter @sammo-ts/legacy-db-migration migrate current-season-fixture \
--profile hwe --expected-scenario 2601 --expected-year 186 --expected-month 1
```
Applying requires both destructive flags so an ordinary archive command cannot
replace a running season accidentally:
```sh
LEGACY_GAME_DATABASE_URL=... GAME_DATABASE_URL=... \
pnpm --filter @sammo-ts/legacy-db-migration migrate current-season-fixture \
--profile hwe --expected-scenario 2601 --expected-year 186 --expected-month 1 \
--replace-current-season --apply
```
## Archived play read model
`/past-plays` is an authenticated, read-only projection. The server derives the
+13 -15
View File
@@ -1,28 +1,26 @@
# 플레이 감사 사용과 DB 적용
플레이 감사는 각 프로필의 `/play-audit`에서 읽는다. Gateway 관리자 조치 원장과
다른 기능이다. 현재 전달은 [전체 설계](design/play-audit.md)의 부분 구현이며
구현·검증 source는 [inventory](design/play-audit-implementation.md)에 기록한다.
이번 전달 대상은 sam.hided.net의 전체 프로필이다. 코드와 migration을 main에 push하고,
운영자는 각 프로필에 직접 DB 보존 업데이트를 적용한다. 이 문서의 검증 결과는 실제
운영 DB에 이미 적용되었다는 뜻이 아니다.
다른 기능이다. 구조·수집 경계·남은 요구사항은 [설계와 구현 기준](design/play-audit.md)에서
관리한다. 아래 적용 절차는 각 프로필의 DB 보존 업데이트를 위한 것이며,
문서에 기능이 기재되어 있다고 운영 DB에 적용된 것은 아니다.
## 지금 사용할 수 있는 기능
| 화면 | 사용할 수 있는 정보 | 읽을 때 주의할 점 |
| -------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 국가 | 월말 금·쌀·기술력, 세율·실제 정산, 유저/NPC/부대장 NPC별 자원·숙련 집계, 월/6개월 그래프 | 보유량은 마지막 월말, 수입/지급액은 기간 합. 부분 기간/미수집은 0과 다름 |
| 장수 | 이름 부분 검색·장수 번호 정렬, 모든 국가·재야의 현재/월말 장수, 자원·능력·숙련·병력·훈련·사기·장비·특기·위치, 독립 로그 상세 | 현재 예약은 현재 조회에서만 제공. 과거 월말은 그달 모든 명령의 이력이 아님 |
| 도시 | 현재/월말 소유·내정 상태와 국가별 주둔 장수 상세 연결 | 월말 주둔은 그달 모든 방문자가 아님. 지도 기반 탐색은 미완성 |
| 외교 | 국가쌍·기간별 문서 제안/승인/철회/파기, 즉시 합의와 월간·명령 관계 전이, 생성/소멸 관계 | 문서 내용과 실제 관계는 별개. 최초 관측 이전 사건은 복원하지 않음 |
| NPC 결정 | 장수별 개인·수뇌 판단, 절차 시도/차단, 관측한 RNG 결과와 선택·실제 실행·대체 시도 | 새 실행부터 수집하며 후보 내부 조건 전체는 미완성 |
| 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면은 아직 없음. NPC 결정에서 저장된 당시 버전을 직접 조회 가능 |
| 화면 | 사용할 수 있는 정보 | 읽을 때 주의할 점 |
| -------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 국가 | 월말 금·쌀·기술력, 세율·실제 정산, 유저/NPC/부대장 NPC별 자원·숙련 집계, 월/6개월 그래프 | 보유량은 마지막 월말, 수입/지급액은 기간 합. 부분 기간/미수집은 0과 다름 |
| 장수 | 이름 부분 검색·장수 번호/수치 정렬, 모든 국가·재야의 현재/월말 장수, 자원·능력·숙련·병력·훈련·사기·장비·특기·위치, 독립 로그 상세 | 현재 예약은 현재 조회에서만 제공. 과거 월말은 그달 모든 명령의 이력이 아님 |
| 도시 | 현재/월말 소유·내정 상태와 국가별 주둔 장수 상세 연결 | 월말 주둔은 그달 모든 방문자가 아님. 지도는 현재 좌표에 당시 소유를 표시 |
| 외교 | 국가쌍·기간별 문서 제안/승인/철회/파기, 즉시 합의와 월간·명령 관계 전이, 생성/소멸 관계 | 문서 내용과 실제 관계는 별개. 최초 관측 이전 사건은 복원하지 않음 |
| NPC 결정 | 장수별 개인·수뇌 판단, 절차 시도/차단, 관측한 RNG 결과와 선택·실제 실행·대체 시도 | 새 실행부터 수집하며 후보 내부 조건 전체는 미완성 |
| 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면은 아직 없음. NPC 결정에서 저장된 당시 버전을 직접 조회 가능 |
목록/그래프와 선택 상세를 분리해 읽는다. 표는 기본50건이며 더 보기를 명시적으로
누른다. 현재 상태는 수동으로 조회하며 백그라운드 polling은 하지 않는다. 필터·월·선택
대상은 URL에 남으므로 같은 권한으로 직접 열기/새로고침할 수 있다.
장수 이름은 선택 시점의 이름으로 부분 검색하며 영문 대소문자를 구분한다. 검색어는
64자까지이고 `%`·`_`도 문자 그대로 찾는다. 장수 번호 정렬은 오름차순/내림차순을 제공한다.
64자까지이고 `%`·`_`도 문자 그대로 찾는다. 장수 번호와 자원·능력·병력·숙련 등 수치 정렬은 오름차순/내림차순을 제공한다.
정책 버전과 외교 사건 상세의 **요청 처리 조회**는 연결된 요청의 현재 상태와 처리 시도
횟수, 접수·처리 시각/tick, 결과·오류 기록 존재 여부를 보여준다. 조회 시각 기준 정보이며
@@ -143,7 +141,7 @@ flush batch로 감사의 시간 제한을 보존한다. 브라우저는 같은 D
- 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진
버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다.
- 일부 국가 생성/소멸 사건은 actor/request가 null이다. 원인을 현재 주체로 추정하지 않는다.
- 자원·능력별 정렬·지도 탐색·모든 전투 지표/연결과 전체 COST gate는 남아 있다.
- 모든 전투 지표/연결과 전체 COST gate는 남아 있다.
- 격리 PostgreSQL/Redis 및 mock API를 쓰는 실제 Chromium 검증은 운영 HTTPS 검증과 다르다.
후속 NPC/행위/계정 저장소의 필드·순서·보존·인덱스 요구는 설계의 R5/조사 A~F와 비용