docs: design general command differential testing
This commit is contained in:
@@ -0,0 +1,720 @@
|
||||
# 일반 장수 명령 차등 테스트 설계
|
||||
|
||||
## 상태
|
||||
|
||||
- 문서 상태: 구현 전 승인 가능한 설계
|
||||
- 비교 기준: `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 통과만으로 일반 장수 명령
|
||||
전체를 `확인` 또는 이식 완료로 표시하지 않는다.
|
||||
@@ -78,6 +78,10 @@ Goal: verify state input -> state output and that flush behaves as expected.
|
||||
- Integration tests: execute the same command and confirm DB persistence.
|
||||
- Mock target: InMemory Repository (Fake).
|
||||
- "Send to DB" behavior is validated via real DB tests.
|
||||
- Cross-engine compatibility compares a canonical semantic snapshot rather than
|
||||
raw MariaDB/PostgreSQL dumps. General-turn commands use the three-way
|
||||
ref DB ↔ core InMemory ↔ core PostgreSQL design in
|
||||
[`architecture/general-command-differential-testing.md`](./architecture/general-command-differential-testing.md).
|
||||
|
||||
### 3) Turn Flow Tests
|
||||
|
||||
|
||||
Reference in New Issue
Block a user