/* * ★ 자리 타입을 **누구에게 묻는가** — 원본이 선언했으면 그 선언이 정본이다. * * ── 무엇이 틀렸나 (2026-08-20) ────────────────────────────────────────────── * 호스트는 「이 자리 타입을 아는가」를 **내장 카탈로그**에게만 물었다(`DOMAIN_CATALOG[system]`). * 그런데 원본은 자기 `DomainDefinition.locationTypes` 를 함께 보낼 수 있고, 그러면 **그 선언이 * 정본**이다 — 카탈로그는 우리가 아는 도메인들의 기본 어휘일 뿐이다. * * 상대가 틀렸으니 결과가 이랬다: 자기 어휘를 선언한 원본(예: F&B 라인 — 배합·가열·세척·금속검출· * 소분·포장)은 **정상적인 자리마다 경고를 하나씩** 받는다. 경고가 소음이 되면 사람은 그것을 읽지 않게 * 되고, 그때 **진짜 경고도 함께 죽는다.** * * ── 지키는 것 ─────────────────────────────────────────────────────────────── * ① 선언이 있으면 선언에 대조한다 ② 선언이 없으면 카탈로그로 떨어진다 ③ 선언을 **못 읽은 것**과 * 「아무것도 선언하지 않은 것」을 가른다(못 읽었으면 카탈로그로 떨어진다 — 빈 목록으로 읽으면 모든 * 자리가 경고를 받는다) ④ 아무 데도 없는 타입은 여전히 경고한다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { createRequire } from 'node:module' const req = createRequire(import.meta.url) const { masterToTwin, declaredLocationTypes } = req('../dist-server/service/reference/reference-master.js') /** F&B 라인 — 내장 MES 카탈로그(트레일러 제조)에는 없는 어휘. */ const FNB = ['mixing', 'cooking', 'washing', 'metal-detect', 'portioning', 'packing'] const CATALOG = { mes: { locationTypes: ['raw-store', 'cut-station', 'fg-store'] } } const master = (types: string[], productionSpec?: unknown) => ({ source: 'chef-1', system: 'mes', siteName: '식품 공장', /* `masterToTwin` 이 공간에서 spaceId 를 꺼낸다 — 최소 모양을 준다. */ space: { spaceId: 'chef-site', width: 60, depth: 30, unit: 'm' }, locations: types.map((t, i) => ({ id: `loc-${i}`, type: t, role: 'location' as const })), equipment: [], ...(productionSpec ? { productionSpec } : {}) }) const specWith = (keys: string[]) => ({ definition: { locationTypes: keys.map(k => ({ key: k, label: k })) } }) const unknownWarnings = (r: any) => (r.warnings ?? []).filter((w: any) => w.code === 'unknown-location-type') test('원본이 선언했으면 그 선언에 대조한다 — 정상적인 자리에 경고를 주지 않는다', () => { const r = masterToTwin(master(FNB, specWith(FNB)), CATALOG) assert.deepEqual(unknownWarnings(r), [], 'F&B 여섯 자리 모두 선언돼 있다') }) test('선언이 없으면 카탈로그로 떨어진다 — 예전 거동 그대로', () => { const r = masterToTwin(master(['raw-store', 'mixing']), CATALOG) const w = unknownWarnings(r) assert.equal(w.length, 1, '카탈로그에 없는 하나만') assert.equal(w[0].params.type, 'mixing') }) test('선언에도 카탈로그에도 없으면 경고한다 — 진짜 경고는 죽이지 않는다', () => { const r = masterToTwin(master([...FNB, 'ghost-station'], specWith(FNB)), CATALOG) const w = unknownWarnings(r) assert.equal(w.length, 1) assert.equal(w[0].params.type, 'ghost-station') }) test('선언을 못 읽으면 카탈로그로 떨어진다 — 빈 목록으로 읽지 않는다', () => { /* * 「없는 것」과 「못 읽은 것」은 다르다. 못 읽은 것을 빈 목록으로 읽으면 **모든 자리가 경고**를 받고, * 그것이 바로 이 시험이 막으려는 소음이다. */ for (const broken of [{}, { definition: {} }, { definition: { locationTypes: [] } }, { definition: { locationTypes: 'nope' } }]) { assert.equal(declaredLocationTypes(broken), undefined, `못 읽은 모양: ${JSON.stringify(broken)}`) } const r = masterToTwin(master(['raw-store', 'mixing'], { definition: { locationTypes: [] } }), CATALOG) assert.equal(unknownWarnings(r).length, 1, '카탈로그 기준으로 하나만 경고한다') }) test('선언 목록의 모양을 너그럽게 읽는다 — 문자열 목록도 받는다', () => { assert.deepEqual(declaredLocationTypes({ definition: { locationTypes: ['mixing', 'packing'] } }), ['mixing', 'packing']) assert.deepEqual(declaredLocationTypes({ definition: { locationTypes: [{ key: 'mixing' }, { nope: 1 }] } }), ['mixing']) }) test('카탈로그도 선언도 없으면 아무 경고도 하지 않는다 — 모르는 것을 문제로 만들지 않는다', () => { const r = masterToTwin(master(FNB), undefined) assert.deepEqual(unknownWarnings(r), [], '견줄 상대가 없으면 판단하지 않는다') })