From cd6ded497074ea16c54a1124cb79531bf322fd1f Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 29 Sep 2026 09:08:28 +0000 Subject: [PATCH] =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=A9=ED=95=98=EA=B3=A0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=EB=90=9C=20=EA=B5=AC=ED=98=84=20=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=EC=9D=84=20=EC=A0=95=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vitepress/config.mts | 1 - docs/design/play-audit-implementation.md | 716 ------------------ docs/design/play-audit.md | 170 +++-- .../game-clock-reconciliation-plan.md | 183 ----- docs/developer/index.md | 68 +- docs/developer/reference-map.md | 47 -- docs/legacy-db-migration.md | 138 +++- docs/play-audit-operations.md | 28 +- tools/legacy-db-migration/README.md | 229 +----- 9 files changed, 285 insertions(+), 1295 deletions(-) delete mode 100644 docs/design/play-audit-implementation.md delete mode 100644 docs/developer/game-clock-reconciliation-plan.md delete mode 100644 docs/developer/reference-map.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 3034cd97..86505ccd 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -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' }, diff --git a/docs/design/play-audit-implementation.md b/docs/design/play-audit-implementation.md deleted file mode 100644 index c58ab6f8..00000000 --- a/docs/design/play-audit-implementation.md +++ /dev/null @@ -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:`는 `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 완료로 해석하지 않는다. diff --git a/docs/design/play-audit.md b/docs/design/play-audit.md index bb6226cf..7e251c15 100644 --- a/docs/design/play-audit.md +++ b/docs/design/play-audit.md @@ -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에 남긴다. diff --git a/docs/developer/game-clock-reconciliation-plan.md b/docs/developer/game-clock-reconciliation-plan.md deleted file mode 100644 index 0039581f..00000000 --- a/docs/developer/game-clock-reconciliation-plan.md +++ /dev/null @@ -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. diff --git a/docs/developer/index.md b/docs/developer/index.md index 44c5f2cd..4413128b 100644 --- a/docs/developer/index.md +++ b/docs/developer/index.md @@ -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) | 화면 스타일의 소유권과 배치 계약 | + +기능 설계의 목표, 코드에 있는 구현, 테스트로 확인한 범위, 실제 배포 상태는 서로 +다릅니다. 문서의 “완료”는 함께 적힌 날짜와 검증 범위로 해석하세요. diff --git a/docs/developer/reference-map.md b/docs/developer/reference-map.md deleted file mode 100644 index d6c15154..00000000 --- a/docs/developer/reference-map.md +++ /dev/null @@ -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) | 화면 스타일의 소유권과 배치 계약 | - -기능 설계의 목표, 코드에 있는 구현, 테스트로 확인한 범위, 실제 배포 상태는 서로 -다릅니다. 문서의 “완료”는 함께 적힌 날짜와 검증 범위로 해석하세요. diff --git a/docs/legacy-db-migration.md b/docs/legacy-db-migration.md index 4337b856..89f2add2 100644 --- a/docs/legacy-db-migration.md +++ b/docs/legacy-db-migration.md @@ -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 diff --git a/docs/play-audit-operations.md b/docs/play-audit-operations.md index 780e8aa3..b4233f8c 100644 --- a/docs/play-audit-operations.md +++ b/docs/play-audit-operations.md @@ -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와 비용 diff --git a/tools/legacy-db-migration/README.md b/tools/legacy-db-migration/README.md index 1c8f1464..f4f96b8c 100644 --- a/tools/legacy-db-migration/README.md +++ b/tools/legacy-db-migration/README.md @@ -1,228 +1,7 @@ # Legacy DB migration CLI -This package migrates the long-lived parts of a restored or still-readable ref -MariaDB database into the core2026 PostgreSQL schemas. It is CLI-only; no HTTP -or administrator route invokes it. `run-plan` is the normal operator entrypoint: -it validates every configured connection first, then runs Gateway followed by -the enabled game profiles in the official order. +복원한 Ref MariaDB의 장기보존 자료를 Core PostgreSQL로 옮기는 CLI package입니다. -The default mode is a read-only dry-run. `--apply` is required before any target -write. PostgreSQL advisory locks prevent two applies for the same target. -Gateway writes are transactional. Game archive writes and their completed -`legacy_archive.import_run` record are transactional. Both paths record an -import run and durable per-table checkpoints. Stable legacy keys make completed -or interrupted runs repeatable. - -## 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 `migration-plan.example.json` to the Git-ignored `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 -``` - -Incremental mode refuses to start without checkpoints from a completed full -apply. It also refuses a changed host/database/user identity or a source table -whose maximum ID moved behind its checkpoint. Password rotation does not change -the source fingerprint. - -| Source data | Incremental policy | -| ------------------------------------------ | ----------------------------------------------------------------------- | -| `member_log` | Read only IDs after the committed high-water mark. | -| game archive/event-history tables | Read only IDs after the profile checkpoint. | -| `member`, root `storage`, `system`, bans | Rescan and idempotently upsert because old rows are mutable. | -| game `storage` | Rescan by `(namespace, key)` and refresh a recreated row's source ID. | -| `ng_games` | Rescan because a season row can gain its final winner after creation. | -| preserved `batres.txt` seasons | Hash each season; import new immutable seasons after a full checkpoint. | - -The append policy assumes Ref primary keys are never reused and completed -archive rows are immutable. Incremental mode does not mirror source deletions. -If either assumption is false, take a new reviewed backup and run full mode; -do not edit checkpoint rows by hand. - -Ref may delete and recreate a mutable game-storage tuple with the same -`(namespace, key)` and a new auto-increment ID. That tuple is the durable -identity; the latest source ID is retained only as recovery metadata. Rows for -deleted tuples remain archived because incremental mode does not infer -tombstones. - -The optional file importer reads only immediate -`logs/preserved/_*/batres.txt` regular files. It maps the -profile and season directory to the archived general's `(source_profile, -server_id, general_no)` key, verifies file and season SHA-256 hashes, and stores -the exact text plus line/byte counts. It never follows symlinks and ignores -`batlog*` phase detail, `gen*`, `fight*`, SQLite and operational logs. Full mode -creates the season checkpoints. Incremental mode accepts new seasons but rejects -a changed checkpointed season or changed source identity. Every mode rejects a -disappeared checkpointed season. A reviewed full run atomically replaces a -changed season's archive rows. The preserved trees currently cover only the -old filesystem-log era, so an absent file remains explicitly unavailable. - -## 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. - -### Isolated current-season comparison fixture - -`current-season-fixture` is separate from the long-lived archive migration. It -replaces the running-season tables of an isolated Core test schema with a Ref -MariaDB season so both implementations can be compared from the same persisted -world. Never run it against a production or shared development schema. - -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 -``` - -The importer preserves the Core template's static city geometry and connection -metadata, then imports Ref cities, nations, generals, queues, diplomacy, troops, -ranks, messages, logs, events, markets, yearbook rows, current storage values and -world clock in one PostgreSQL transaction. Ref message target keys are converted -to the typed Core message payload. `CURRENT_SEASON_CAPTURE_USER_ID` may bind one -Ref owner selected by `CURRENT_SEASON_CAPTURE_SOURCE_OWNER` to an existing Core -test account; other positive owners receive deterministic legacy UUIDs. - -Process locks, selection tokens, Redis-owned tournament brackets, legacy annual -aggregate text and diplomatic-letter workflow are deliberately excluded and are -listed in the JSON result. This fixture is evidence for persisted-state and GUI -comparison, not proof that the two engines consume RNG identically after the -next turn. - -Kakao members retain their OAuth ID, email, and OAuth metadata. Only a row with -a non-empty OAuth ID receives `kakao_verified_at`. -`kakao_grace_started_at` is set to the migration time. -The existing `token_valid_until` is copied to `kakao_talk_verified_until` for -Kakao rows so a still-current “send to me” proof remains current after cutover. -Imported 128-hex password hashes are marked for reset. They can be upgraded to -Argon2id after the first successful login only when the DB-external -`GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT` is safely recovered. Otherwise, a verified -Kakao flow requires a new password before session issuance. A non-Kakao account -uses the CLI reset below. Reapplying a dump preserves the target account's -current credential, OAuth, identity, roles, sanctions, consent, and login state. - -Only tables present in the checked ref schemas are eligible. Extra tables found -in a dump, such as an old root `config` table, are left in the recovery dump and -are not silently imported. - -For an isolated test account, put a temporary password in a mode-0600 file and -run: - -```sh -GATEWAY_DATABASE_URL=... pnpm --filter @sammo-ts/legacy-db-migration migrate \ - reset-password --login-id test-user --password-file /secure/path/password --apply -``` - -The password value is never accepted on the command line or printed. +설치·source 복원·설정·dry-run/apply·증분 이관·검증은 +[레거시 DB 이관 매뉴얼](../../docs/legacy-db-migration.md)에서 관리합니다. +설정 예시는 [migration-plan.example.json](./migration-plan.example.json)입니다.