/* * 커맨드 라우팅 판정 — **순수**. "이 커맨드를 누구에게 보낼 것인가" 만 정한다. * 실제 dispatch·저널 기록은 엔진이 한다(`design/plans/command-routing.md` §1·§4 P0). * * ── 왜 떼어냈나 ───────────────────────────────────────────────────────────── * 예전에는 리졸버가 `inst.runtime.dispatch(...)` 를 곧바로 불렀다. `runtime` 은 **시뮬 경로에서만** * 만들어지므로 실물을 비추는 트윈(mirror)에서는 그 한 줄이 `TypeError` 로 터졌다 — 조치 채널이 * 통째로 닿지 않았고, 사용자에게는 이유 있는 거절이 아니라 그냥 실패였다. * * 판정이 코드 한가운데 있으면 이런 부류를 테스트로 못 잡는다(엔진은 DB·TypeORM 을 물고 있어 단위 * 테스트가 불러올 수 없다). 그래서 판정만 순수하게 떼어 둔다 — `planWarmStart` 와 같은 이유다. * * ── 판정 규칙 = 권위 × 성격 ───────────────────────────────────────────────── * | 성격 \ 모드 | 시뮬 | 미러(현실을 비춤) | * | 액추에이션 | 커널 런타임 | **거절**(아웃바운드 어댑터 미결선) | * | 주석(확인 ack) | 커널 런타임 | 관측 커널 | * * 미러에서 액추에이션을 그냥 허용하면 **새 거짓말이 생긴다**: 트윈은 "보류됨" 이라 말하고 실 시스템은 * 모른다. 알리지 않고 커널만 바꾸지 않고, 이유를 밝혀 거절한다. * * 반면 확인(ack)은 액추에이션이 아니다 — 신호를 봤다는 트윈 쪽 주석이라 현실을 건드리지 않는다. * 미러에서도 성립해야 하므로 관측 커널로 보낸다. */ /** 관측 쪽 주석 커맨드 — 현실을 바꾸지 않으므로 미러에서도 통과한다. */ const ANNOTATION_COMMANDS = new Set(['attention.ack']) /** 라우팅에 필요한 인스턴스 사실만 — 엔진 객체를 그대로 받지 않는다(순수하게 유지). */ export interface RoutableInstance { /** 'live' 면 현실을 비추는 트윈(미러). 그 외는 시뮬. */ mode?: string /** 커널 런타임(tick/dispatch)이 붙어 있나 — 시뮬 경로에서만 만들어진다. */ hasRuntime: boolean /** 관측 커널이 dispatch 를 받을 수 있나. */ hasKernelDispatch: boolean } export type CommandRoute = /** 커널 런타임으로(시뮬 — 커널이 세계다). */ | { target: 'runtime' } /** 관측 커널로(미러 — 주석만). 방출된 사실은 저널 큐에 실어야 한다. */ | { target: 'kernel' } /** 보내지 않는다. 사유는 언어 중립 코드 — 화면이 `twin.cmderr.` 로 사람 말을 만든다. */ | { target: 'reject'; errorCode: string; errorParams?: Record } /** 이 커맨드가 실 프로세스를 바꾸는 것인가(= 액추에이션). */ export const isActuation = (type: string): boolean => !ANNOTATION_COMMANDS.has(type) /** * 어디로 보낼지 정한다. * * `inst` 가 없거나 호출자 도메인 소유가 아니면 **같은 사유로** 거절한다 — 존재 여부를 알려 주면 * 다른 테넌트의 인스턴스 id 를 추측할 수 있다(있음/없음이 구별되면 그것이 정보다). */ export function routeCommand(inst: RoutableInstance | undefined, ownedByCaller: boolean, type: string): CommandRoute { if (!inst || !ownedByCaller) return { target: 'reject', errorCode: 'no-instance' } if (!type) return { target: 'reject', errorCode: 'unknown-command' } if (inst.mode !== 'live') { /* 시뮬 — 커널이 세계다. 런타임이 없으면 오류를 내지 않고 이유를 말한다. */ return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' } } /* 미러 — 실 프로세스 변경은 아웃바운드 어댑터의 몫이고 아직 결선되지 않았다. */ if (isActuation(type)) return { target: 'reject', errorCode: 'actuation-not-wired', errorParams: { type } } return inst.hasKernelDispatch ? { target: 'kernel' } : { target: 'reject', errorCode: 'no-runtime' } }