/* * **조치가 어떻게 됐나** — 승인한 사람에게 돌려줄 사실 한 묶음. * * ── 왜 판정을 커널이 하나 (2026-09-04) ───────────────────────────────────── * 「저쪽에서 사람이 할 일이 남았나」의 답이 한 필드에 없다. `dispatchNote` 만 보면 틀린다 — * 그 문장은 **넘길 때 저쪽이 한 말**이고, 사람이 나중에 지시서를 골라도 그대로 남는다. 저쪽에 * 다시 물을 일이 없으니 갱신되지 않는다(인티그레이션 레인이 MES 코드를 읽어 찾았다: 다시 보내면 * 지금 상태를 답하는데, `acked` 가 종착이라 다시 보낼 일이 없다). * * 그러니 답은 **「그 문장이 있고 효과가 아직 없을 때」**다. 그 겹침을 화면마다 하게 두면 목록· * 상세·알림이 각자 겹치고, 한 곳이 빠진 날 그 화면만 다른 말을 한다. 오늘 설비 등급 소속에서 * 같은 이유로 합침을 계약 한 자리에 뒀다. * * ── 커널은 저쪽 말을 해석하지 않는다 ─────────────────────────────────────── * `effectStatus` 는 원천이 보낸 낱말 그대로다(§`QualityDelta` 와 같은 규율). 여기서 보는 것은 * **그 값이 있는가**뿐이고, `released` 인지 `closed` 인지는 도메인의 뜻이라 커널이 가르지 않는다. */ import type { StoredCommand } from './command-dispatcher.js' /** 승인한 사람에게 돌려줄 것 — 화면이 겹쳐 볼 것이 없게 판정까지 낸다. */ export interface ActuationOutcome { id: string /** proposed · approved · dispatching · acked · failed · rejected. */ state: string /** 저쪽이 준 이름 — 나갔을 때만. 없으면 무엇을 되돌릴지 지목할 수 없다. */ dispatchRef?: string /** 못 나간 사유 — 저쪽 문장 그대로. */ error?: string /** 저쪽이 받으면서 덧붙인 말 — **실패가 아니다.** 원인 쪽 문장. */ note?: string /** * 그 말 중 **다음에 할 일** 한 줄. * * 왜 둘로 오나: 읽는 사람은 「그래서 내가 뭘 해야 하나」를 먼저 찾는다(UX 레인). 한 문장으로 * 주면 화면이 자르게 되고, **자르는 규칙이 화면마다 생긴다.** 어댑터가 이미 둘로 알고 있으니 * 붙이지 않고 그대로 나른다. * * 어댑터가 아직 갈라 주지 않으면 **비어 있다.** 그때 `note` 는 두 문장이 붙은 한 덩이이고, * 화면은 그것을 통째로 그려야 한다 — 파싱해서 나누면 어댑터가 갈라 주는 날 조용히 깨진다. */ noteNext?: string /** 원천이 보고한 상태 — 낱말 그대로. */ effectStatus?: string /** 그 보고를 본 시각. */ effectAt?: string /** * 되돌릴 수 있나 — 화면은 **참일 때만** 한 줄 낸다. * * 지금 규칙에서 결재함에 올라온 조치는 거의 다 되돌릴 수 없다(되돌릴 수 없는 것만 승인을 * 요구한다). 그래서 「되돌릴 수 없습니다」를 매번 적으면 늘 참인 경고가 되고, 늘 참인 경고는 * 사흘이면 안 읽힌다 — 그러면 그 자리에 다른 말이 필요한 날 자리가 이미 죽어 있다(UX 레인 판단). */ reversible?: boolean /** * **저쪽에서 사람이 할 일이 남았나** — 이 판정이 이 타입의 존재 이유다(머리말). * * 「모른다」가 있다: 아직 넘기지 않았으면 물어볼 것이 없다. `false` 로 접으면 화면이 「할 일 * 없음」으로 그리고, 그것은 넘긴 뒤와 구별되지 않는다. */ pendingAtSource?: boolean /** 넘길 수 있는 상태인가 — `acked` 는 종착이라 저쪽이 준비돼 있어도 다시 넘기지 않는다. */ dispatchable: boolean } /** 넘김을 시도해 본 상태인가 — 이 뒤에야 「저쪽에 할 일이 남았나」를 물을 수 있다. */ const TRIED = new Set(['dispatching', 'acked', 'failed']) /** * 저장된 조치 하나를 사람이 읽을 사실로 옮긴다 — **순수하다.** * * DB 도 어댑터도 보지 않는다. 그래서 이 판정은 시험이 값만 바꿔 가며 확인할 수 있고, 화면이 * 같은 규칙을 다시 쓰지 않아도 된다. */ export function actuationOutcomeOf(command: StoredCommand, dispatchable: boolean): ActuationOutcome { const note = command.dispatchNote const effect = command.effect /* * 넘겨 보지 않았으면 판정하지 않는다. 넘긴 뒤라면 「말이 있는데 효과가 아직 없다」가 곧 * 「저쪽에서 사람이 할 일이 남았다」다. */ const pendingAtSource = TRIED.has(command.state) ? Boolean(note) && !effect?.status : undefined return { id: command.id, state: command.state, ...(command.dispatchRef ? { dispatchRef: command.dispatchRef } : {}), ...(command.error ? { error: command.error } : {}), ...(note ? { note } : {}), ...(command.dispatchNoteNext ? { noteNext: command.dispatchNoteNext } : {}), ...(effect?.status ? { effectStatus: effect.status } : {}), ...(effect?.at ? { effectAt: effect.at } : {}), ...(command.reversible === undefined ? {} : { reversible: command.reversible }), ...(pendingAtSource === undefined ? {} : { pendingAtSource }), dispatchable } }