/* * ★ **어느 레시피로 만들었나 — 짐작하지 않는다.** * * ── 무엇이 있었나 (2026-09-05) ───────────────────────────────────────────── * 계보 진단이 자재로 되짚어 `recipes.find(...)` 로 **먼저 걸리는 하나**를 골랐다. 배열 순서가 답을 * 정한다는 뜻이다. 그리고 한 자재에 레시피가 둘인 경우가 이미 있다 — plant 마스터 실측: * * BOLT-M8 → RCP-BOLT-STD 유효시작 2026-09-01 09:25:29.447 * RCP-BOLT-FAST 유효시작 2026-09-01 09:25:29.510 * * 빠른 방식으로 만든 볼트인데 표준 방식과 견주고 「설계와 다릅니다」가 나온다. 사람은 라인에 * 문제가 있다고 믿고 확인하러 간다. **틀린 답이 확신에 찬 얼굴로 나오는 것**이 그 상태였다. * * ── 답은 적혀 있었다 ─────────────────────────────────────────────────────── * 만들 때 정해지는 사실이고 커널이 오더에 싣는다(`FlowOrder.recipeKey`). 변환 사건은 그 오더를 * 거래 식별자로 가리키고, 계보 함수는 그 값을 이미 꺼내 놓고도 쓰지 않았다. * * 세 자리가 다 있었고 **마지막 연결만 없었다.** * * ── 왜 순수 함수인가 ─────────────────────────────────────────────────────── * 규칙만 따로 재려면 DB 도 트윈도 필요 없어야 한다. 여기서 값으로 못 박아 두면 다음에 「하나쯤 * 골라 주는 게 친절하지 않나」가 올라올 때 그 줄이 빨개진다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { chooseRecipe } from '../server/service/twin-model/choose-recipe.js' const STD = { key: 'RCP-BOLT-STD', outputs: [{ material: 'BOLT-M8' }] } const FAST = { key: 'RCP-BOLT-FAST', outputs: [{ material: 'BOLT-M8' }] } const OTHER = { key: 'RCP-CT-BLANK', outputs: [{ material: 'CT-BLANK' }] } const ALL = [STD, FAST, OTHER] /* ── 오더가 말하면 그것을 쓴다 ───────────────────────────────────────────── */ test('★ 오더가 말한 레시피를 쓴다 — 버전이 둘이어도 고민하지 않는다', () => { const r = chooseRecipe(ALL, [STD, FAST], 'RCP-BOLT-FAST') assert.equal(r.recipe, FAST, '오더가 말했는데 다른 것을 골랐다') assert.equal(r.basis, 'order') }) test('★ 배열 순서가 답을 바꾸지 않는다 — 옛 결함의 핵심이 이것이었다', () => { const forward = chooseRecipe([STD, FAST], [STD, FAST], 'RCP-BOLT-FAST') const reversed = chooseRecipe([FAST, STD], [FAST, STD], 'RCP-BOLT-FAST') assert.equal(forward.recipe, reversed.recipe, '순서를 뒤집으니 답이 달라졌다') assert.equal(forward.recipe?.key, 'RCP-BOLT-FAST') }) /* ── 오더가 말 안 할 때 ─────────────────────────────────────────────────── */ test('★ 후보가 하나면 쓴다 — 고를 것이 없으면 짐작이 아니다', () => { const r = chooseRecipe(ALL, [OTHER], undefined) assert.equal(r.recipe, OTHER) assert.equal(r.basis, 'only-candidate') }) test('★ 후보가 둘 이상이면 답하지 않는다 — 여기가 옛 결함의 자리다', () => { /* * 옛 코드: `recipes.find(...)` — 이 자리에서 STD 를 돌려줬다. 그 답이 틀릴 확률이 절반이고, * 틀렸다는 사실은 아무 데도 안 남았다. */ const r = chooseRecipe(ALL, [STD, FAST], undefined) assert.equal(r.recipe, undefined, '어느 것으로 만들었는지 모르는 채로 하나를 골랐다') assert.equal(r.basis, 'ambiguous') }) /* ── 어긋남과 모름을 가른다 ─────────────────────────────────────────────── */ test('★ 오더가 말한 레시피가 선언에 없으면 「모른다」가 아니라 「어긋났다」', () => { /* * 사람이 할 일이 다르다. 어긋남은 선언이나 원본을 고칠 일이고, 모호함은 그 개체의 기록이 없는 * 일이다. 한 낱말로 합치면 화면이 둘 중 어느 쪽인지 말할 수 없다. */ const r = chooseRecipe(ALL, [STD, FAST], 'RCP-사라진것') assert.equal(r.recipe, undefined) assert.equal(r.basis, 'said-but-undeclared') }) test('★ 후보가 아예 없으면 none — 「이 자재를 내는 레시피가 없다」', () => { const r = chooseRecipe(ALL, [], undefined) assert.equal(r.recipe, undefined) assert.equal(r.basis, 'none') }) /* ── 무너지지 않는다 ────────────────────────────────────────────────────── */ test('빈 목록·빈 값에도 터지지 않는다', () => { assert.equal(chooseRecipe([], [], undefined).basis, 'none') assert.equal(chooseRecipe([], [], 'X').basis, 'said-but-undeclared') assert.equal(chooseRecipe(ALL, [STD], '').basis, 'only-candidate', '빈 문자열은 「말했다」가 아니다') }) /* ── 검사가 실제로 무언가를 재나 ─────────────────────────────────────────── */ test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => { /* 네 판정이 다 나오는지. 하나라도 안 나오면 위 검사들이 같은 가지만 밟고 있는 것이다. */ const bases = new Set([ chooseRecipe(ALL, [STD, FAST], 'RCP-BOLT-FAST').basis, chooseRecipe(ALL, [OTHER], undefined).basis, chooseRecipe(ALL, [STD, FAST], undefined).basis, chooseRecipe(ALL, [STD], 'RCP-없음').basis, chooseRecipe(ALL, [], undefined).basis ]) assert.deepEqual( [...bases].sort(), ['ambiguous', 'none', 'only-candidate', 'order', 'said-but-undeclared'], `판정이 다 나오지 않는다: ${[...bases].join(' · ')}` ) })