/* * ★ 단계 판정 — **체크리스트가 아니라 증거로 센다.** * * 이 테스트가 지키는 것: * ① **「모른다」는 달성이 아니다.** 계측되지 않은 신호(`null`)를 0 으로 대체하면 「반출 0건」과 * 「반출을 세지 않았다」가 같아진다. 배선하지 않은 계측으로 단계를 주장할 길을 남기지 않는다. * ② **반출을 세었는데 없으면 라이브가 아니다.** 되풀어 읽는 스냅샷은 원리적으로 반출을 못 낸다 — * 이 판별식이 폴링을 「실시간」이라 부르는 것을 막는다. * ③ **선언이 비면 예측이 아니다.** 커널 기본값 위에 선 숫자도 화면에서는 예측처럼 보인다. * ④ **앞을 건너뛸 수 없다.** 뒤 단계는 자기 조건을 따지지 않고 `prerequisite` 으로 막는다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { assessStagePath, forbiddenClaims, type StageEvidence } from '../server/engine/stage-path.ts' /** 전부 달성한 증거 — 각 시험은 여기서 한 축만 무너뜨린다(무엇이 판정을 바꿨는지 분명해진다). */ const full = (): StageEvidence => ({ locationCount: 12, itemCount: 340, masterWarnings: 3, liveIngestTotal: 1200, removalsObserved: 17, forecastRan: true, specCoverage: { declared: 6, total: 8 }, actuateCapability: true, correlatedEffectObserved: 2 }) test('전부 증거가 있으면 폐루프까지 도달하고 금지 표현이 없다', () => { const p = assessStagePath(full()) assert.deepEqual(p.stages.map(s => s.achieved), [true, true, true, true]) assert.equal(p.reached, 'closed-loop') assert.equal(p.next, null) assert.deepEqual(forbiddenClaims(p), []) }) test('못 옮긴 값을 세지 않았으면 구조도 달성이 아니다 — 0 이라고 우기지 않는다', () => { const p = assessStagePath({ ...full(), masterWarnings: null }) const s1 = p.stages[0] assert.equal(s1.achieved, false) assert.deepEqual(s1.unmeasured, ['master-warnings']) assert.deepEqual(s1.blockedBy, [], '자리와 물품은 있으므로 막힌 것이 아니라 모르는 것이다') assert.equal(p.reached, null, '첫 칸을 못 밟았으면 서 있는 칸이 없다') }) test('못 옮긴 값이 3건이어도 구조는 달성이다 — 요구는 「없다」가 아니라 「말할 수 있다」', () => { assert.equal(assessStagePath({ ...full(), masterWarnings: 3 }).stages[0].achieved, true) assert.equal(assessStagePath({ ...full(), masterWarnings: 0 }).stages[0].achieved, true) }) test('반출을 세었는데 0 이면 라이브가 아니다 — 스냅샷 되풀어 읽기의 징표', () => { const p = assessStagePath({ ...full(), removalsObserved: 0 }) assert.equal(p.stages[1].achieved, false) assert.deepEqual(p.stages[1].blockedBy, ['no-removal-observed']) assert.equal(p.reached, 'structure') assert.ok(forbiddenClaims(p).includes('claim.realtime'), '실시간이라고 말할 수 없다') }) test('반출이 계측되지 않으면 「없다」가 아니라 「모른다」로 막힌다', () => { const p = assessStagePath({ ...full(), removalsObserved: null }) assert.deepEqual(p.stages[1].unmeasured, ['removals']) assert.deepEqual(p.stages[1].blockedBy, [], '인제스트는 있으므로 막힌 것이 아니다') }) test('선언이 비면 예측이 아니다 — 기본값 위에 선 숫자도 숫자로 보인다', () => { const p = assessStagePath({ ...full(), specCoverage: { declared: 0, total: 8 } }) assert.equal(p.stages[2].achieved, false) assert.deepEqual(p.stages[2].blockedBy, ['declarations-empty']) assert.equal(p.reached, 'live') assert.ok(forbiddenClaims(p).includes('claim.prediction')) }) test('접수만으로는 폐루프가 아니다 — 효과가 관측으로 돌아와야 닫힌다', () => { const p = assessStagePath({ ...full(), correlatedEffectObserved: 0 }) assert.deepEqual(p.stages[3].blockedBy, ['effect-not-observed']) assert.equal(p.reached, 'forecast') }) test('앞이 막히면 뒤는 자기 조건을 따지지 않는다 — 순서를 건너뛸 수 없다', () => { const p = assessStagePath({ ...full(), locationCount: 0, itemCount: 0 }) assert.deepEqual(p.stages[0].blockedBy, ['no-locations', 'no-items']) for (const later of p.stages.slice(1)) assert.deepEqual(later.blockedBy, ['prerequisite']) assert.deepEqual(p.next, { stage: 'structure', blockedBy: ['no-locations', 'no-items'], unmeasured: [] }) }) test('다음 걸음은 목록이 아니라 하나다', () => { const p = assessStagePath({ ...full(), removalsObserved: 0, specCoverage: null, actuateCapability: false }) assert.equal(p.next?.stage, 'live', '뒤쪽 결손을 함께 들이밀지 않는다') }) test('비직렬 수량 물품만 있어도 구조는 달성이다 — 식품·화학은 개수로 오지 않는다', () => { /* itemCount 는 「자리에 놓인 물품 수」이고, 비직렬 수량 물품(subLot)도 한 줄로 센다. */ assert.equal(assessStagePath({ ...full(), itemCount: 1 }).stages[0].achieved, true) })