# 일반 장수 명령 차등 테스트 설계 ## 상태 - 문서 상태: 구현 전 승인 가능한 설계 - 비교 기준: `ref/sam`의 `ng_compare` 브랜치 - 대상: 휴식과 `cr_건국`을 포함한 일반 장수 예약 명령 55개 - 범위: 명령 결과, RNG 소비, 로그, 예약 턴 lifecycle, DB 영속화 이 문서는 테스트 구현 자체가 아니다. 아래의 파일, 실행기, 격리 스택과 fixture가 구현되고 완료 기준을 통과하기 전까지 55개 명령의 동적 호환 상태를 `확인`으로 올리지 않는다. ## 결정 요약 일반 장수 명령은 하나의 canonical fixture로 다음 세 결과를 만든다. 1. ref PHP가 격리된 MariaDB에서 실제 예약 턴을 실행한 결과 2. core2026이 `InMemoryTurnWorld`에서 같은 예약 턴을 실행한 결과 3. 2의 dirty state를 격리된 PostgreSQL에 flush하고 다시 읽은 결과 두 비교를 모두 통과해야 한다. ```text canonical fixture │ ├── ref fixture adapter ──> PHP full turn ──> MariaDB ──> ref projection │ │ └── core fixture adapter ─> InMemory full turn ──────────┼─ exact semantic diff │ │ └── DB hooks ─> PostgreSQL ─┘ ``` - `ref projection == core memory projection`은 호환 로직을 검증한다. - `core memory projection == core persisted projection`은 loader/flush를 검증한다. - raw MariaDB dump와 raw PostgreSQL dump는 비교하지 않는다. 테이블 구조와 필드 소유권이 다르므로 의미 필드로 정규화한 JSON을 비교한다. - 명령 결과와 RNG는 원칙적으로 exact 비교한다. 저장 레이아웃 차이만 projection에서 제거한다. ## 목표와 비목표 ### 목표 - 동일한 게임 상태, 명령, 인자, 시각과 seed로 ref/core를 실행한다. - 성공, 실행 중 실패, 제약 실패, alternative, 다중 턴과 전처리 경로를 구분한다. - 장수·도시·국가·부대·외교·예약 큐·rank·로그 등 모든 관찰 가능한 side effect를 비교한다. - RNG의 domain, 호출 순서, 연산, 인자와 결과를 비교한다. - core 메모리 결과가 실제 PostgreSQL에 같은 의미로 저장되고 재시작 후 같은 상태로 로드되는지 검증한다. - 명령이 예상 밖의 raw DB 필드를 바꾸면 projection 누락으로 실패시킨다. - 각 명령의 테스트 근거와 아직 없는 경로를 기계적으로 집계한다. ### 비목표 - MariaDB와 PostgreSQL의 물리 schema, sequence, index 또는 내부 row ID를 동일하게 만드는 일 - 레거시 제품 브랜치 `devel`에 비교 endpoint를 추가하는 일 - 운영/개발 DB를 초기화하거나 기존 ref DB를 fixture 저장소로 사용하는 일 - 현재 구현을 정답으로 삼아 snapshot을 자동 승인하는 일 - UI와 API 요청 형식 검증을 이 suite 하나로 대체하는 일 ## 테스트 계층 ### 1. core logic regression 기존 `InMemoryTurnWorld`와 예약 턴 handler를 사용한다. 빠른 테스트이며 fixture의 core memory projection을 단언한다. DB 제약이나 직렬화는 보장하지 않는다. ### 2. ref ↔ core differential integration Docker의 ref PHP CLI와 격리 MariaDB를 호출하므로 integration test로 분류한다. 두 엔진의 canonical 결과와 RNG trace를 비교한다. ### 3. core persistence integration 동일한 core 실행 결과를 `databaseHooks`로 격리 PostgreSQL에 flush하고 새 connection으로 다시 load한다. 메모리 projection과 재조회 projection을 비교한다. ### 4. 선택적 daemon system test 예약 API, command queue와 daemon lifecycle까지 필요한 대표 명령만 별도 system test로 둔다. 55개 수치 호환성 검증을 이 느린 계층에 모두 넣지 않는다. ## 제안 파일 구조 구현 시 다음 경계를 사용한다. ```text core2026/ tools/integration-tests/ fixtures/general/ manifest.json base/ scenario-2.json che_화계/ success-basic.json failure-probability.json probability-clamp-max.json injury-and-item.json src/general-command/ fixtureSchema.ts canonicalSchema.ts compareCanonical.ts referenceRunner.ts referenceProjection.ts coreMemoryRunner.ts coreMemoryProjection.ts coreDatabaseRunner.ts coreDatabaseProjection.ts changedPathAudit.ts databaseSandbox.ts tracingRng.ts test/ generalCommandDifferential.test.ts generalCommandPersistence.test.ts generalCommandComparator.test.ts ref/sam/ # ng_compare 전용 hwe/compare/ general_command_trace.php GeneralCommandFixture.php GeneralCommandProjection.php ComparisonTracingRNG.php docker_compose_files/ general-command-differential/ compose.yml .env.example README.md scripts/ prepare-secrets.sh initialize-templates.sh run-fixture.sh verify-isolation.sh secrets/ mariadb_password.example postgres_password.example ``` 기존 `battleDifferential.test.ts`의 workspace 탐색, `docker compose exec`, stdin JSON 전달과 tracing RNG 패턴을 재사용한다. 일반 명령용 코드는 전투 fixture와 섞지 않는다. integration Vitest는 case DB 수명주기를 예측할 수 있도록 `fileParallelism: false`, 기본 `testTimeout: 120_000`을 유지한다. CI sharding은 별도 Compose project와 worker prefix를 받은 프로세스 사이에서만 수행한다. ## 실행 격리 ### 전용 Compose stack `general-command-differential`은 개발, ref UI, input-event E2E와 수명주기가 다르므로 별도 Compose stack으로 둔다. 필수 service: - `ref-db`: 고정 버전 MariaDB, 외부 port 미공개 - `ref-runner`: 기존 ref PHP image, CLI 명령만 허용 - `core-db`: 고정 버전 PostgreSQL, 프로젝트 전용 loopback port - 선택 profile `system`: Redis와 core daemon runner DB data directory는 suite 전용 `tmpfs`를 기본으로 한다. 테스트가 중단되어도 운영·개발 volume을 가리킬 수 없게 Compose project, container, network와 port 이름을 별도로 고정한다. 실제 비밀값은 Git에서 제외된 secret file로만 주입한다. ref의 생성된 `d_setting/DB.php` overlay는 `/run` 또는 `mktemp` 아래에 mode `0600`으로 만들어 container에 read-only mount하고 종료 시 삭제한다. JSON 결과, 명령행과 보고서에는 credential을 넣지 않는다. ### ref는 transaction rollback을 사용하지 않는다 레거시 `general`, `city`, `general_turn`, `general_record`, `rank_data` 등 핵심 테이블은 Aria engine이다. 따라서 transaction rollback은 fixture 격리를 보장하지 못한다. ref 격리는 다음 순서로 수행한다. 1. suite 시작 시 schema와 비교 기준 scenario config로 immutable template DB 두 개(root/HWE)를 만든다. 2. case마다 검증된 prefix `sammo_gc_ref__`의 DB를 새로 만든다. 3. template dump를 case DB에 복원하고 fixture override를 적용한다. 4. case DB를 가리키는 임시 `RootDB.php`/`DB.php` overlay로 PHP CLI를 한 번 실행한다. 5. canonical 결과를 읽은 뒤 case DB를 삭제한다. 6. cleanup 대상 이름이 허용 prefix와 정확히 일치하지 않으면 삭제를 거부한다. 이 흐름은 기존 `test-fast-forward-sandbox.sh`의 DB 복제, DB 이름 override, 원본 DB 불변 검사 패턴을 재사용한다. 기존 `sammo_ref_hwe`는 읽거나 복제 기준으로도 사용하지 않고, suite가 직접 만든 template만 사용한다. ### core DB 격리 suite 전용 PostgreSQL 안에 `public`과 `che` schema 및 migration을 적용한 template database를 만든다. case마다 template에서 새 database를 만들고 case 종료 후 검증된 prefix에 한해 삭제한다. 각 case는 다음을 보장한다. - 새 Prisma connection 사용 - 명시적인 profile/scenario - fixture에 없는 이전 row가 없음 - flush 이후 connection을 닫고 새 connection으로 재조회 - dirty-state acknowledge는 DB commit 이후에만 수행 - 실패한 case도 case database만 정리 현재 `.env.ci`가 가리키는 개발 DB의 `public`/`che` schema를 truncate하는 기존 initialization test는 이 suite의 backend로 사용하지 않는다. ## 기준 scenario와 base state fixture는 양쪽에 공통으로 존재하는 `scenario_2`의 rule, map과 unit set을 사용한다. 전체 scenario의 NPC와 국가를 그대로 seed하면 검색, 정렬과 무작위 후보가 fixture 밖 row에 영향을 받으므로 다음 base를 별도로 만든다. - scenario config, `GameConst`, map과 unit set은 `scenario_2`에서 로드 - map의 모든 도시는 중립 기본 row로 생성 - 장수, 국가, 부대, 외교와 예약 턴은 fixture가 명시한 것만 생성 - game/root env는 명령 생성과 턴 실행에 필요한 key 전체를 명시 - 시간은 UTC ISO 문자열과 게임 연·월을 함께 고정 - 자동 턴은 기본적으로 끄고 fixture가 요구할 때만 켬 - 국가 예약 명령은 기본 휴식으로 고정 - actor만 due 상태로 두고 대상/보조 장수의 turn time은 실행 범위 밖으로 둠 - fixture의 test hidden seed를 임시 `UniqueConst.php` overlay와 core world meta 양쪽에 같은 값으로 주입 base generator가 양쪽 입력을 따로 만들되, 기준 값은 하나의 canonical base JSON에서 가져온다. ## Fixture 계약 fixture는 구현 내부 객체가 아니라 게임 의미를 기술한다. Zod schema와 JSON Schema를 함께 생성하고 resource validation에 포함한다. 개념 예시는 다음과 같다. ```json { "schemaVersion": 1, "id": "che_화계/success-basic", "scenario": "scenario_2", "execution": { "mode": "full-turn", "year": 200, "month": 1, "turnTime": "0200-01-01T00:00:00.000Z", "hiddenSeed": "general-differential-test-seed", "actorGeneralId": 101, "command": { "key": "che_화계", "args": { "destCityId": 2 } }, "autorun": false }, "world": { "generals": [], "cities": [], "nations": [], "troops": [], "diplomacy": [], "generalTurns": [], "nationTurns": [], "rank": [], "events": [], "rootUsers": [] }, "observe": { "generalIds": [101, 201], "cityIds": [1, 2], "nationIds": [1, 2], "metaKeys": ["intel_exp", "firenum", "killturn", "myset", "inherit_lived_month"], "collections": ["logs", "generalTurns", "rank"] }, "expect": { "outcome": "success" }, "evidence": { "legacyFiles": ["hwe/sammo/Command/General/che_화계.php", "hwe/func.php"], "contract": "화계 성공 기본 경로" } } ``` 실제 fixture에는 생략 없이 모든 필수 entity field를 넣는다. `hiddenSeed`는 테스트 전용 공개값이며 운영 seed를 복사하지 않는다. ### Fixture 불변식 - 양쪽에서 같은 numeric ID를 사용한다. - 이름과 정렬 순서에 영향을 주는 문자열도 명시한다. - `undefined`를 사용하지 않는다. 값 없음은 `null`, collection 없음은 `[]`, object 없음은 `{}`로 표현한다. - 자동 증가 DB ID는 fixture의 의미 식별자로 사용하지 않는다. - 확률 결과를 임의 stub으로 강제하지 않는다. seed와 입력 상태로 원하는 분기를 만들고 RNG trace를 고정한다. - fixture의 기대값은 core 실행 결과에서 생성하지 않는다. ref trace와 레거시 코드 근거를 함께 기록한다. - fixture update는 별도 review 대상이며 snapshot 자동 갱신 명령을 제공하지 않는다. ## 실행 모드 ### `full-turn` 기본 모드다. 양쪽의 실제 예약 턴 실행 순서를 거친다. - lived-month 증가 - preprocess trigger, 부상 회복, 병력 군량 - block - 필요 시 국가 명령 - 일반 명령 제약, term stack, cooldown, alternative - 명령별 RNG - queue shift - killturn, myset, autorun limit - next turn time - retirement/deletion - DB flush와 로그 확정 55개 호환 판정은 이 모드의 결과로 한다. ### `command-only` 수식과 RNG 분기를 좁게 진단하는 보조 모드다. 전체 호환 판정의 근거로 단독 사용하지 않는다. full-turn 실패가 preprocess/queue 문제인지 명령 resolver 문제인지 분리할 때 사용한다. ## ref runner 계약 `general_command_trace.php`는 다음 조건을 모두 만족해야 한다. - `PHP_SAPI === 'cli'` - 명시적인 `SAMMO_GENERAL_COMPARE=1` guard - stdin의 fixture 한 개만 처리 - case 전용 DB 이름 외 연결 거부 - fixture seed 후 `TurnExecutionHelper`의 실제 경로 호출 - logger를 flush하고 DB 결과를 읽은 뒤 JSON 한 개 출력 - stdout에는 JSON만, 진단은 stderr - 기존 명령 계산·정렬·RNG·DB mutation 순서를 바꾸지 않음 - 종료 전 원본 template/main DB가 변하지 않았음을 runner가 검사 출력은 engine-specific raw state와 trace를 담는다. canonical 변환은 `referenceProjection.ts`가 수행한다. PHP와 TS projection이 서로의 오류를 그대로 복제하지 않도록 한 구현을 공유하지 않는다. 현재 `TurnExecutionHelper`는 내부에서 RNG를 직접 생성하므로 `ng_compare`에 최소 test seam이 필요하다. 기본값은 기존 `new RandUtil(new LiteHashDRBG(seed))`를 그대로 사용하고, CLI guard가 활성화된 경우에만 같은 DRBG를 tracing proxy로 감싸는 factory를 주입한다. observer on/off에서 동일 fixture의 DB 결과가 같다는 계측 무영향 테스트를 ref에 둔다. ## core runner 계약 ### memory runner - fixture를 `TurnWorldSnapshot`, `TurnWorldState`, `InMemoryReservedTurnStore`로 변환 - production `createReservedTurnHandler`와 `InMemoryTurnProcessor` 사용 - fixture가 선언한 한 장수의 한 due turn만 실행 - 실행 전후 world, dirty state, queue, logs와 RNG trace 반환 `reservedTurnHandler`도 RNG를 내부 생성하므로 production default를 보존하는 선택적 `rngFactory(domain, seed)` test seam을 둔다. 옵션을 생략한 경로는 현재 구현과 byte-for-byte 같은 DRBG를 만들고, 테스트만 tracing wrapper를 반환한다. seed 구성 자체를 runner에서 다시 구현하지 않는다. ### database runner - 같은 fixture를 case PostgreSQL에 seed - production loader로 새 `InMemoryTurnWorld` 생성 - 같은 handler/processor 실행 - production `databaseHooks`로 commit - 모든 connection을 닫고 새 loader/Prisma query로 결과 재조회 - memory projection과 persisted projection 비교 테스트 전용 runner가 `buildCityUpdate` 같은 private production helper를 복제해 직접 호출해서는 안 된다. 반드시 production hook 경계를 지나야 `City.state`/`City.meta.state`와 같은 투영 오류를 검출할 수 있다. ## RNG trace RNG trace entry는 다음 형태다. ```json { "domain": "generalCommand", "sequence": 3, "operation": "nextInt", "arguments": { "maxInclusive": 99 }, "result": 42 } ``` domain은 최소 다음을 구분한다. - `preprocess` - `nationCommand` - `generalCommand` - `uniqueLottery` - 명령이 추가로 분리한 명시적 child domain 비교 규칙: - entry 수 exact - domain과 sequence exact - primitive RNG operation exact - arguments exact - integer/byte/bit result exact - float는 JSON number의 실제 값 exact PHP/JavaScript의 동일 계산 결과가 표현 차이만 보이는 경우에도 RNG trace 허용치를 넓히지 않는다. 게임 상태 수치에 불가피한 부동소수점 차이가 있으면 필드별 compatibility rule을 근거와 함께 별도 등록한다. ## Canonical snapshot 결과 envelope: ```text schemaVersion fixtureId engine ref | core-memory | core-db execution requestedCommand resolvedCommand outcome success | command-failure | constraint-denied | fallback | error blockedReason nextTurnTime before after delta rng unmappedChanges ``` `before`와 `after`는 다음 collection을 ID/복합 key로 정렬한다. ### 일반 상태 - world: year, month, tick/turn term, killturn 설정과 명령이 읽거나 바꾼 env - generals: scalar stats, 소속, 관직, 자원, 병력, 부상, 장비, 특기, 성격, 능력 경험, turn time, lastTurn - general meta: killturn, myset, autorun/cooldown, 계승, command별 변경 key - item inventory: instance ID 자체보다 item key, slot, charges와 values - rank: `(generalId, type, value)` ### 도시·국가·관계 - cities: 소속, state, 인구와 최대치, 내정치와 최대치, 수비·성벽, 보급, 전선, trust, trade, region, conflict - nations: 수도, 군주, 규모, 자원, 기술, power, type과 명령 관련 meta - troops: leader/id, nation, name와 membership에 의해 바뀐 장수 troopId - diplomacy: 양방향 row를 `(srcNationId, destNationId)`로 정렬하고 state, term, dead/showing과 의미 meta 비교 ### side-effect collection - generalTurns, nationTurns: logical key, action, args와 순서 - logs: scope, category, subtype, year, month, 대상 ID와 최종 formatting text - messages - events - hall/archive rows - inheritance point/log/result - access-log 변경 - 생성·삭제된 entity ID DB auto ID, `createdAt`, `updatedAt`, connection별 sequence와 물리 JSON key 순서는 제거한다. JSON object key는 정렬하지만 array 순서는 보존한다. core memory log는 production `finalizeLogEntry`를 같은 고정 year/month/time context로 통과시킨 뒤 persisted/ref log와 비교한다. ## 명령 seed 밖의 난수 사료 NPC 랜덤임관의 PHP 전역 `shuffle()`처럼 `RandUtil` 밖의 난수는 명령 seed만으로 재현할 수 없다. 이 값을 무시하거나 fixture에서 해당 분기를 제외하지 않는다. 비교 환경에서는 외부 비결정값을 명시적 input tape로 취급한다. ```json { "externalDecisions": [ { "domain": "legacyGlobalShuffle", "input": [201, 202, 203], "output": [203, 201, 202] } ] } ``` - ref `ng_compare`는 CLI guard 아래에서만 해당 shuffle 호출을 작은 wrapper로 통과시키고 tape의 permutation을 사용한다. - core runner도 같은 permutation을 입력으로 사용한다. - input/output 원소가 정확한 permutation이 아니면 실패한다. - guard가 꺼진 wrapper는 PHP builtin `shuffle()`을 그대로 한 번 호출한다. - 계측 on/off의 deterministic 경로 무영향 테스트와, guard-off shuffle의 permutation property test를 별도로 둔다. - canonical trace에는 external decision의 소비 순서도 포함한다. 따라서 이 경로의 호환 의미는 “같은 외부 shuffle 결과가 주어졌을 때 이후 후보 평가, `RandUtil` 소비와 선택 결과가 같다”이다. PHP 전역 RNG 자체를 core command seed와 같다고 주장하지 않는다. ## 변경 경로 감사 fixture가 명시한 필드만 비교하면 예상하지 못한 side effect를 놓칠 수 있다. 각 runner는 engine raw state의 before/after diff도 만든다. - 알려진 raw path는 canonical mapping registry에 연결한다. - 명령이 바꾼 raw path가 canonical path나 명시적 ignore rule에 연결되지 않으면 `unmappedChanges`에 넣고 실패한다. - ignore rule은 timestamp, auto ID처럼 게임 의미가 없는 필드만 허용한다. - ignore entry에는 engine, raw path pattern, 이유와 근거 파일을 기록한다. - broad wildcard로 `aux`, `meta` 또는 전체 table을 무시하지 않는다. 이 gate는 새로운 aux/meta key나 누락된 persistence table을 조용히 통과시키지 않기 위한 것이다. ## 비교 규칙 기본은 deep exact equality다. 정규화 허용: - snake_case ↔ camelCase - `intel` ↔ `intelligence` - ref scalar/aux/rank ↔ core typed field/meta/rank row - `None` ↔ `null`인 장비 없음 표현 - DB가 부여한 ID와 timestamp 제거 - JSON object key 정렬 정규화 금지: - 반올림, truncation 또는 clamp 결과 변경 - RNG 호출 추가/삭제/재정렬 - collection 정렬로 실제 처리 순서 은폐 - 로그 문구나 조사 차이를 임의로 제거 - 누락 row를 기본값으로 만들어 일치시킴 - 도시 `state`와 `meta.state`처럼 소유권이 다른 필드를 같은 값으로 간주 필드별 허용 차이는 `compatibility-rules.json`에 다음을 반드시 기록한다. - fixture 또는 command - canonical path - 허용 조건 - 레거시/core 근거 파일 - 사용자 상태와 이후 턴에 영향이 없는 이유 - 제거 예정 여부 ## 화계 첫 acceptance matrix 설계 검증의 첫 명령은 `che_화계`로 한다. 최소 fixture: | fixture | 보호할 계약 | | ------------------------ | ------------------------------------------------------------------------------------- | | `success-basic` | 비용, 성공, 농업·상업 피해, state 32, 경험·공헌·지력 경험·firenum, queue/LastTurn/log | | `failure-probability` | 실패 RNG, 피해·부상·아이템 소비 없음, 실패 경험/공헌 범위 | | `probability-clamp-zero` | 음수 계산 결과 0 clamp와 RNG 소비 | | `probability-clamp-max` | 0.5 상한, 거리 나눗셈 순서 | | `defence-population` | 대상국 장수만 포함, 최대 지력, 인원 log2, 보급·치안 보정 | | `injury-and-item` | 장수별 부상 판정/상한 80, crew/train/atmos 0.98 절삭, 일회용 아이템 소비 | | `damage-clamp` | 낮은 농업·상업에서 0 미만 방지 | | `constraint-denied` | 중립/같은 도시/자원/보급/불가침 제약과 queue fallback | `success-basic`은 현재의 `City.meta.state`와 `City.state` 혼동을 반드시 실패로 검출해야 한다. 이 fixture가 해당 production line을 고의로 잘못 바꿨을 때 실패하고 복구하면 통과하는 것을 mutation audit에 기록한다. ## 55개 명령 coverage manifest `manifest.json`은 PHP command inventory와 TS registry의 합집합을 기준으로 생성 검증한다. 각 명령에는 다음 case class가 필요하다. - 실행 가능한 기본 경로 - full constraint 거부 - 확률 분기가 있으면 성공과 실패 - 값 clamp/상한/하한이 있으면 경계 - 다중 턴이면 stack 중간과 완료/레거시 reset - alternative가 있으면 원 명령과 대체 명령 - 생성/삭제/소속 변경이 있으면 관련 collection - 아이템/특기/국가/관직 보정이 있으면 최소 한 개 - 명령별 외부 side effect가 있으면 해당 collection 적용 불가능한 case class는 `notApplicable`과 레거시 근거를 기록한다. fixture가 하나 있다는 이유만으로 명령을 covered로 세지 않는다. inventory gate가 검증할 집계: - ref command 수 - core command 수 - command별 필수 case class 충족 - fixture schema validation - orphan fixture - skip/todo/notApplicable 근거 - unmapped changed path 수 ## 실패 출력과 artifact 실패 시 다음만 출력한다. - fixture ID와 세 engine - 첫 canonical mismatch path와 양쪽 값 - 전체 RNG trace에서 최초 divergence와 전후 제한된 window - unmapped raw changed path - 재현 명령 전체 DB dump, credential, 운영 hidden seed, 사용자 개인정보는 출력하거나 artifact로 저장하지 않는다. 필요하면 canonical JSON만 Git 제외된 `artifacts/general-command//`에 저장하고 민감 필드를 검사한다. ## 실행 명령 계약 구현 후 제공할 명령: ```bash # stack 준비와 격리 검증 pnpm general-diff:prepare pnpm general-diff:verify-isolation # 화계 한 fixture pnpm general-diff:test --fixture che_화계/success-basic # 한 명령 전체 pnpm general-diff:test --command che_화계 # 55개 coverage 및 전체 differential/persistence pnpm general-diff:check ``` 명령은 기존 개발 DB를 발견하거나 전용 stack marker가 없으면 실행을 거부한다. `general-diff:check`는 skip이 있으면 실패한다. 진단용 `--allow-skip`은 CI와 완료 판정에서 금지한다. ## CI 단계 ### PR fast gate - fixture/schema/manifest validation - comparator unit test - 변경된 명령과 공통 실행기 영향 명령의 differential - 해당 fixture의 core persistence ### compatibility gate - 55개 manifest의 모든 필수 case - ref/core RNG 및 canonical state exact diff - core memory/persisted exact diff - unmapped changes 0 - skip 0 명령별 case를 shard할 수 있지만 같은 case DB를 공유하지 않는다. ## Comparator 자체 검증 `generalCommandComparator.test.ts`는 실제 production 결과 없이도 다음 synthetic mismatch를 각각 검출해야 한다. - 숫자 1 차이 - null과 누락 - array 순서 - RNG operation/result - 로그 대상과 format text - queue shift 방향 - 생성 대신 수정 - 삭제 누락 - `City.state`와 `meta.state` - 알려지지 않은 aux/meta/raw DB 변경 protected behavior를 고의로 perturb한 mutation audit를 fixture review에 포함한다. 단순히 현재 구현을 snapshot으로 저장하고 다시 읽는 테스트는 호환 근거로 인정하지 않는다. ## 구현 순서 1. canonical fixture/schema와 comparator unit test 2. 전용 Compose stack과 원본 DB 불변 isolation test 3. ref `general_command_trace.php`를 `ng_compare`에 추가 4. core memory runner 5. core DB runner와 새 connection reload 6. 화계 acceptance matrix 완성 및 현재 city state 결함 수정 7. 계략 4종으로 공통 runner 검증 8. side-effect family별 대표 명령 확장 9. 55개 manifest 완성 10. project-generated Prisma client를 사용하는 전용 connection readiness check 추가 11. `pnpm general-diff:check`를 compatibility gate로 등록 ref 계측 commit, core test/infra commit, 제품 버그 수정 commit은 분리한다. ## 완료 기준 다음이 모두 증명되어야 이 설계의 구현을 완료로 본다. - 전용 stack이 기존 ref/dev DB를 바꾸지 않는 isolation test 통과 - ref runner가 CLI/test guard 밖에서 접근 불가 - 화계 8개 acceptance fixture 통과 - 화계 `state=32`가 core DB 재조회에도 유지됨 - 각 fixture의 ref/core RNG trace exact 일치 - 외부 비결정 경로는 input tape 소비와 이후 결과 exact 일치 - 55개 명령의 필수 manifest case 충족 - ref/core memory canonical diff 0 - core memory/persisted canonical diff 0 - unmapped changed path 0 - skip/todo 0 - comparator mutation audit 통과 - 관련 typecheck, lint, build와 integration test 통과 - `docs/ref-core2026-mapping.md`에서 동적 검증 근거와 미확인 항목 갱신 - 실행 명령, 기준 commit과 fixture 목록을 `report/`에 기록 이 조건 전에는 정적 제약·로그 검사와 smoke test 통과만으로 일반 장수 명령 전체를 `확인` 또는 이식 완료로 표시하지 않는다.