# 일반 장수 명령 차등 테스트 설계

## 상태

- 문서 상태: 구현 전 승인 가능한 설계
- 비교 기준: `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_<worker>_<caseHash>`의 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/<fixture>/`에 저장하고 민감 필드를 검사한다.

## 실행 명령 계약

구현 후 제공할 명령:

```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 통과만으로 일반 장수 명령
전체를 `확인` 또는 이식 완료로 표시하지 않는다.
