/* * **조치를 어디로 보낼 수 있나** — 판단만. 저장소도 어댑터 호출도 여기 없다. * * 배선(§`actuation-routing.ts`)에서 갈라 두는 이유는 규칙 쪽과 같다(§`rule-evaluate.ts`). 이 파일을 * 부르는 시험이 TypeORM 데코레이터를 지나면 시험이 서지 않고, 그러면 **거절 판정을 아무 시험도 걷지 * 않는다.** 조치 경로에서 그것은 승인이 쌓이는데 현장에 아무것도 안 가는 상태로 나타난다. */ import type { ReferenceCapability } from './reference-adapter.js' /** 판단에 필요한 것만 — 엔티티 전체를 받지 않는다. */ export interface ActuationTargetInput { /** 어느 트윈인가 — **거절 문장이 이것을 말한다.** 「모르는 트윈입니다」만으로는 찾을 수 없다. */ instanceId: string /** 트윈이 있나. 없으면 `undefined`. */ twinExists: boolean /** 이 트윈이 어느 사이트에서 왔나 — `TwinInstance.origin.siteId`. */ siteId?: string /** 연결된 원본. 없으면 `undefined`(시뮬레이션으로만 세운 트윈). */ ref?: { source: string; adapterType: string } /** 커넥터가 등록되어 있나. */ adapterRegistered: boolean /** * 그 커넥터가 실제로 낼 수 있는 능력 — `capabilitiesOf` 가 판정한 값. * * **낼 수 있나는 이 값만 본다.** 아래 둘은 못 낼 때 무엇을 고쳐야 하는지 말하기 위한 것이고, * 판정을 다시 만들지 않는다. */ capabilities: readonly ReferenceCapability[] /** 커넥터가 `capabilities` 에 조치를 적었나. */ declaresActuate: boolean /** 커넥터에 `actuate` 함수가 있나. */ hasActuateImpl: boolean } /** 보낼 수 있다 — 어디로까지 정해진 상태. */ export interface ActuationAllowed { ok: true siteId: string adapterType: string source: string } /** 보낼 수 없다 — 왜인지 코드와 문장으로. */ export interface ActuationRefused { ok: false code: ActuationRefusal detail: string } export type ActuationTarget = ActuationAllowed | ActuationRefused /** * 보낼 수 있나 — **타입 가드로 둔다.** * * 이 저장소는 기본 설정이 `strict: false` 이고(`packages/tsconfig-base.json`), 그 상태에서는 `ok` 가 * `true`·`false` 로 갈린 유니온이 `if (!t.ok)` 로 좁혀지지 않는다. 부르는 쪽마다 캐스팅을 넣으면 그 * 캐스팅이 언젠가 틀린 쪽을 가리킨다. 판정을 여기 한 자리에 둔다. */ export function isActuationAllowed(target: ActuationTarget): target is ActuationAllowed { return target.ok === true } /** * 왜 못 보내나 — **코드로 답한다.** * * 화면이 이 코드로 다음 할 일을 가른다. 문장만 주면 화면은 그것을 그대로 띄우는 것밖에 못 하고, * 사람은 연결 설정을 열어야 하는지 커넥터를 고쳐야 하는지 모른다. */ export type ActuationRefusal = | 'unknown-twin' | 'no-source' /* 원본이 없는 트윈 — 보낼 곳이 없다 */ | 'no-adapter' /* 커넥터가 등록되지 않았다 */ | 'not-declared' /* 구현은 있는데 선언이 없다 — 한 줄 넣으면 된다 */ | 'not-implemented' /* 선언은 있는데 구현이 없다 — 만들다 만 커넥터다 */ | 'not-supported' /* 선언도 구현도 없다 — 이 커넥터는 관측만 낸다 */ | 'no-site' /* 어느 사이트로 낼지 모른다 */ /** * 보낼 수 있나 — 보낼 수 있으면 **어디로**까지 답한다. * * 순서가 뜻을 갖는다. 앞의 것이 없으면 뒤를 물을 수 없다 — 원본을 모르면 능력을 물을 데가 없다. */ export function actuationTarget(input: ActuationTargetInput): ActuationTarget { if (!input.twinExists) { return { ok: false, code: 'unknown-twin', detail: `모르는 트윈입니다 (${input.instanceId})` } } if (!input.ref) { /* * 시뮬레이션으로만 세운 트윈이 이 경우다. 규칙 경로가 시뮬 트윈을 이미 막지만 * (§`rule-runner.ts`), 사람이 손으로 낸 조치는 그 문을 지나지 않는다. 막는 지점이 두 경로에 * 각각 있는 것이고, 같은 판정을 두 벌 만든 것이 아니다. */ return { ok: false, code: 'no-source', detail: `트윈 "${input.instanceId}" 에는 연결된 원본이 없습니다 — 보낼 곳이 없습니다` } } if (!input.adapterRegistered) { return { ok: false, code: 'no-adapter', detail: `등록되지 않은 커넥터입니다 (${input.ref.adapterType})` } } if (!input.capabilities.includes('actuate')) { /* * **왜 못 내는지를 셋으로 가른다** — 고칠 것이 셋 다 다르다. * * 선언 없음 · 구현 있음 한 줄 넣으면 된다 * 선언 있음 · 구현 없음 만들다 만 커넥터다. 등록 검사는 이쪽을 말하지 않는다 * 선언도 구현도 없음 이 커넥터는 조치를 받지 않는다 * * 뭉개면 한 줄로 끝날 일에 커넥터를 다시 만들려 들거나, 반대로 「관측만 냅니다」를 믿고 조치를 * 포기한다. 가운데 것을 「관측만 냅니다」로 답하는 것은 **사실이 아니다** — 그 사람은 조치를 * 하겠다고 적어 두었다. */ const { adapterType, source } = input.ref if (input.hasActuateImpl && !input.declaresActuate) { return { ok: false, code: 'not-declared', detail: `커넥터 "${adapterType}" 가 조치를 보내는 구현은 있는데 능력으로 선언하지 않았습니다 — ` + "connector 의 capabilities 에 'actuate' 를 넣어야 합니다" } } if (input.declaresActuate && !input.hasActuateImpl) { return { ok: false, code: 'not-implemented', detail: `커넥터 "${adapterType}" 가 조치를 보낸다고 선언했는데 그 구현이 없습니다 — ` + 'actuate 를 만들거나 선언을 지워야 합니다' } } return { ok: false, code: 'not-supported', detail: `원본 "${source}" (${adapterType}) 는 조치를 받지 않습니다 — 이 커넥터는 관측만 냅니다` } } /* * 사이트를 **지어내지 않는다.** 원본 하나가 여러 사이트를 낼 수 있다(`discoverSites`). * * 구동(§`control-routing.ts`)은 사이트를 몰라도 넘긴다 — 시늉을 멈추는 일에는 대상이 하나다. * 조치는 안 된다. 어느 공장에 낼지 모르는 작업지시를 첫 번째 사이트로 보내면 **잘못된 공장이 * 실제로 만든다.** */ const siteId = String(input.siteId ?? '').trim() if (!siteId) { return { ok: false, code: 'no-site', detail: `트윈 "${input.instanceId}" 이 어느 사이트에서 왔는지 알 수 없습니다 — 조치를 보낼 대상을 정할 수 없습니다` } } return { ok: true, siteId, adapterType: input.ref.adapterType, source: input.ref.source } } /** 승인은 남는다는 것을 함께 말한다 — 원인을 고친 뒤 다시 넘길 수 있다. */ export const APPROVAL_STANDS = '승인은 그대로 남아 있습니다 — 고친 뒤 다시 넘길 수 있습니다'