/* * 조치 왕복의 결과를 레퍼런스에 어떻게 적을 것인가 — **순수하게.** * * `actuation-routing.ts` 에 두면 시험이 그 파일을 들여야 하고, 그 파일은 엔티티를 값으로 들인다 * (데코레이터가 붙은 클래스). 가벼운 모듈로 떼어 두면 판단만 따로 확인할 수 있다. * * 여기서 들이는 것은 **타입뿐**이다 — 런타임에 남지 않는다. */ import type { ActuationFailure, ActuationState } from './twin-reference.js' import type { ActuationResult } from './reference-adapter.js' /** 레퍼런스에 적을 칸들. `actuationVerifiedAt` 이 없는 것은 「건드리지 않는다」는 뜻이다. */ export interface ActuationOutcomePatch { actuationState: ActuationState actuationVerifiedAt?: string actuationFailure: ActuationFailure | null } /** * 결과 → 적을 것. * * ── 세 가지를 지킨다 ─────────────────────────────────────────────────────── * **받아들여지면 지난 거절을 지운다.** 남겨 두면 화면이 「됐는데 실패 사유가 있다」를 그린다. * * **거절은 성공 시각을 건드리지 않는다.** 「전에 한 번 닿았다」와 「이번에 안 됐다」는 둘 다 사실이고, * 지우면 앞엣것이 사라진다 — 사람이 「한 번이라도 닿은 적 있나」를 물을 자리가 없어진다. * * **코드가 없으면 사유를 비운다.** 자리를 채우려고 `error` 문장을 옮기지 않는다. `lastError` 가 이미 * 겪었다 — 저장된 사람 문장은 번역도 집계도 안 되고, 커넥터가 그 문장을 고치는 날 옛 행과 새 행이 * 서로 다른 말을 한다. 모르는 것은 안 적는다. */ export function actuationOutcomePatch(result: ActuationResult, at: string): ActuationOutcomePatch { if (result.ok) { return { actuationState: 'verified', actuationVerifiedAt: at, actuationFailure: null } } return { actuationState: 'failed', actuationFailure: result.errorCode ? { code: result.errorCode, at, ...(result.errorParams ? { params: result.errorParams } : {}) } : null } } /** * 조치가 가는 주소를 이루는 칸들 — 이 중 하나라도 바뀌면 지난 왕복은 **다른 주소의 이야기**다. * * `intakeSiteId` 가 없으면 `siteId` 가 그 자리를 대신한다(부딪힘 판정과 같은 규칙이다 — * `connection-portability.ts`). 비밀값을 함께 보는 이유는, 주소가 같아도 저쪽이 우리를 못 알아보면 * 그 왕복이 다시 성립하지 않기 때문이다. */ const ACTUATION_ADDRESS = ['intakeEndpoint', 'intakeSiteId', 'siteId', 'intakeSecret'] as const const addressOf = (cfg: Record | null | undefined) => ACTUATION_ADDRESS.map(k => String((cfg as any)?.[k] ?? '').trim().replace(/\/$/, '')).join('\u0000') /** * 저장이 조치의 주소를 바꾸나 — **바꿨으면 검증은 저장 시각부터 다시 없다**(ADR-0046 ②). * * 2026-09-18 실측: 칸 셋은 서 있고 왕복의 결과를 적는 자리도 있는데, 주소를 바꿔도 `verified` 가 * 그대로 남았다. 화면은 다른 공장을 가리키는 연결을 「확인됨」으로 그린다 — 그 초록이 가장 위험한 * 거짓이다. * * `unverified` 는 실패가 아니다. 아직 그 주소로 넘겨 본 적이 없다는 사실이고, 사람이 할 일은 한 번 * 넘겨 보는 것이다. */ export function actuationResetPatch( before: Record | null | undefined, after: Record | null | undefined ): { actuationState: ActuationState; actuationVerifiedAt: null; actuationFailure: null } | null { if (addressOf(before) === addressOf(after)) return null return { actuationState: 'unverified', actuationVerifiedAt: null, actuationFailure: null } } /** * 이 연결이 조치를 **보낼 수 있게 적혀 있나** — 주소가 없으면 어디로도 안 간다. * * 검증 상태를 화면에 보일지 가르는 값이다. 읽기만 하는 연결(마스터 동기만 하는 커넥터)에 「아직 * 확인 안 됨」을 그리면, 할 일이 없는 사람에게 할 일이 있는 것처럼 보인다. */ export function actuationAddressed(cfg: Record | null | undefined): boolean { const endpoint = String((cfg as any)?.intakeEndpoint ?? '').trim() const site = String((cfg as any)?.intakeSiteId ?? (cfg as any)?.siteId ?? '').trim() return !!endpoint && !!site }