/* * ★ **답의 모양이 세 곳에서 같아야 한다** — 어댑터가 답한 칸이 중간에서 사라지지 않게. * * ── 무엇이 있었나 (2026-09-04, 인티그레이션 레인이 찾음) ──────────────────── * 조치의 답이 세 타입을 지난다. * * ReferenceAdapter.actuate 반환 커넥터가 만든다 * ActuationAck routeActuation 이 되돌린다 * CommandAck dispatch 가 받아 표에 적는다 * * 가운데가 `ok`·`ref`·`error` 셋만 들고 있었다. 그래서 **타입은 `note`·`noteNext`·`retry` 를 * 버린다고 말했다.** 값은 흘렀다 — JS 객체가 키를 안 버리므로. 실제로 `dispatch_note` 에 앉았다. * * 그런데 **커넥터를 만드는 사람이 그 칸이 있다는 것을 알 방법이 없었다.** 이 저장소에서 하루 * 종일 「자리는 있고 길이 없다」로 부른 것의 타입 버전이다 — 동작은 하고 계약이 안 알려 준다. * * ── 왜 원문을 읽나 ────────────────────────────────────────────────────────── * 타입은 런타임에 없다. 그래서 「세 모양이 같은가」를 값으로 잴 수 없고, 원문에서 읽는 것이 * 지금 할 수 있는 최선이다. 이 검사가 막으려는 것은 **한 곳에만 칸을 더하는 일**이다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' /** 주석 속 낱말이 세어지지 않게 — 오늘 두 번 그 함정에 걸렸다. */ const bare = (p: string): string => readFileSync(new URL(p, import.meta.url), 'utf8') .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\/\/[^\n]*/g, '') /** 답이 반드시 나를 칸 — 하나라도 중간에서 사라지면 커넥터가 적은 것이 버려진다. */ const CARRIED = ['note', 'noteNext', 'retry'] as const const SOURCES = { '어댑터 actuate 반환': '../server/service/reference/reference-adapter.ts', ActuationAck: '../server/service/reference/actuation-routing.ts', CommandAck: '../server/service/actuation/command-dispatcher.ts' } as const test('★ 세 타입이 같은 칸을 든다 — 한 곳에만 더하면 값이 중간에서 버려진다', () => { const missing: string[] = [] for (const [label, path] of Object.entries(SOURCES)) { const src = bare(path) for (const field of CARRIED) { /* `field?:` 로 선언된 자리를 찾는다 — 이름만 스치는 것은 세지 않는다. */ if (!new RegExp(`\\b${field}\\?:`).test(src)) missing.push(`${label} 에 ${field} 없음`) } } assert.deepEqual( missing, [], '조치의 답이 지나는 세 타입 중 한 곳이 칸을 버린다. 값은 흐르지만 커넥터를 만드는 사람은\n' + '그 칸이 있다는 것을 알 수 없다:\n ' + missing.join('\n ') ) }) test('★ 갈래 셋은 한 곳에서 정의되고 나머지는 그것을 가리킨다 — 반복하면 갈라진다', () => { /* * `again`·`after-fix`·`never` 는 어댑터가 고르고 일꾼이 판정한다. 세 곳에 낱말을 각각 적으면 * 한쪽만 늘어나는 날 다른 쪽이 **모르는 값을 「말하지 않은 것」으로 읽어 조용히 집지 않는다.** * * 처음에 이 검사를 「세 곳에 낱말이 다 있는가」로 뒀다. 그것이 곧 **반복을 요구하는 검사**였고, * 반복이 바로 갈라짐의 원인이다. 그래서 규칙을 바꿨다: 정의는 한 곳, 나머지는 그 이름을 가리킨다. */ const outlooks = ['again', 'after-fix', 'never'] as const /* 정의하는 자리 — 셋을 다 안다. */ const plan = bare('../server/service/actuation/retry-plan.ts') assert.match(plan, /export type RetryOutlook/, '갈래를 정의하는 자리가 없다') for (const o of outlooks) assert.ok(plan.includes(`'${o}'`), `정의가 갈래 '${o}' 를 모른다`) /* 선언하는 자리는 낱말을 다 적거나 그 이름을 가리켜야 한다. */ const loose: string[] = [] for (const [label, path] of Object.entries(SOURCES)) { const src = bare(path) if (!/\bretry\?:/.test(src)) continue const namesType = /retry\?:\s*RetryOutlook/.test(src) const listsAll = outlooks.every(o => src.includes(`'${o}'`)) if (!namesType && !listsAll) loose.push(`${label} 이 갈래를 좁히지 않는다`) } assert.deepEqual( loose, [], '갈래를 문자열로 열어 두면 커넥터가 모르는 낱말을 보내도 타입이 통과시킨다:\n ' + loose.join('\n ') ) }) test('★ 이 검사가 실제로 무언가를 잰다 — 원문을 못 읽으면 빨강', () => { for (const [label, path] of Object.entries(SOURCES)) { const src = bare(path) assert.ok(src.length > 1000, `${label} 원문이 ${src.length}자다 — 경로가 틀렸다`) } })