@things-factory/headless-twin 10.1.223 → 10.1.227

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist-server/engine/kpi-query.js +1 -1
  2. package/dist-server/engine/kpi-query.js.map +1 -1
  3. package/dist-server/engine/twin-engine.d.ts +1 -1
  4. package/dist-server/engine/twin-engine.js +1 -1
  5. package/dist-server/engine/twin-engine.js.map +1 -1
  6. package/dist-server/service/actuation/command-dispatcher.d.ts +1 -1
  7. package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
  8. package/dist-server/service/reference/actuation-routing.d.ts +1 -1
  9. package/dist-server/service/reference/actuation-routing.js.map +1 -1
  10. package/dist-server/service/reference/connection-portability.d.ts +1 -1
  11. package/dist-server/service/reference/connection-portability.js +2 -2
  12. package/dist-server/service/reference/connection-portability.js.map +1 -1
  13. package/dist-server/service/reference/hook-contract.js +1 -1
  14. package/dist-server/service/reference/hook-contract.js.map +1 -1
  15. package/dist-server/service/reference/reference-adapter.d.ts +1 -1
  16. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  17. package/dist-server/service/reference/reference-live.js +1 -1
  18. package/dist-server/service/reference/reference-live.js.map +1 -1
  19. package/dist-server/service/reference/reference-progress.js +1 -1
  20. package/dist-server/service/reference/reference-progress.js.map +1 -1
  21. package/dist-server/service/twin-model/choose-recipe.d.ts +1 -1
  22. package/dist-server/service/twin-model/choose-recipe.js +1 -1
  23. package/dist-server/service/twin-model/choose-recipe.js.map +1 -1
  24. package/dist-server/service/twin-model/twin-lineage-query.js +1 -1
  25. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  26. package/package.json +6 -6
  27. package/server/engine/kpi-query.ts +1 -1
  28. package/server/engine/twin-engine.ts +1 -1
  29. package/server/service/actuation/command-dispatcher.ts +1 -1
  30. package/server/service/reference/actuation-routing.ts +1 -1
  31. package/server/service/reference/connection-portability.ts +2 -2
  32. package/server/service/reference/hook-contract.ts +1 -1
  33. package/server/service/reference/reference-adapter.ts +1 -1
  34. package/server/service/reference/reference-live.ts +1 -1
  35. package/server/service/reference/reference-progress.ts +1 -1
  36. package/server/service/twin-model/choose-recipe.ts +1 -1
  37. package/server/service/twin-model/twin-lineage-query.ts +1 -1
  38. package/server/service/twin-subject/twin-subject-backfill.ts +1 -1
  39. package/test/ack-shape-alignment.test.ts +1 -1
  40. package/test/canonical-ingest-vocabularies.test.ts +1 -1
  41. package/test/checkpoint-refuses-empty.test.ts +1 -1
  42. package/test/choose-recipe.test.ts +1 -1
  43. package/test/event-type-split.test.ts +1 -1
  44. package/test/live-cursor-wiring.test.ts +1 -1
  45. package/test/master-to-twin.test.ts +1 -1
  46. package/test/read-failure-visible.test.ts +1 -1
  47. package/tsconfig.shared.tsbuildinfo +1 -1
  48. package/tsconfig.tsbuildinfo +1 -1
@@ -83,7 +83,7 @@ export type StoredCommand = TwinCommand & {
83
83
  *
84
84
  * ── 계약에도 같은 칸을 두었다 ──────────────────────────────────────────
85
85
  * `ops-contract` 의 `TwinCommand.effect` 가 이것이다. 다만 이 저장소는 그 패키지를 레지스트리에서
86
- * 받으므로(`^0.9.0`), 다음 판이 올라올 때까지 여기에 산다 — 위의 `approvalRef`·`causeRef` 와 같다.
86
+ * 받으므로(`^0.9.0`), 다음 버전이 올라올 때까지 여기에 산다 — 위의 `approvalRef`·`causeRef` 와 같다.
87
87
  */
88
88
  effect?: {
89
89
  status: string;
@@ -1 +1 @@
1
- {"version":3,"file":"command-dispatcher.js","sourceRoot":"","sources":["../../../server/service/actuation/command-dispatcher.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **승인을 지난 커맨드만 어댑터에 넘긴다** — 게이트가 서는 자리.\n *\n * ── 왜 이 자리인가 (2026-08-31) ──────────────────────────────────────────\n * 설계가 「기본 = propose-only」를 정해 두었는데 코드에 그 규율이 없었다. 인티그레이션 레인이 어댑터에\n * 조치를 붙이려다 그것을 보고 멈췄다 — 게이트 없이 붙이면 트윈이 사람 승인 없이 현장에 작업지시를\n * 내린다. 작업지시는 취소해도 이미 만든 것이 남는다.\n *\n * ── 확인을 여기서 한 번만 한다 ───────────────────────────────────────────\n * 어댑터는 승인을 확인하지 않는다. 그 확인은 이 파일이 했고 **타입이 그것을 증명한다**\n * (`ApprovedCommand` 는 `approveCommand`·`asApproved` 로만 만들어진다). 어댑터가 또 확인하면 규칙이\n * 두 벌이 되고, 한쪽만 고쳐지는 날이 온다.\n *\n * ── 저장소에 닿는 자리를 밖으로 낸다 ─────────────────────────────────────\n * 훅에서와 같은 이유다(§`HookStore`). 시험이 **재기동을 넣을 수 있어야** 한다 — 승인이 프로세스 안에만\n * 살아 있으면 「승인된다」는 보이지만 「다시 세운 뒤에도 승인이 남아 있나」는 보이지 않는다.\n */\nimport {\n CMD,\n approveCommand,\n asApproved,\n commandFromSpec,\n nextCommandState,\n passThroughReversible,\n requiresApproval,\n type ApprovedCommand,\n type CommandApproval,\n type CommandOrigin,\n type CommandTypeSpec,\n type ISOTime,\n type TwinCommand\n} from '@operato/ops-contract'\n\n/**\n * 조치의 결과 — 어댑터가 돌려준다.\n *\n * 계약에도 같은 이름의 타입이 있는데(`contract.ts:3074`) **다른 것**이다. 그쪽은 커널 자극의 답이고\n * 이쪽은 현장 지시의 답이다. 자극과 조치를 가른 것과 같은 갈림이다(§`propose` 의 거절).\n */\nimport type { RetryOutlook } from './retry-plan.js'\n\nexport interface CommandAck {\n ok: boolean\n /** 저쪽 문서 · 작업 식별자. 되돌릴 때 무엇을 되돌릴지 말하려면 이것이 있어야 한다. */\n ref?: string\n /**\n * **받아들였으나 저쪽이 덧붙인 말** — 실패가 아니고, 「끝났다」도 아니다.\n *\n * ── 왜 필요한가 (2026-09-02 실측) ──────────────────────────────────────\n * MES 에 조치 둘을 넘겼더니 트윈 쪽 두 행이 구별되지 않았다.\n *\n * WO-20260902-004 지시서 붙음 acked · ref 있음 · error 없음\n * WO-20260902-005 지시서 못 붙음 acked · ref 있음 · error 없음 ← 같다\n *\n * 005 는 사람이 MES 에서 지시서를 골라야 현장에 간다. 커넥터는 그 사실을 받았는데 **적을 칸이\n * 없어서 로그로만** 남겼다. `error` 에 적으면 실패로 읽혀 커맨드가 `failed` 로 앉는다.\n *\n * **해석하지 않고 그대로 적는다.** 무엇이 남았는지는 저쪽 어휘이고, 커널이 그 방언을 알면 보편\n * 계약이 깨진다 — 효과의 상태를 그대로 적는 것과 같은 규율(§`effect-match.ts`).\n */\n note?: string\n /**\n * 그 말 중 **다음에 할 일** 한 줄 — 원인은 `note` 다.\n *\n * 왜 둘인가: 읽는 사람은 「그래서 내가 뭘 해야 하나」를 먼저 찾는다(UX 레인). 한 문장으로 주면\n * 화면이 자르게 되고, **자르는 규칙이 화면마다 생긴다.** 어댑터는 이미 둘로 알고 있으니 붙이지\n * 않고 따로 준다.\n */\n noteNext?: string\n error?: string\n /**\n * **다시 해서 될 일인가** — 실패에만 답한다(§`RetryOutlook`).\n *\n * again 그대로 다시 해 볼 만하다 못 닿았거나 저쪽이 잠깐 흔들렸다\n * after-fix 사람이 고친 뒤 그대로 나간다 설정 문제\n * never 이 조치로는 영원히 안 된다 지시 내용이 틀렸다\n *\n * **문장에 담지 않는 이유**: 일꾼이 그것을 쓰려면 문장을 파싱해야 하고, 번역되면 깨진다.\n * `code` 를 따로 둔 것과 같은 이유다.\n *\n * 비어 있으면 **말하지 않은 것**이고 `never` 가 아니다. 그때 일꾼은 집지 않고 목록에 남긴다 —\n * 모르는 것을 `never` 로 접으면 고칠 수 있는 것을 사람이 포기한다.\n *\n * 타입을 좁게 둔다 — **어댑터가 답하는 값**이고, 여기서 넓히면 커넥터가 모르는 낱말을 보내도\n * 타입이 통과시킨다. 표에서 읽는 쪽은 `string` 이다(DB 는 무엇이든 들 수 있으므로\n * `isRetryOutlook` 으로 걸러 읽는다).\n */\n retry?: RetryOutlook\n}\n\n/**\n * 저장소에서 되세운 커맨드 — 계약의 커맨드에 **호스트가 아는 한 칸**이 붙는다.\n *\n * `approvalRef` 를 계약에 넣지 않는다. 계약은 운영 도메인의 어휘(EPCIS·ISA-95)를 들고, 「승인 기록이\n * 어느 업무 시스템의 몇 번 문서인가」는 이 저장소의 배선이다. 계약에 넣으면 커널을 쓰는 모든 소비처가\n * 우리 업무 모듈의 존재를 알게 된다.\n */\nexport type StoredCommand = TwinCommand & {\n approvalRef?: string\n proposedBy?: string\n /** 규칙이 낸 것이면 그 근거가 된 관측(`Attention.id`) — 중복을 막는 열쇠다. */\n causeRef?: string\n ruleRef?: string\n /**\n * 저쪽에서 실제로 무엇이 되었나 — **상태가 아니라 별개 사실이다.**\n *\n * `acked` 는 「저쪽이 답했다」이고 종착 상태다. 여기에 「현장에 나갔다」를 상태로 이어 붙이면\n * `acked` 가 종착이 아니게 되고, 되돌아오는 관측이 없는 커넥터의 조치가 **영원히 끝나지 않은\n * 것으로 보인다.** 그런 커넥터가 정상이다 — 관측을 되돌려 주는 능력은 커넥터의 성질이지 조치의\n * 수명이 아니다.\n *\n * `status` 는 **해석하지 않고 그대로** 적는다(§`effect-match.ts`). 오더 상태 어휘는 도메인\n * 소유이고, 어느 값이 「현장에 나갔다」인지는 현장마다 다르다.\n *\n * 이 칸이 비어 있는 것은 「효과가 없었다」가 아니라 **「모른다」**다. 넘기다 닿지 못한 조치는\n * 식별자를 못 받으므로 이을 길이 아예 없다.\n *\n * ── 계약에도 같은 칸을 두었다 ──────────────────────────────────────────\n * `ops-contract` 의 `TwinCommand.effect` 가 이것이다. 다만 이 저장소는 그 패키지를 레지스트리에서\n * 받으므로(`^0.9.0`), 다음 판이 올라올 때까지 여기에 산다 — 위의 `approvalRef`·`causeRef` 와 같다.\n */\n effect?: { status: string; at: string; seen: number }\n /** 받아들였으나 저쪽이 덧붙인 말 — 해석하지 않고 그대로(§`CommandAck.note`). */\n dispatchNote?: string\n /** 그 말 중 다음에 할 일 한 줄 — 원인은 위 칸이다(§`CommandAck.noteNext`). */\n dispatchNoteNext?: string\n /**\n * 다시 해서 될 일인가 — 어댑터가 실패에 답한 갈래(§`RetryOutlook`). 비면 말하지 않은 것이다.\n *\n * **여기는 `string` 이다** — 표에서 읽은 값이고 DB 는 무엇이든 들 수 있다. 좁은 타입으로 두면\n * 낡은 행이나 손으로 고친 값이 타입상 참인 척한다. 판정하는 쪽이 `isRetryOutlook` 으로 걸러\n * 읽고, 모르는 낱말은 「말하지 않은 것」과 같이 다룬다.\n */\n retry?: string\n /** 몇 번 넘겨 봤나. 첫 시도가 1. */\n attempts?: number\n /** 다음에 넘겨 볼 시각(ISO) — 물러섬을 표에 든다. 메모리에 두면 재기동에 사라진다. */\n nextAttemptAt?: string\n}\n\n/** 커맨드가 사는 자리 — 재기동을 넘어야 한다. */\nexport interface CommandStore {\n load(domainId: string, id: string): Promise<StoredCommand | null>\n save(domainId: string, command: StoredCommand): Promise<void>\n /** 새로 낸다 — `save` 는 있는 행만 고친다. 낼 길이 없으면 대기 줄은 영원히 비어 있다. */\n create(domainId: string, command: StoredCommand): Promise<StoredCommand>\n /**\n * 결정을 기다리는 것들의 근거 — 규칙 판정이 중복을 피하려면 이것이 있어야 한다.\n *\n * 규칙을 쓰지 않는 소비처도 있으므로 없어도 된다. 없으면 판정에 빈 집합이 가고, **그때는 중복이 난다**\n * — 그 사실을 부르는 쪽이 알아야 하므로 조용히 빈 집합을 만들어 주지 않는다.\n */\n openCauses?(domainId: string, instanceId: string): Promise<ReadonlySet<string>>\n /**\n * 넘긴 지시의 식별자로 조치를 찾는다 — **폐루프가 이 하나에 걸린다.**\n *\n * 없으면 나간 지시와 돌아온 관측을 이을 수 없다. 그 사실을 부르는 쪽이 알아야 하므로 조용히 빈\n * 지도를 만들어 주지 않는다(§`openCauses` 와 같은 규율).\n */\n byDispatchRef?(domainId: string, refs: readonly string[]): Promise<ReadonlyMap<string, StoredCommand>>\n /**\n * 효과를 기다리는 조치들 — **backfill 하는 길이 이것에 걸린다.**\n *\n * 유입 자리에서 잇는 것만으로는 부족하다. 우리가 내려가 있던 동안, 또는 배선이 아직 없던 동안 온\n * 보고는 **영원히 사라진다.** 실제로 그렇게 됐다 — 배선을 만든 날 그 앞에 온 보고 하나를 못 이었다.\n *\n * 넘긴 식별자가 있는 것만 낸다. 없는 것은 이을 열쇠가 없다.\n */\n awaitingEffect?(domainId: string): Promise<readonly StoredCommand[]>\n /**\n * **넘길 차례를 기다리는 것들** — 승인만 되고 안 나간 것과, 실패해서 다시 해 볼 것.\n *\n * 일꾼이 이것으로 표를 읽는다. 없으면 일꾼이 아무것도 하지 않는다(옛 거동) — 저장소가\n * 이 능력을 갖추지 않은 구현도 그대로 돌아야 한다.\n */\n pendingDispatch?(domainId: string): Promise<readonly StoredCommand[]>\n /**\n * 효과만 적는다 — **상태를 건드리지 않는다.**\n *\n * `save` 로 적으면 상태·승인·오류까지 함께 쓰이고, 유입 경로가 조치의 수명에 손을 대게 된다.\n * 관측이 조치를 되돌리거나 다시 넘기게 하는 길은 두지 않는다.\n */\n recordEffects?(domainId: string, updates: readonly { commandId: string; status: string; at: string; seen: number }[]): Promise<void>\n /**\n * 근거마다 마지막으로 조치를 낸 시각 — 승인 직후 같은 것이 다시 올라오는 것을 막는다.\n *\n * 없어도 된다. 없으면 판정이 결정 대기만 보고, **승인하는 순간 같은 조건이 다시 올라온다.**\n */\n lastProposedAt?(domainId: string, instanceId: string): Promise<ReadonlyMap<string, number>>\n}\n\n/**\n * 조치를 낸다 — **대기 줄에 무언가가 들어오는 유일한 자리.**\n *\n * ── 왜 뒤늦게 생겼나 (2026-08-31) ────────────────────────────────────────\n * 게이트와 승인 화면을 만들고 나서 물었다 — 「무엇을 승인하는 화면인가」. 커맨드 행을 **적는 코드가\n * 하나도 없었다.** 자리는 있고 길이 없는 그 부류다.\n *\n * 종류를 선언된 것으로 제한한다. 모르는 종류를 받으면 되돌릴 수 있는지도 모르는 채로 게이트를 지나게\n * 되고, 그러면 게이트가 시늉이 된다.\n */\nexport async function propose(\n store: CommandStore,\n domainId: string,\n specs: readonly CommandTypeSpec[],\n seed: {\n id: string\n instanceId: string\n type: string\n origin: CommandOrigin\n proposedAt: ISOTime\n payload?: Record<string, unknown>\n /** 낸 사람. 사람이 냈으면 반드시 있어야 한다 — 없으면 자기 승인을 막을 수 없다. */\n proposedBy?: string\n /** 규칙이 낸 것이면 그 근거. 이 값이 없으면 다음 주기가 같은 것을 또 올린다. */\n causeRef?: string\n ruleRef?: string\n }\n): Promise<StoredCommand> {\n const spec = specs.find(s => s.type === seed.type)\n if (!spec) {\n /*\n * **자극과 조치는 다른 어휘다.**\n *\n * 계약에 두 벌이 있다. `CMD` 는 커널 안에서 도는 자극이고(`resource.repair` = 모델에서 수리된 것으로\n * 둔다), 여기서 다루는 것은 현장에 나가는 지시다(`production.order` = 실제로 만든다). 주의 신호가\n * 권하는 것(`Attention.suggestedAction.command`)은 앞엣것이다.\n *\n * 둘이 섞이는 길이 실재한다 — 주의를 읽어 조치를 내는 코드를 쓰다 보면 그 값을 그대로 넘기게 된다.\n * 막히기는 하는데 **왜 막히는지 말하지 않으면** 부르는 쪽은 선언이 빠진 줄로 읽고 선언을 추가한다.\n * 그 순간 모델 안의 자극이 현장 지시가 된다.\n */\n if ((Object.values(CMD) as string[]).includes(seed.type)) {\n throw new Error(\n `그것은 커널 안에서 도는 자극이다 (${seed.type}) — 현장에 나가는 지시가 아니다. ` +\n '모델 상태를 바꾸려면 커널에 자극을 넣고, 현장에 무언가를 시키려면 그 지시를 조치 종류로 선언한다'\n )\n }\n\n throw new Error(\n `선언되지 않은 조치 종류다 (${seed.type}) — 선언된 것: ${specs.map(s => s.type).join(', ') || '(없음)'}. ` +\n '되돌릴 수 있는지를 모르는 채로 승인 게이트를 지나게 할 수 없다'\n )\n }\n\n /* 사람이 낸 것이면 누구인지 있어야 한다 — 되돌릴 수 없는 조치는 누가 냈는지가 기록에 남는다. */\n if (seed.origin === 'operator' && !seed.proposedBy?.trim()) {\n throw new Error('사람이 내는 조치는 낸 사람이 있어야 한다 — 누가 냈는지 남지 않는 조치는 받지 않는다')\n }\n\n /* 요구하는 칸이 없으면 계약이 던진다 — 빈 지시가 대기 줄에 쌓이면 승인하는 사람이 판단할 근거가 없다. */\n return await store.create(domainId, {\n ...commandFromSpec(spec, seed),\n ...(seed.proposedBy?.trim() ? { proposedBy: seed.proposedBy.trim() } : {}),\n ...(seed.causeRef ? { causeRef: seed.causeRef } : {}),\n ...(seed.ruleRef ? { ruleRef: seed.ruleRef } : {})\n })\n}\n\n/**\n * 결재선에 올라간 자리를 커맨드에 적는다.\n *\n * 이것이 적힌 뒤로 직접 결정은 막힌다. 그래서 **올리기와 적기가 한 트랜잭션에 있어야 한다** — 올렸는데\n * 적지 못하면 결재선에는 있고 커맨드는 직접 결정도 되는 상태가 남는다.\n */\nexport async function linkApproval(\n store: CommandStore,\n domainId: string,\n id: string,\n approvalRef: string\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n const next: StoredCommand = { ...command, approvalRef }\n await store.save(domainId, next)\n return next\n}\n\n/**\n * 승인한다 — **직접 문**.\n *\n * 승인할 수 없는 상태면 계약이 던진다 — 여기서 답으로 돌려주지 않는 이유는, 실패를 답으로 주면 부르는\n * 쪽이 그것을 무시할 수 있기 때문이다.\n */\nexport async function approve(\n store: CommandStore,\n domainId: string,\n id: string,\n approval: CommandApproval\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n assertDirectDoorOpen(command)\n\n const approved = approveCommand(command, approval)\n await store.save(domainId, { ...approved, ...carried(command) })\n return approved\n}\n\n/** 거절한다 — 여기서 끝난다. 되살아나지 않는다. */\nexport async function reject(\n store: CommandStore,\n domainId: string,\n id: string,\n approval: CommandApproval\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n assertDirectDoorOpen(command)\n return await settle(store, domainId, command, false, approval)\n}\n\n/**\n * 결재선의 판정을 커맨드에 옮긴다 — **결재선 문**.\n *\n * 부르는 쪽은 앱의 브리지다. 올라간 자리를 함께 넘기게 해서, 다른 결재 건의 판정이 이 커맨드에 적히는\n * 길을 막는다.\n *\n * 거절을 여기서 **끝으로** 다룬다. 업무 목록에서 「거절」은 되돌려 보내 고쳐 올리라는 뜻이지만, 조치에는\n * 고칠 것이 없다 — 지시 내용은 제안될 때 이미 정해져 있다. 그러니 되돌려 받을 것이 없고, 게이트를 닫는\n * 것이 맞다. 커맨드가 닫힌 뒤에 결재선에서 누가 승인해도 계약이 `rejected → approved` 를 허용하지 않는다.\n */\nexport async function settleByApproval(\n store: CommandStore,\n domainId: string,\n id: string,\n decision: { approvalRef: string; approved: boolean; by: string; at: string; note?: string }\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n\n if (!command.approvalRef) {\n throw new Error(`결재선에 올라가지 않은 커맨드다 (${id}) — 이 판정이 어디서 왔는지 확인할 수 없다`)\n }\n if (command.approvalRef !== decision.approvalRef) {\n throw new Error(\n `다른 결재 건의 판정이다 (커맨드 ${id} 는 ${command.approvalRef} 를 기다리는데 판정은 ${decision.approvalRef} 에서 왔다)`\n )\n }\n\n const approval: CommandApproval = {\n by: decision.by,\n at: decision.at,\n ...(decision.note ? { note: decision.note } : {})\n }\n\n if (!decision.approved) return await settle(store, domainId, command, false, approval)\n\n const approved = approveCommand(command, approval)\n await store.save(domainId, { ...approved, ...carried(command) })\n return approved\n}\n\nasync function mustLoad(store: CommandStore, domainId: string, id: string): Promise<StoredCommand> {\n const command = await store.load(domainId, id)\n if (!command) throw new Error(`모르는 커맨드다 (${id})`)\n return command\n}\n\n/** 결재선에 올라간 커맨드는 직접 결정하지 않는다 — 문은 한 벌이다. */\nfunction assertDirectDoorOpen(command: StoredCommand): void {\n if (!command.approvalRef) return\n throw new Error(\n `이 조치는 결재선에 올라가 있다 (${command.approvalRef}) — 승인·거절은 업무 목록에서 한다. ` +\n '여기서 결정하면 결재선에는 대기로 남고 조치는 나간 상태가 된다'\n )\n}\n\n/** 승인 기록만 바뀌고 나머지는 그대로 간다 — 하나라도 흘리면 그 사실이 사라진다. */\nfunction carried(command: StoredCommand): Partial<StoredCommand> {\n return {\n ...(command.approvalRef ? { approvalRef: command.approvalRef } : {}),\n ...(command.proposedBy ? { proposedBy: command.proposedBy } : {}),\n ...(command.causeRef ? { causeRef: command.causeRef } : {}),\n ...(command.ruleRef ? { ruleRef: command.ruleRef } : {})\n }\n}\n\nasync function settle(\n store: CommandStore,\n domainId: string,\n command: StoredCommand,\n approved: boolean,\n approval: CommandApproval\n): Promise<StoredCommand> {\n const to = nextCommandState(command.state, approved ? 'approve' : 'reject')\n if (!to) throw new Error(`${command.state} 상태의 커맨드는 ${approved ? '승인' : '거절'}할 수 없다 (${command.id})`)\n\n const next: StoredCommand = { ...command, state: to, approval, ...carried(command) }\n await store.save(domainId, next)\n return next\n}\n\n/**\n * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**\n *\n * `actuate` 는 `ApprovedCommand` 만 받으므로, 승인 없이 이 함수를 지나가는 길이 없다. 저장소에서\n * 되세운 커맨드는 타입이 지워진 채로 오므로 `asApproved` 로 다시 확인한다 — 확인 없이 표식을 붙이면\n * **재기동이 게이트를 여는 길**이 된다.\n *\n * 되돌릴 수 있는 커맨드는 사람 없이 지난다(시뮬·what-if). 그 판단도 계약이 한다.\n */\nexport async function dispatch(\n store: CommandStore,\n domainId: string,\n id: string,\n actuate: (command: ApprovedCommand) => Promise<CommandAck>\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n const ref = carried(command)\n\n /*\n * 되돌릴 수 있는 것은 제안 상태에서 바로 지난다. 되돌릴 수 없는 것은 승인 기록이 있어야 하고,\n * 없으면 여기서 던진다 — 조용히 건너뛰면 그 커맨드가 승인 없이 나간 것이 된다.\n */\n const approved: ApprovedCommand =\n command.state === 'proposed' && !requiresApproval(command) ? passThroughReversible(command) : asApproved(command)\n\n const to = nextCommandState(approved.state, 'dispatch')\n if (!to) throw new Error(`${approved.state} 상태의 커맨드는 넘길 수 없다 (${id})`)\n\n /*\n * **넘기기 전에 「넘기는 중」을 먼저 적는다.** 넘기고 나서 적으면, 그 사이에 프로세스가 죽었을 때\n * 저쪽은 받았는데 우리는 안 보낸 줄 알고 다시 보낸다. 현장이 두 번 움직인다.\n *\n * ── 지난 시도의 자국을 지운다 (2026-09-03, 인티그레이션 레인이 걸어서 찾음) ──\n * `failed → dispatch` 를 열어 둔 것의 짝이다. 다시 넘겨 성공했는데 **지난 실패의 `error` 가 그대로\n * 남아** 한 행이 두 가지를 동시에 말했다.\n *\n * state acked 받았다\n * error 「그쪽에 비밀값이 설정되지 않았습니다」 안 받는다 ← 지난 시도의 것\n *\n * 화면이 그 행을 그리면 어느 쪽을 믿어야 하는지 알 수 없다. `dispatchNote` 도 같은 부류였다 —\n * 새 응답에 덧붙일 말이 없으면 옛 말이 남는다.\n *\n * 그래서 「넘기는 중」이 **이번 시도의 빈 종이**여야 한다. 셋 다 이번 응답이 채우는 값이다.\n *\n * `dispatchRef` 를 지워도 효과가 고아가 되지 않는다 — 식별자는 성공할 때만 붙고(§아래), 성공은\n * `acked` 로 끝난다(종착). 그러니 다시 넘길 수 있는 커맨드에는 식별자가 없고 효과도 없다.\n */\n const dispatching: StoredCommand = {\n ...approved,\n state: to,\n ...ref,\n dispatchRef: undefined,\n dispatchNote: undefined,\n error: undefined\n }\n await store.save(domainId, dispatching)\n\n let ack: CommandAck\n try {\n ack = await actuate(approved)\n } catch (e: any) {\n /* 던진 실패는 어댑터가 갈래를 말할 기회가 없었다 — `retry` 를 비워 두고 일꾼이 「모름」으로 읽는다. */\n const failed: StoredCommand = { ...dispatching, state: 'failed', error: String(e?.message ?? e) }\n await store.save(domainId, failed)\n return failed\n }\n\n const settled: StoredCommand = ack?.ok\n ? {\n ...dispatching,\n state: 'acked',\n dispatchRef: ack.ref,\n ...(ack.note ? { dispatchNote: ack.note } : {}),\n ...(ack.noteNext ? { dispatchNoteNext: ack.noteNext } : {})\n }\n : {\n ...dispatching,\n state: 'failed',\n error: ack?.error ?? '어댑터가 이유를 말하지 않았다',\n /* 다시 될 일인지 어댑터가 답했으면 적는다 — 일꾼이 다음 주기에 이 값을 본다. */\n ...(ack?.retry ? { retry: ack.retry } : {})\n }\n await store.save(domainId, settled)\n return settled\n}\n"]}
1
+ {"version":3,"file":"command-dispatcher.js","sourceRoot":"","sources":["../../../server/service/actuation/command-dispatcher.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **승인을 지난 커맨드만 어댑터에 넘긴다** — 게이트가 서는 자리.\n *\n * ── 왜 이 자리인가 (2026-08-31) ──────────────────────────────────────────\n * 설계가 「기본 = propose-only」를 정해 두었는데 코드에 그 규율이 없었다. 인티그레이션 레인이 어댑터에\n * 조치를 붙이려다 그것을 보고 멈췄다 — 게이트 없이 붙이면 트윈이 사람 승인 없이 현장에 작업지시를\n * 내린다. 작업지시는 취소해도 이미 만든 것이 남는다.\n *\n * ── 확인을 여기서 한 번만 한다 ───────────────────────────────────────────\n * 어댑터는 승인을 확인하지 않는다. 그 확인은 이 파일이 했고 **타입이 그것을 증명한다**\n * (`ApprovedCommand` 는 `approveCommand`·`asApproved` 로만 만들어진다). 어댑터가 또 확인하면 규칙이\n * 두 벌이 되고, 한쪽만 고쳐지는 날이 온다.\n *\n * ── 저장소에 닿는 자리를 밖으로 낸다 ─────────────────────────────────────\n * 훅에서와 같은 이유다(§`HookStore`). 시험이 **재기동을 넣을 수 있어야** 한다 — 승인이 프로세스 안에만\n * 살아 있으면 「승인된다」는 보이지만 「다시 세운 뒤에도 승인이 남아 있나」는 보이지 않는다.\n */\nimport {\n CMD,\n approveCommand,\n asApproved,\n commandFromSpec,\n nextCommandState,\n passThroughReversible,\n requiresApproval,\n type ApprovedCommand,\n type CommandApproval,\n type CommandOrigin,\n type CommandTypeSpec,\n type ISOTime,\n type TwinCommand\n} from '@operato/ops-contract'\n\n/**\n * 조치의 결과 — 어댑터가 돌려준다.\n *\n * 계약에도 같은 이름의 타입이 있는데(`contract.ts:3074`) **다른 것**이다. 그쪽은 커널 자극의 답이고\n * 이쪽은 현장 지시의 답이다. 자극과 조치를 가른 것과 같은 갈림이다(§`propose` 의 거절).\n */\nimport type { RetryOutlook } from './retry-plan.js'\n\nexport interface CommandAck {\n ok: boolean\n /** 저쪽 문서 · 작업 식별자. 되돌릴 때 무엇을 되돌릴지 말하려면 이것이 있어야 한다. */\n ref?: string\n /**\n * **받아들였으나 저쪽이 덧붙인 말** — 실패가 아니고, 「끝났다」도 아니다.\n *\n * ── 왜 필요한가 (2026-09-02 실측) ──────────────────────────────────────\n * MES 에 조치 둘을 넘겼더니 트윈 쪽 두 행이 구별되지 않았다.\n *\n * WO-20260902-004 지시서 붙음 acked · ref 있음 · error 없음\n * WO-20260902-005 지시서 못 붙음 acked · ref 있음 · error 없음 ← 같다\n *\n * 005 는 사람이 MES 에서 지시서를 골라야 현장에 간다. 커넥터는 그 사실을 받았는데 **적을 칸이\n * 없어서 로그로만** 남겼다. `error` 에 적으면 실패로 읽혀 커맨드가 `failed` 로 앉는다.\n *\n * **해석하지 않고 그대로 적는다.** 무엇이 남았는지는 저쪽 어휘이고, 커널이 그 방언을 알면 보편\n * 계약이 깨진다 — 효과의 상태를 그대로 적는 것과 같은 규율(§`effect-match.ts`).\n */\n note?: string\n /**\n * 그 말 중 **다음에 할 일** 한 줄 — 원인은 `note` 다.\n *\n * 왜 둘인가: 읽는 사람은 「그래서 내가 뭘 해야 하나」를 먼저 찾는다(UX 레인). 한 문장으로 주면\n * 화면이 자르게 되고, **자르는 규칙이 화면마다 생긴다.** 어댑터는 이미 둘로 알고 있으니 붙이지\n * 않고 따로 준다.\n */\n noteNext?: string\n error?: string\n /**\n * **다시 해서 될 일인가** — 실패에만 답한다(§`RetryOutlook`).\n *\n * again 그대로 다시 해 볼 만하다 못 닿았거나 저쪽이 잠깐 흔들렸다\n * after-fix 사람이 고친 뒤 그대로 나간다 설정 문제\n * never 이 조치로는 영원히 안 된다 지시 내용이 틀렸다\n *\n * **문장에 담지 않는 이유**: 일꾼이 그것을 쓰려면 문장을 파싱해야 하고, 번역되면 깨진다.\n * `code` 를 따로 둔 것과 같은 이유다.\n *\n * 비어 있으면 **말하지 않은 것**이고 `never` 가 아니다. 그때 일꾼은 집지 않고 목록에 남긴다 —\n * 모르는 것을 `never` 로 접으면 고칠 수 있는 것을 사람이 포기한다.\n *\n * 타입을 좁게 둔다 — **어댑터가 답하는 값**이고, 여기서 넓히면 커넥터가 모르는 낱말을 보내도\n * 타입이 통과시킨다. 표에서 읽는 쪽은 `string` 이다(DB 는 무엇이든 들 수 있으므로\n * `isRetryOutlook` 으로 걸러 읽는다).\n */\n retry?: RetryOutlook\n}\n\n/**\n * 저장소에서 되세운 커맨드 — 계약의 커맨드에 **호스트가 아는 한 칸**이 붙는다.\n *\n * `approvalRef` 를 계약에 넣지 않는다. 계약은 운영 도메인의 어휘(EPCIS·ISA-95)를 들고, 「승인 기록이\n * 어느 업무 시스템의 몇 번 문서인가」는 이 저장소의 배선이다. 계약에 넣으면 커널을 쓰는 모든 소비처가\n * 우리 업무 모듈의 존재를 알게 된다.\n */\nexport type StoredCommand = TwinCommand & {\n approvalRef?: string\n proposedBy?: string\n /** 규칙이 낸 것이면 그 근거가 된 관측(`Attention.id`) — 중복을 막는 열쇠다. */\n causeRef?: string\n ruleRef?: string\n /**\n * 저쪽에서 실제로 무엇이 되었나 — **상태가 아니라 별개 사실이다.**\n *\n * `acked` 는 「저쪽이 답했다」이고 종착 상태다. 여기에 「현장에 나갔다」를 상태로 이어 붙이면\n * `acked` 가 종착이 아니게 되고, 되돌아오는 관측이 없는 커넥터의 조치가 **영원히 끝나지 않은\n * 것으로 보인다.** 그런 커넥터가 정상이다 — 관측을 되돌려 주는 능력은 커넥터의 성질이지 조치의\n * 수명이 아니다.\n *\n * `status` 는 **해석하지 않고 그대로** 적는다(§`effect-match.ts`). 오더 상태 어휘는 도메인\n * 소유이고, 어느 값이 「현장에 나갔다」인지는 현장마다 다르다.\n *\n * 이 칸이 비어 있는 것은 「효과가 없었다」가 아니라 **「모른다」**다. 넘기다 닿지 못한 조치는\n * 식별자를 못 받으므로 이을 길이 아예 없다.\n *\n * ── 계약에도 같은 칸을 두었다 ──────────────────────────────────────────\n * `ops-contract` 의 `TwinCommand.effect` 가 이것이다. 다만 이 저장소는 그 패키지를 레지스트리에서\n * 받으므로(`^0.9.0`), 다음 버전이 올라올 때까지 여기에 산다 — 위의 `approvalRef`·`causeRef` 와 같다.\n */\n effect?: { status: string; at: string; seen: number }\n /** 받아들였으나 저쪽이 덧붙인 말 — 해석하지 않고 그대로(§`CommandAck.note`). */\n dispatchNote?: string\n /** 그 말 중 다음에 할 일 한 줄 — 원인은 위 칸이다(§`CommandAck.noteNext`). */\n dispatchNoteNext?: string\n /**\n * 다시 해서 될 일인가 — 어댑터가 실패에 답한 갈래(§`RetryOutlook`). 비면 말하지 않은 것이다.\n *\n * **여기는 `string` 이다** — 표에서 읽은 값이고 DB 는 무엇이든 들 수 있다. 좁은 타입으로 두면\n * 낡은 행이나 손으로 고친 값이 타입상 참인 척한다. 판정하는 쪽이 `isRetryOutlook` 으로 걸러\n * 읽고, 모르는 낱말은 「말하지 않은 것」과 같이 다룬다.\n */\n retry?: string\n /** 몇 번 넘겨 봤나. 첫 시도가 1. */\n attempts?: number\n /** 다음에 넘겨 볼 시각(ISO) — 물러섬을 표에 든다. 메모리에 두면 재기동에 사라진다. */\n nextAttemptAt?: string\n}\n\n/** 커맨드가 사는 자리 — 재기동을 넘어야 한다. */\nexport interface CommandStore {\n load(domainId: string, id: string): Promise<StoredCommand | null>\n save(domainId: string, command: StoredCommand): Promise<void>\n /** 새로 낸다 — `save` 는 있는 행만 고친다. 낼 길이 없으면 대기 줄은 영원히 비어 있다. */\n create(domainId: string, command: StoredCommand): Promise<StoredCommand>\n /**\n * 결정을 기다리는 것들의 근거 — 규칙 판정이 중복을 피하려면 이것이 있어야 한다.\n *\n * 규칙을 쓰지 않는 소비처도 있으므로 없어도 된다. 없으면 판정에 빈 집합이 가고, **그때는 중복이 난다**\n * — 그 사실을 부르는 쪽이 알아야 하므로 조용히 빈 집합을 만들어 주지 않는다.\n */\n openCauses?(domainId: string, instanceId: string): Promise<ReadonlySet<string>>\n /**\n * 넘긴 지시의 식별자로 조치를 찾는다 — **폐루프가 이 하나에 걸린다.**\n *\n * 없으면 나간 지시와 돌아온 관측을 이을 수 없다. 그 사실을 부르는 쪽이 알아야 하므로 조용히 빈\n * 지도를 만들어 주지 않는다(§`openCauses` 와 같은 규율).\n */\n byDispatchRef?(domainId: string, refs: readonly string[]): Promise<ReadonlyMap<string, StoredCommand>>\n /**\n * 효과를 기다리는 조치들 — **backfill 하는 길이 이것에 걸린다.**\n *\n * 유입 자리에서 잇는 것만으로는 부족하다. 우리가 내려가 있던 동안, 또는 배선이 아직 없던 동안 온\n * 보고는 **영원히 사라진다.** 실제로 그렇게 됐다 — 배선을 만든 날 그 앞에 온 보고 하나를 못 이었다.\n *\n * 넘긴 식별자가 있는 것만 낸다. 없는 것은 이을 열쇠가 없다.\n */\n awaitingEffect?(domainId: string): Promise<readonly StoredCommand[]>\n /**\n * **넘길 차례를 기다리는 것들** — 승인만 되고 안 나간 것과, 실패해서 다시 해 볼 것.\n *\n * 일꾼이 이것으로 표를 읽는다. 없으면 일꾼이 아무것도 하지 않는다(옛 거동) — 저장소가\n * 이 능력을 갖추지 않은 구현도 그대로 돌아야 한다.\n */\n pendingDispatch?(domainId: string): Promise<readonly StoredCommand[]>\n /**\n * 효과만 적는다 — **상태를 건드리지 않는다.**\n *\n * `save` 로 적으면 상태·승인·오류까지 함께 쓰이고, 유입 경로가 조치의 수명에 손을 대게 된다.\n * 관측이 조치를 되돌리거나 다시 넘기게 하는 길은 두지 않는다.\n */\n recordEffects?(domainId: string, updates: readonly { commandId: string; status: string; at: string; seen: number }[]): Promise<void>\n /**\n * 근거마다 마지막으로 조치를 낸 시각 — 승인 직후 같은 것이 다시 올라오는 것을 막는다.\n *\n * 없어도 된다. 없으면 판정이 결정 대기만 보고, **승인하는 순간 같은 조건이 다시 올라온다.**\n */\n lastProposedAt?(domainId: string, instanceId: string): Promise<ReadonlyMap<string, number>>\n}\n\n/**\n * 조치를 낸다 — **대기 줄에 무언가가 들어오는 유일한 자리.**\n *\n * ── 왜 뒤늦게 생겼나 (2026-08-31) ────────────────────────────────────────\n * 게이트와 승인 화면을 만들고 나서 물었다 — 「무엇을 승인하는 화면인가」. 커맨드 행을 **적는 코드가\n * 하나도 없었다.** 자리는 있고 길이 없는 그 부류다.\n *\n * 종류를 선언된 것으로 제한한다. 모르는 종류를 받으면 되돌릴 수 있는지도 모르는 채로 게이트를 지나게\n * 되고, 그러면 게이트가 시늉이 된다.\n */\nexport async function propose(\n store: CommandStore,\n domainId: string,\n specs: readonly CommandTypeSpec[],\n seed: {\n id: string\n instanceId: string\n type: string\n origin: CommandOrigin\n proposedAt: ISOTime\n payload?: Record<string, unknown>\n /** 낸 사람. 사람이 냈으면 반드시 있어야 한다 — 없으면 자기 승인을 막을 수 없다. */\n proposedBy?: string\n /** 규칙이 낸 것이면 그 근거. 이 값이 없으면 다음 주기가 같은 것을 또 올린다. */\n causeRef?: string\n ruleRef?: string\n }\n): Promise<StoredCommand> {\n const spec = specs.find(s => s.type === seed.type)\n if (!spec) {\n /*\n * **자극과 조치는 다른 어휘다.**\n *\n * 계약에 두 벌이 있다. `CMD` 는 커널 안에서 도는 자극이고(`resource.repair` = 모델에서 수리된 것으로\n * 둔다), 여기서 다루는 것은 현장에 나가는 지시다(`production.order` = 실제로 만든다). 주의 신호가\n * 권하는 것(`Attention.suggestedAction.command`)은 앞엣것이다.\n *\n * 둘이 섞이는 길이 실재한다 — 주의를 읽어 조치를 내는 코드를 쓰다 보면 그 값을 그대로 넘기게 된다.\n * 막히기는 하는데 **왜 막히는지 말하지 않으면** 부르는 쪽은 선언이 빠진 줄로 읽고 선언을 추가한다.\n * 그 순간 모델 안의 자극이 현장 지시가 된다.\n */\n if ((Object.values(CMD) as string[]).includes(seed.type)) {\n throw new Error(\n `그것은 커널 안에서 도는 자극이다 (${seed.type}) — 현장에 나가는 지시가 아니다. ` +\n '모델 상태를 바꾸려면 커널에 자극을 넣고, 현장에 무언가를 시키려면 그 지시를 조치 종류로 선언한다'\n )\n }\n\n throw new Error(\n `선언되지 않은 조치 종류다 (${seed.type}) — 선언된 것: ${specs.map(s => s.type).join(', ') || '(없음)'}. ` +\n '되돌릴 수 있는지를 모르는 채로 승인 게이트를 지나게 할 수 없다'\n )\n }\n\n /* 사람이 낸 것이면 누구인지 있어야 한다 — 되돌릴 수 없는 조치는 누가 냈는지가 기록에 남는다. */\n if (seed.origin === 'operator' && !seed.proposedBy?.trim()) {\n throw new Error('사람이 내는 조치는 낸 사람이 있어야 한다 — 누가 냈는지 남지 않는 조치는 받지 않는다')\n }\n\n /* 요구하는 칸이 없으면 계약이 던진다 — 빈 지시가 대기 줄에 쌓이면 승인하는 사람이 판단할 근거가 없다. */\n return await store.create(domainId, {\n ...commandFromSpec(spec, seed),\n ...(seed.proposedBy?.trim() ? { proposedBy: seed.proposedBy.trim() } : {}),\n ...(seed.causeRef ? { causeRef: seed.causeRef } : {}),\n ...(seed.ruleRef ? { ruleRef: seed.ruleRef } : {})\n })\n}\n\n/**\n * 결재선에 올라간 자리를 커맨드에 적는다.\n *\n * 이것이 적힌 뒤로 직접 결정은 막힌다. 그래서 **올리기와 적기가 한 트랜잭션에 있어야 한다** — 올렸는데\n * 적지 못하면 결재선에는 있고 커맨드는 직접 결정도 되는 상태가 남는다.\n */\nexport async function linkApproval(\n store: CommandStore,\n domainId: string,\n id: string,\n approvalRef: string\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n const next: StoredCommand = { ...command, approvalRef }\n await store.save(domainId, next)\n return next\n}\n\n/**\n * 승인한다 — **직접 문**.\n *\n * 승인할 수 없는 상태면 계약이 던진다 — 여기서 답으로 돌려주지 않는 이유는, 실패를 답으로 주면 부르는\n * 쪽이 그것을 무시할 수 있기 때문이다.\n */\nexport async function approve(\n store: CommandStore,\n domainId: string,\n id: string,\n approval: CommandApproval\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n assertDirectDoorOpen(command)\n\n const approved = approveCommand(command, approval)\n await store.save(domainId, { ...approved, ...carried(command) })\n return approved\n}\n\n/** 거절한다 — 여기서 끝난다. 되살아나지 않는다. */\nexport async function reject(\n store: CommandStore,\n domainId: string,\n id: string,\n approval: CommandApproval\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n assertDirectDoorOpen(command)\n return await settle(store, domainId, command, false, approval)\n}\n\n/**\n * 결재선의 판정을 커맨드에 옮긴다 — **결재선 문**.\n *\n * 부르는 쪽은 앱의 브리지다. 올라간 자리를 함께 넘기게 해서, 다른 결재 건의 판정이 이 커맨드에 적히는\n * 길을 막는다.\n *\n * 거절을 여기서 **끝으로** 다룬다. 업무 목록에서 「거절」은 되돌려 보내 고쳐 올리라는 뜻이지만, 조치에는\n * 고칠 것이 없다 — 지시 내용은 제안될 때 이미 정해져 있다. 그러니 되돌려 받을 것이 없고, 게이트를 닫는\n * 것이 맞다. 커맨드가 닫힌 뒤에 결재선에서 누가 승인해도 계약이 `rejected → approved` 를 허용하지 않는다.\n */\nexport async function settleByApproval(\n store: CommandStore,\n domainId: string,\n id: string,\n decision: { approvalRef: string; approved: boolean; by: string; at: string; note?: string }\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n\n if (!command.approvalRef) {\n throw new Error(`결재선에 올라가지 않은 커맨드다 (${id}) — 이 판정이 어디서 왔는지 확인할 수 없다`)\n }\n if (command.approvalRef !== decision.approvalRef) {\n throw new Error(\n `다른 결재 건의 판정이다 (커맨드 ${id} 는 ${command.approvalRef} 를 기다리는데 판정은 ${decision.approvalRef} 에서 왔다)`\n )\n }\n\n const approval: CommandApproval = {\n by: decision.by,\n at: decision.at,\n ...(decision.note ? { note: decision.note } : {})\n }\n\n if (!decision.approved) return await settle(store, domainId, command, false, approval)\n\n const approved = approveCommand(command, approval)\n await store.save(domainId, { ...approved, ...carried(command) })\n return approved\n}\n\nasync function mustLoad(store: CommandStore, domainId: string, id: string): Promise<StoredCommand> {\n const command = await store.load(domainId, id)\n if (!command) throw new Error(`모르는 커맨드다 (${id})`)\n return command\n}\n\n/** 결재선에 올라간 커맨드는 직접 결정하지 않는다 — 문은 한 벌이다. */\nfunction assertDirectDoorOpen(command: StoredCommand): void {\n if (!command.approvalRef) return\n throw new Error(\n `이 조치는 결재선에 올라가 있다 (${command.approvalRef}) — 승인·거절은 업무 목록에서 한다. ` +\n '여기서 결정하면 결재선에는 대기로 남고 조치는 나간 상태가 된다'\n )\n}\n\n/** 승인 기록만 바뀌고 나머지는 그대로 간다 — 하나라도 흘리면 그 사실이 사라진다. */\nfunction carried(command: StoredCommand): Partial<StoredCommand> {\n return {\n ...(command.approvalRef ? { approvalRef: command.approvalRef } : {}),\n ...(command.proposedBy ? { proposedBy: command.proposedBy } : {}),\n ...(command.causeRef ? { causeRef: command.causeRef } : {}),\n ...(command.ruleRef ? { ruleRef: command.ruleRef } : {})\n }\n}\n\nasync function settle(\n store: CommandStore,\n domainId: string,\n command: StoredCommand,\n approved: boolean,\n approval: CommandApproval\n): Promise<StoredCommand> {\n const to = nextCommandState(command.state, approved ? 'approve' : 'reject')\n if (!to) throw new Error(`${command.state} 상태의 커맨드는 ${approved ? '승인' : '거절'}할 수 없다 (${command.id})`)\n\n const next: StoredCommand = { ...command, state: to, approval, ...carried(command) }\n await store.save(domainId, next)\n return next\n}\n\n/**\n * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**\n *\n * `actuate` 는 `ApprovedCommand` 만 받으므로, 승인 없이 이 함수를 지나가는 길이 없다. 저장소에서\n * 되세운 커맨드는 타입이 지워진 채로 오므로 `asApproved` 로 다시 확인한다 — 확인 없이 표식을 붙이면\n * **재기동이 게이트를 여는 길**이 된다.\n *\n * 되돌릴 수 있는 커맨드는 사람 없이 지난다(시뮬·what-if). 그 판단도 계약이 한다.\n */\nexport async function dispatch(\n store: CommandStore,\n domainId: string,\n id: string,\n actuate: (command: ApprovedCommand) => Promise<CommandAck>\n): Promise<StoredCommand> {\n const command = await mustLoad(store, domainId, id)\n const ref = carried(command)\n\n /*\n * 되돌릴 수 있는 것은 제안 상태에서 바로 지난다. 되돌릴 수 없는 것은 승인 기록이 있어야 하고,\n * 없으면 여기서 던진다 — 조용히 건너뛰면 그 커맨드가 승인 없이 나간 것이 된다.\n */\n const approved: ApprovedCommand =\n command.state === 'proposed' && !requiresApproval(command) ? passThroughReversible(command) : asApproved(command)\n\n const to = nextCommandState(approved.state, 'dispatch')\n if (!to) throw new Error(`${approved.state} 상태의 커맨드는 넘길 수 없다 (${id})`)\n\n /*\n * **넘기기 전에 「넘기는 중」을 먼저 적는다.** 넘기고 나서 적으면, 그 사이에 프로세스가 죽었을 때\n * 저쪽은 받았는데 우리는 안 보낸 줄 알고 다시 보낸다. 현장이 두 번 움직인다.\n *\n * ── 지난 시도의 자국을 지운다 (2026-09-03, 인티그레이션 레인이 걸어서 찾음) ──\n * `failed → dispatch` 를 열어 둔 것의 짝이다. 다시 넘겨 성공했는데 **지난 실패의 `error` 가 그대로\n * 남아** 한 행이 두 가지를 동시에 말했다.\n *\n * state acked 받았다\n * error 「그쪽에 비밀값이 설정되지 않았습니다」 안 받는다 ← 지난 시도의 것\n *\n * 화면이 그 행을 그리면 어느 쪽을 믿어야 하는지 알 수 없다. `dispatchNote` 도 같은 부류였다 —\n * 새 응답에 덧붙일 말이 없으면 옛 말이 남는다.\n *\n * 그래서 「넘기는 중」이 **이번 시도의 빈 종이**여야 한다. 셋 다 이번 응답이 채우는 값이다.\n *\n * `dispatchRef` 를 지워도 효과가 고아가 되지 않는다 — 식별자는 성공할 때만 붙고(§아래), 성공은\n * `acked` 로 끝난다(종착). 그러니 다시 넘길 수 있는 커맨드에는 식별자가 없고 효과도 없다.\n */\n const dispatching: StoredCommand = {\n ...approved,\n state: to,\n ...ref,\n dispatchRef: undefined,\n dispatchNote: undefined,\n error: undefined\n }\n await store.save(domainId, dispatching)\n\n let ack: CommandAck\n try {\n ack = await actuate(approved)\n } catch (e: any) {\n /* 던진 실패는 어댑터가 갈래를 말할 기회가 없었다 — `retry` 를 비워 두고 일꾼이 「모름」으로 읽는다. */\n const failed: StoredCommand = { ...dispatching, state: 'failed', error: String(e?.message ?? e) }\n await store.save(domainId, failed)\n return failed\n }\n\n const settled: StoredCommand = ack?.ok\n ? {\n ...dispatching,\n state: 'acked',\n dispatchRef: ack.ref,\n ...(ack.note ? { dispatchNote: ack.note } : {}),\n ...(ack.noteNext ? { dispatchNoteNext: ack.noteNext } : {})\n }\n : {\n ...dispatching,\n state: 'failed',\n error: ack?.error ?? '어댑터가 이유를 말하지 않았다',\n /* 다시 될 일인지 어댑터가 답했으면 적는다 — 일꾼이 다음 주기에 이 값을 본다. */\n ...(ack?.retry ? { retry: ack.retry } : {})\n }\n await store.save(domainId, settled)\n return settled\n}\n"]}
@@ -11,7 +11,7 @@ import { type ActuationTarget } from './actuation-target.js';
11
11
  *
12
12
  * 값은 흘렀다 — JS 객체가 키를 안 버리므로. 어제 `dispatch_note` 에 실제로 앉았다. 그런데
13
13
  * **커넥터를 만드는 사람이 그 칸이 있다는 것을 알 방법이 없었다.** 우리가 어제 하루 종일
14
- * 「자리는 있고 길이 없다」로 부른 것의 타입 판이다.
14
+ * 「자리는 있고 길이 없다」로 부른 것의 타입 버전이다.
15
15
  *
16
16
  * 좁히지 않는다: 세 칸을 다 실어 `CommandAck` 와 같은 모양으로 둔다. 커널이 어느 칸을 어떻게
17
17
  * 쓰는지는 `dispatch` 가 정하고, 이 자리는 나르기만 한다.
@@ -1 +1 @@
1
- {"version":3,"file":"actuation-routing.js","sourceRoot":"","sources":["../../../server/service/reference/actuation-routing.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **승인된 조치가 원본으로 나가는 길** — 판단은 `actuation-target.ts`, 여기는 배선.\n *\n * ── 무엇이 끊겨 있었나 (2026-09-02) ────────────────────────────────────────────\n * MES 커넥터는 `actuate` 를 다 만들어 두었다. 서명·접수 판정·초안 상태까지 있었다. 그런데 넘기는\n * 리졸버가 **어댑터를 찾지도 않고** 「구현이 없습니다」라고 답했다. 그 문장을 사실로 읽어서 설계\n * 문서에도, 다른 레인에도 그렇게 알렸다 — 둘 다 틀렸다.\n *\n * 자리는 있고 길이 없는 상태다. 이 저장소에서 가장 자주 나는 결함 부류이고, 조치 경로에서 나면\n * 결과가 하나다 — **승인은 쌓이는데 현장에는 아무것도 가지 않는다.**\n *\n * ── 왜 구동과 따로 두나 ───────────────────────────────────────────────────────\n * 모양이 같아서 한 함수로 묶고 싶어진다. 묶지 않는다.\n *\n * 구동은 시뮬레이터를 움직인다 틀리면 시늉이 어긋난다\n * 조치는 현장에 작업지시를 낸다 틀리면 만들지 않아도 될 것을 만든다\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport type { ApprovedCommand } from '@operato/ops-contract'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\n\nimport { TwinReference } from './twin-reference.js'\nimport { twinError } from '../../engine/log.js'\nimport { actuationOutcomePatch } from './actuation-verification.js'\nimport { capabilitiesOf, getAdapter, type ActuationReadiness, type ActuationResult, type SiteDescriptor } from './reference-adapter.js'\nimport { APPROVAL_STANDS, actuationTarget, isActuationAllowed, type ActuationTarget } from './actuation-target.js'\n\n/** 어댑터가 답하는 모양 그대로 — 디스패처가 이 값을 커맨드 행에 적는다. */\n/**\n * 조치를 낸 결과 — **어댑터가 답한 것을 좁히지 않고 그대로 나른다.**\n *\n * ── 왜 넓혔나 (2026-09-04, 인티그레이션 레인이 찾음) ────────────────────────\n * 이 타입이 `ok`·`ref`·`error` 셋만 들고 있었다. 그래서 `routeActuation` 이 어댑터의 답을\n * 그대로 되돌리는데 **타입은 `note`·`noteNext`·`retry` 를 버린다고 말했다.**\n *\n * 값은 흘렀다 — JS 객체가 키를 안 버리므로. 어제 `dispatch_note` 에 실제로 앉았다. 그런데\n * **커넥터를 만드는 사람이 그 칸이 있다는 것을 알 방법이 없었다.** 우리가 어제 하루 종일\n * 「자리는 있고 길이 없다」로 부른 것의 타입 판이다.\n *\n * 좁히지 않는다: 세 칸을 다 실어 `CommandAck` 와 같은 모양으로 둔다. 커널이 어느 칸을 어떻게\n * 쓰는지는 `dispatch` 가 정하고, 이 자리는 나르기만 한다.\n */\nexport interface ActuationAck {\n ok: boolean\n /** 저쪽 문서·작업 식별자. 없으면 무엇을 되돌릴지 지목할 수 없다. */\n ref?: string\n /** 받아들였으나 저쪽이 덧붙인 말 — **실패가 아니다.** 원인 쪽 문장. */\n note?: string\n /** 그 말 중 다음에 할 일 한 줄 — 붙여 보내지 않는다. */\n noteNext?: string\n error?: string\n /** 다시 해서 될 일인가 — 실패에만. 말하지 않으면 일꾼이 집지 않는다. */\n retry?: 'again' | 'after-fix' | 'never'\n}\n\n/**\n * 저장소를 **한 번만** 읽고, 판정과 함께 부를 것까지 돌려준다.\n *\n * 판정한 뒤 보낼 때 다시 읽으면 그 사이에 연결 설정이 바뀔 수 있다 — 판정한 커넥터와 실제로 부르는\n * 커넥터가 달라진다. 그리고 같은 행을 두 번 읽을 이유가 없다.\n */\nasync function resolve(domainId: string, instanceId: string) {\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n const ref = inst\n ? await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })\n : null\n const adapter = ref ? getAdapter(ref.adapterType) : undefined\n\n const target = actuationTarget({\n instanceId,\n twinExists: !!inst,\n siteId: (inst?.origin as any)?.siteId,\n ...(ref ? { ref: { source: ref.source, adapterType: ref.adapterType } } : {}),\n adapterRegistered: !!adapter,\n capabilities: capabilitiesOf(adapter),\n declaresActuate: !!adapter?.capabilities?.includes('actuate'),\n hasActuateImpl: typeof adapter?.actuate === 'function'\n })\n\n return { target, ref, adapter }\n}\n\n/**\n * 이 트윈의 원본에 승인된 조치를 보낸다.\n *\n * **거절할 때는 무엇이 없어서인지 말한다.** 「보내지 못했습니다」만 답하면 사람이 연결 설정을 열어야\n * 하는지, 커넥터를 고쳐야 하는지, 트윈을 다시 만들어야 하는지 알 수 없다.\n */\nexport async function routeActuation(\n domainId: string,\n instanceId: string,\n command: ApprovedCommand\n): Promise<ActuationAck> {\n const { target, ref, adapter } = await resolve(domainId, instanceId)\n if (!isActuationAllowed(target)) return { ok: false, error: `${target.detail}. ${APPROVAL_STANDS}` }\n\n /*\n * 여기까지 왔으면 판정이 그 둘의 존재를 이미 확인했다(§`actuationTarget`). 그래도 조용히 지나가지\n * 않는다 — 판정과 배선이 어긋나면 그것이 결함이고, `!` 로 덮으면 그 결함이 「아무 일도 일어나지\n * 않았다」로 나타난다.\n */\n if (!adapter?.actuate) {\n return { ok: false, error: `커넥터를 찾았는데 조치를 보낼 자리가 없습니다 (${target.adapterType}). ${APPROVAL_STANDS}` }\n }\n\n const site: SiteDescriptor = { siteId: target.siteId }\n const result = await adapter.actuate(ref?.connectionConfig ?? {}, site, command)\n\n /*\n * **왕복이 일어났다는 사실을 남긴다** (ADR-0046 ②).\n *\n * 설정이 맞는 것과 저쪽이 받는 것은 다르고, 그 차이는 한 번 넘겨 봐야만 안다. 승인 화면이 그것을\n * 미리 말하려면 지난 왕복의 결과가 어딘가에 남아 있어야 한다 — 없으면 화면은 매번 「해 봐야 안다」만\n * 말한다.\n *\n * 두 경로(`dispatchAfterApproval` · `sweepPendingDispatch`)가 다 이 함수를 지나므로 여기 한 곳이면\n * 된다. 실패해도 조치 결과는 그대로 돌려준다 — 기록이 본업을 막으면 안 되고, 삼키지도 않는다.\n */\n if (ref) await recordActuationOutcome(ref, result).catch(err => twinError('[reference] actuation state not recorded', err))\n\n return result\n}\n\n/**\n * 조치 결과를 레퍼런스에 적는다 — **코드만, 문장은 아니다.**\n *\n * 거절 사유에 코드가 없으면 `actuationFailure` 를 비워 둔다. 자리를 채우려고 `error` 문장을 옮기면\n * `lastError` 가 이미 겪은 것을 반복한다 — 저장된 문장은 번역도 집계도 안 되고, 커넥터가 그 문장을\n * 고치는 날 옛 행과 새 행이 다른 말을 한다. **모르는 것은 안 적는다.**\n */\nasync function recordActuationOutcome(ref: TwinReference, result: ActuationResult): Promise<void> {\n await getRepository(TwinReference).update({ id: ref.id }, actuationOutcomePatch(result, new Date().toISOString()))\n}\n\n/**\n * 이 트윈이 조치를 받을 수 있나 — **넘기기 전에 화면이 먼저 알 수 있게.**\n *\n * 승인 화면이 「넘기기」를 보이기 전에 이것을 묻는다. 누를 수 없는 것을 보여 주고 눌렀을 때 거절하는\n * 것보다, 왜 못 넘기는지 먼저 말하는 것이 낫다.\n */\nexport async function actuationTargetOf(domainId: string, instanceId: string): Promise<ActuationTarget> {\n return (await resolve(domainId, instanceId)).target\n}\n\n/**\n * **저쪽이 받을 준비가 됐나** — 넘기기 전에 묻는다. 화면이 사람을 헛걸음시키지 않게.\n *\n * ── 왜 `actuationTargetOf` 와 따로인가 ──────────────────────────────────────\n * 그쪽은 **망을 타지 않는다.** 화면이 목록을 그릴 때마다 부를 수 있어야 하므로 그래야 한다.\n * 이 함수는 커넥터가 저쪽에 물을 수 있고, 그래서 부르는 시점이 다르다 — 사람이 「넘기기」를\n * 열었을 때다.\n *\n * ── 답이 문이 아니다 ────────────────────────────────────────────────────────\n * **이 답으로 넘김을 막지 않는다.** 막으면 점검이 틀렸거나 잠깐 못 닿은 사이에 성공할 수 있었던\n * 조치가 못 나간다. 화면이 미리 말하는 데까지가 이 함수의 일이다.\n *\n * 커넥터가 이 얼굴을 안 만들었으면 **「모른다」로 답한다** — 「준비됐다」가 아니다. 없는 확인을\n * 통과로 읽으면 이 함수가 있으나 없으나 같아진다.\n */\nexport async function actuationReadinessOf(domainId: string, instanceId: string): Promise<ActuationReadiness> {\n const { target, ref, adapter } = await resolve(domainId, instanceId)\n\n /* 보낼 대상 자체가 없으면 저쪽에 물을 것도 없다 — 그 사실을 그대로 옮긴다. */\n if (!isActuationAllowed(target)) {\n return { checked: 'local', ready: false, reason: target.detail, code: target.code }\n }\n\n if (!adapter?.actuationReadiness) {\n /* 확인할 길이 없다. 「준비됐다」로 답하지 않는다 — 그러면 화면이 없는 확인을 통과로 읽는다. */\n return {\n checked: 'local',\n reason: `커넥터 \"${target.adapterType}\" 는 저쪽이 받을 준비가 됐는지 알려 주지 않습니다 — 넘겨 보고 알게 됩니다`,\n code: 'not-checkable'\n }\n }\n\n try {\n return await adapter.actuationReadiness(ref?.connectionConfig ?? {}, { siteId: target.siteId })\n } catch (e: any) {\n /*\n * 점검이 터진 것을 「저쪽이 죽었다」로 옮기지 않는다 — 우리 점검의 실패다. 그리고 던지지 않는다:\n * 이 함수 때문에 승인 화면이 안 열리면, 알려 주려던 것보다 큰 것을 잃는다.\n */\n return {\n checked: 'local',\n reason: `준비 상태를 확인하다 실패했습니다 — ${String(e?.message ?? e).slice(0, 160)}`,\n code: 'check-failed'\n }\n }\n}\n"]}
1
+ {"version":3,"file":"actuation-routing.js","sourceRoot":"","sources":["../../../server/service/reference/actuation-routing.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **승인된 조치가 원본으로 나가는 길** — 판단은 `actuation-target.ts`, 여기는 배선.\n *\n * ── 무엇이 끊겨 있었나 (2026-09-02) ────────────────────────────────────────────\n * MES 커넥터는 `actuate` 를 다 만들어 두었다. 서명·접수 판정·초안 상태까지 있었다. 그런데 넘기는\n * 리졸버가 **어댑터를 찾지도 않고** 「구현이 없습니다」라고 답했다. 그 문장을 사실로 읽어서 설계\n * 문서에도, 다른 레인에도 그렇게 알렸다 — 둘 다 틀렸다.\n *\n * 자리는 있고 길이 없는 상태다. 이 저장소에서 가장 자주 나는 결함 부류이고, 조치 경로에서 나면\n * 결과가 하나다 — **승인은 쌓이는데 현장에는 아무것도 가지 않는다.**\n *\n * ── 왜 구동과 따로 두나 ───────────────────────────────────────────────────────\n * 모양이 같아서 한 함수로 묶고 싶어진다. 묶지 않는다.\n *\n * 구동은 시뮬레이터를 움직인다 틀리면 시늉이 어긋난다\n * 조치는 현장에 작업지시를 낸다 틀리면 만들지 않아도 될 것을 만든다\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport type { ApprovedCommand } from '@operato/ops-contract'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\n\nimport { TwinReference } from './twin-reference.js'\nimport { twinError } from '../../engine/log.js'\nimport { actuationOutcomePatch } from './actuation-verification.js'\nimport { capabilitiesOf, getAdapter, type ActuationReadiness, type ActuationResult, type SiteDescriptor } from './reference-adapter.js'\nimport { APPROVAL_STANDS, actuationTarget, isActuationAllowed, type ActuationTarget } from './actuation-target.js'\n\n/** 어댑터가 답하는 모양 그대로 — 디스패처가 이 값을 커맨드 행에 적는다. */\n/**\n * 조치를 낸 결과 — **어댑터가 답한 것을 좁히지 않고 그대로 나른다.**\n *\n * ── 왜 넓혔나 (2026-09-04, 인티그레이션 레인이 찾음) ────────────────────────\n * 이 타입이 `ok`·`ref`·`error` 셋만 들고 있었다. 그래서 `routeActuation` 이 어댑터의 답을\n * 그대로 되돌리는데 **타입은 `note`·`noteNext`·`retry` 를 버린다고 말했다.**\n *\n * 값은 흘렀다 — JS 객체가 키를 안 버리므로. 어제 `dispatch_note` 에 실제로 앉았다. 그런데\n * **커넥터를 만드는 사람이 그 칸이 있다는 것을 알 방법이 없었다.** 우리가 어제 하루 종일\n * 「자리는 있고 길이 없다」로 부른 것의 타입 버전이다.\n *\n * 좁히지 않는다: 세 칸을 다 실어 `CommandAck` 와 같은 모양으로 둔다. 커널이 어느 칸을 어떻게\n * 쓰는지는 `dispatch` 가 정하고, 이 자리는 나르기만 한다.\n */\nexport interface ActuationAck {\n ok: boolean\n /** 저쪽 문서·작업 식별자. 없으면 무엇을 되돌릴지 지목할 수 없다. */\n ref?: string\n /** 받아들였으나 저쪽이 덧붙인 말 — **실패가 아니다.** 원인 쪽 문장. */\n note?: string\n /** 그 말 중 다음에 할 일 한 줄 — 붙여 보내지 않는다. */\n noteNext?: string\n error?: string\n /** 다시 해서 될 일인가 — 실패에만. 말하지 않으면 일꾼이 집지 않는다. */\n retry?: 'again' | 'after-fix' | 'never'\n}\n\n/**\n * 저장소를 **한 번만** 읽고, 판정과 함께 부를 것까지 돌려준다.\n *\n * 판정한 뒤 보낼 때 다시 읽으면 그 사이에 연결 설정이 바뀔 수 있다 — 판정한 커넥터와 실제로 부르는\n * 커넥터가 달라진다. 그리고 같은 행을 두 번 읽을 이유가 없다.\n */\nasync function resolve(domainId: string, instanceId: string) {\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n const ref = inst\n ? await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })\n : null\n const adapter = ref ? getAdapter(ref.adapterType) : undefined\n\n const target = actuationTarget({\n instanceId,\n twinExists: !!inst,\n siteId: (inst?.origin as any)?.siteId,\n ...(ref ? { ref: { source: ref.source, adapterType: ref.adapterType } } : {}),\n adapterRegistered: !!adapter,\n capabilities: capabilitiesOf(adapter),\n declaresActuate: !!adapter?.capabilities?.includes('actuate'),\n hasActuateImpl: typeof adapter?.actuate === 'function'\n })\n\n return { target, ref, adapter }\n}\n\n/**\n * 이 트윈의 원본에 승인된 조치를 보낸다.\n *\n * **거절할 때는 무엇이 없어서인지 말한다.** 「보내지 못했습니다」만 답하면 사람이 연결 설정을 열어야\n * 하는지, 커넥터를 고쳐야 하는지, 트윈을 다시 만들어야 하는지 알 수 없다.\n */\nexport async function routeActuation(\n domainId: string,\n instanceId: string,\n command: ApprovedCommand\n): Promise<ActuationAck> {\n const { target, ref, adapter } = await resolve(domainId, instanceId)\n if (!isActuationAllowed(target)) return { ok: false, error: `${target.detail}. ${APPROVAL_STANDS}` }\n\n /*\n * 여기까지 왔으면 판정이 그 둘의 존재를 이미 확인했다(§`actuationTarget`). 그래도 조용히 지나가지\n * 않는다 — 판정과 배선이 어긋나면 그것이 결함이고, `!` 로 덮으면 그 결함이 「아무 일도 일어나지\n * 않았다」로 나타난다.\n */\n if (!adapter?.actuate) {\n return { ok: false, error: `커넥터를 찾았는데 조치를 보낼 자리가 없습니다 (${target.adapterType}). ${APPROVAL_STANDS}` }\n }\n\n const site: SiteDescriptor = { siteId: target.siteId }\n const result = await adapter.actuate(ref?.connectionConfig ?? {}, site, command)\n\n /*\n * **왕복이 일어났다는 사실을 남긴다** (ADR-0046 ②).\n *\n * 설정이 맞는 것과 저쪽이 받는 것은 다르고, 그 차이는 한 번 넘겨 봐야만 안다. 승인 화면이 그것을\n * 미리 말하려면 지난 왕복의 결과가 어딘가에 남아 있어야 한다 — 없으면 화면은 매번 「해 봐야 안다」만\n * 말한다.\n *\n * 두 경로(`dispatchAfterApproval` · `sweepPendingDispatch`)가 다 이 함수를 지나므로 여기 한 곳이면\n * 된다. 실패해도 조치 결과는 그대로 돌려준다 — 기록이 본업을 막으면 안 되고, 삼키지도 않는다.\n */\n if (ref) await recordActuationOutcome(ref, result).catch(err => twinError('[reference] actuation state not recorded', err))\n\n return result\n}\n\n/**\n * 조치 결과를 레퍼런스에 적는다 — **코드만, 문장은 아니다.**\n *\n * 거절 사유에 코드가 없으면 `actuationFailure` 를 비워 둔다. 자리를 채우려고 `error` 문장을 옮기면\n * `lastError` 가 이미 겪은 것을 반복한다 — 저장된 문장은 번역도 집계도 안 되고, 커넥터가 그 문장을\n * 고치는 날 옛 행과 새 행이 다른 말을 한다. **모르는 것은 안 적는다.**\n */\nasync function recordActuationOutcome(ref: TwinReference, result: ActuationResult): Promise<void> {\n await getRepository(TwinReference).update({ id: ref.id }, actuationOutcomePatch(result, new Date().toISOString()))\n}\n\n/**\n * 이 트윈이 조치를 받을 수 있나 — **넘기기 전에 화면이 먼저 알 수 있게.**\n *\n * 승인 화면이 「넘기기」를 보이기 전에 이것을 묻는다. 누를 수 없는 것을 보여 주고 눌렀을 때 거절하는\n * 것보다, 왜 못 넘기는지 먼저 말하는 것이 낫다.\n */\nexport async function actuationTargetOf(domainId: string, instanceId: string): Promise<ActuationTarget> {\n return (await resolve(domainId, instanceId)).target\n}\n\n/**\n * **저쪽이 받을 준비가 됐나** — 넘기기 전에 묻는다. 화면이 사람을 헛걸음시키지 않게.\n *\n * ── 왜 `actuationTargetOf` 와 따로인가 ──────────────────────────────────────\n * 그쪽은 **망을 타지 않는다.** 화면이 목록을 그릴 때마다 부를 수 있어야 하므로 그래야 한다.\n * 이 함수는 커넥터가 저쪽에 물을 수 있고, 그래서 부르는 시점이 다르다 — 사람이 「넘기기」를\n * 열었을 때다.\n *\n * ── 답이 문이 아니다 ────────────────────────────────────────────────────────\n * **이 답으로 넘김을 막지 않는다.** 막으면 점검이 틀렸거나 잠깐 못 닿은 사이에 성공할 수 있었던\n * 조치가 못 나간다. 화면이 미리 말하는 데까지가 이 함수의 일이다.\n *\n * 커넥터가 이 얼굴을 안 만들었으면 **「모른다」로 답한다** — 「준비됐다」가 아니다. 없는 확인을\n * 통과로 읽으면 이 함수가 있으나 없으나 같아진다.\n */\nexport async function actuationReadinessOf(domainId: string, instanceId: string): Promise<ActuationReadiness> {\n const { target, ref, adapter } = await resolve(domainId, instanceId)\n\n /* 보낼 대상 자체가 없으면 저쪽에 물을 것도 없다 — 그 사실을 그대로 옮긴다. */\n if (!isActuationAllowed(target)) {\n return { checked: 'local', ready: false, reason: target.detail, code: target.code }\n }\n\n if (!adapter?.actuationReadiness) {\n /* 확인할 길이 없다. 「준비됐다」로 답하지 않는다 — 그러면 화면이 없는 확인을 통과로 읽는다. */\n return {\n checked: 'local',\n reason: `커넥터 \"${target.adapterType}\" 는 저쪽이 받을 준비가 됐는지 알려 주지 않습니다 — 넘겨 보고 알게 됩니다`,\n code: 'not-checkable'\n }\n }\n\n try {\n return await adapter.actuationReadiness(ref?.connectionConfig ?? {}, { siteId: target.siteId })\n } catch (e: any) {\n /*\n * 점검이 터진 것을 「저쪽이 죽었다」로 옮기지 않는다 — 우리 점검의 실패다. 그리고 던지지 않는다:\n * 이 함수 때문에 승인 화면이 안 열리면, 알려 주려던 것보다 큰 것을 잃는다.\n */\n return {\n checked: 'local',\n reason: `준비 상태를 확인하다 실패했습니다 — ${String(e?.message ?? e).slice(0, 160)}`,\n code: 'check-failed'\n }\n }\n}\n"]}
@@ -105,7 +105,7 @@ export declare function sayPlan(plan: ImportPlan): string;
105
105
  *
106
106
  * 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이
107
107
  * 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽
108
- * 판본입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.
108
+ * 버전입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.
109
109
  *
110
110
  * 낙타 이름의 경계를 밑줄로 가릅니다 — 안 그러면 `HOOKSECRET` 과 `HOOK_SECRET` 이 갈려 한쪽만 읽힙니다.
111
111
  */
@@ -116,7 +116,7 @@ availableSecrets = () => false) {
116
116
  return { actions: [], needsHuman: [], collisions: [], errors: [`이 파일은 연결 설정 파일가 아닙니다(kind: ${JSON.stringify(b.kind ?? null)})`] };
117
117
  }
118
118
  if (b.version !== 1)
119
- errors.push(`모르는 판입니다(version: ${JSON.stringify(b.version)}) — 읽을 수 있는 것만 읽습니다`);
119
+ errors.push(`모르는 버전입니다(version: ${JSON.stringify(b.version)}) — 읽을 수 있는 것만 읽습니다`);
120
120
  if (!Array.isArray(b.connections))
121
121
  return { actions: [], needsHuman: [], collisions: [], errors: [...errors, 'connections 가 목록이 아닙니다'] };
122
122
  const have = new Map(existing.map(e => [e.source, e]));
@@ -181,7 +181,7 @@ function sayPlan(plan) {
181
181
  *
182
182
  * 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이
183
183
  * 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽
184
- * 판본입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.
184
+ * 버전입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.
185
185
  *
186
186
  * 낙타 이름의 경계를 밑줄로 가릅니다 — 안 그러면 `HOOKSECRET` 과 `HOOK_SECRET` 이 갈려 한쪽만 읽힙니다.
187
187
  */
@@ -1 +1 @@
1
- {"version":3,"file":"connection-portability.js","sourceRoot":"","sources":["../../../server/service/reference/connection-portability.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **연결을 내보내고 다시 들여옵니다** — 실제 셋업을 파일로 보관하고 되세웁니다.\n *\n * ── 이것은 데모가 아니라 제품 기능입니다 (2026-09-05) ───────────────────────\n * 데모 시드(`seed-demo-twins.ts`)와 **다른 것**입니다. 그쪽은 내장 가상 마스터로 보여 줄 트윈을\n * 만들고 `TWIN_PROVISION_DEMO` 뒤에 숨습니다. 이것은 **실제로 붙인 연결을 옮기는 길**이라\n * 데모 스위치 뒤에 두지 않습니다 — 백업·이관·복구가 데모일 수 없습니다.\n *\n * 그래서 특정 공장을 제품 코드에 적지 않습니다. 적으면 배포마다 남의 공장이 하나씩 따라가고,\n * 데모 시드 머리말이 적어 둔 「프로덕션 배포는 **트윈 0** 에서 시작한다」가 깨집니다. 무엇을 세울지는\n * **파일이 말하고 코드는 그 파일을 읽을 줄만 압니다.**\n *\n * ── 왜 필요했나 ────────────────────────────────────────────────────────────\n * 실 MES 두 공장을 트윈에 세웠는데(`mes-line-a` · `mes-miratek`) **그 셋업이 한 데이터베이스\n * 안에만 있었습니다.** 잃으면 다시 손으로 만들어야 하고, 다른 배포에 옮길 길도 없었습니다.\n *\n * ── 담는 것과 안 담는 것 ────────────────────────────────────────────────────\n *\n * 담는다 연결 선언 — 주소 · 도메인 · 사이트 · 치수 · 이름공간 · 받는 자리\n * 안 담는다 비밀값 — 토큰 · 웹훅 서명값 · 받는 쪽 비밀값\n * 안 담는다 사실(저널) — 원본이 다시 보내면 된다. 담으면 **가짜 과거가 배포마다 복제된다**\n * 안 담는다 마스터에서 온 트윈 모델 — 연결이 있으면 재동기화가 다시 만든다\n *\n * **비밀값은 이름만 적습니다.** 어느 칸이 채워져 있었는지는 알려 주되 값은 안 나갑니다 — 파일은\n * 주고받는 물건이고, 한 번 나간 비밀값은 되돌릴 수 없습니다. 들여올 때 「이 셋이 없다」고 말하고\n * 사람이 연결 화면에서 채웁니다.\n *\n * ── 왜 사실을 안 담나 ───────────────────────────────────────────────────────\n * 저널은 되풀이 읽히고 미러로 복제되고 구독으로 방송됩니다. 내보낸 사실을 다른 배포에 들여오면\n * **일어나지 않은 일이 그 공장의 지난 기록에 섭니다.** 커서만 옮겨 두면 원본이 그 뒤부터 다시\n * 보내 주므로, 사실은 원본에서 오는 것이 맞습니다.\n */\n\n/** 값이 아니라 **채워져 있었다는 사실**만 나가는 칸들. */\nexport const SECRET_KEYS = ['token', 'hookSecret', 'intakeSecret', 'password', 'apiKey'] as const\n\nexport interface PortableConnection {\n source: string\n system?: string\n siteName?: string\n adapterType: string\n /** 비밀값을 뺀 연결 설정. */\n config: Record<string, unknown>\n /** 내보낼 때 채워져 있던 비밀 칸 이름 — 값은 없습니다. */\n secretsRequired: string[]\n /** 이 연결이 무엇을 세우나. */\n scope?: unknown\n}\n\nexport interface ConnectionBundle {\n kind: 'operato-twin/connections'\n version: 1\n exportedAt: string\n /** 어디서 내보냈나 — 사람이 나중에 「이게 어느 배포 것인가」를 물을 때 답합니다. */\n exportedFrom?: { domain?: string; note?: string }\n connections: PortableConnection[]\n}\n\n/**\n * 무엇을 세우나 **선언만** 남깁니다 — 지난 동기화의 **측정값은 뺍니다.**\n *\n * `scopeSpec` 에는 `produced`(선언) 옆에 `assessments`(그때 몇 개가 섰나) · `lastFailures` 가 함께\n * 삽니다. 그것까지 내보내면 새 배포가 **다른 기계에서 잰 수**를 자기 것으로 보입니다 — 「설비 42」가\n * 아무것도 안 세운 배포의 화면에 서고, 그 수가 어디서 왔는지 아무도 모릅니다.\n *\n * 재는 것은 그 배포에서 다시 재야 합니다. 오늘 「설정을 고쳤다」와 「모델이 다시 섰다」를 가른 것과\n * 같은 구별입니다.\n */\nfunction producedOf(scopeSpec: any): { produced: unknown } | undefined {\n const produced = scopeSpec?.produced\n return Array.isArray(produced) && produced.length ? { produced } : undefined\n}\n\nconst isSecret = (key: string): boolean =>\n (SECRET_KEYS as readonly string[]).includes(key) || /secret|token|password|apikey/i.test(key)\n\n/**\n * 연결 행들을 옮길 수 있는 모양으로 — **비밀값을 빼고**.\n *\n * 빼는 판정을 이름으로 합니다. 목록으로만 하면 커넥터가 새 비밀 칸을 더할 때 그 값이 조용히\n * 파일로 나갑니다 — 이름 규칙을 함께 두어 **새 칸이 기본으로 안 나가게** 합니다.\n */\nexport function exportConnections(\n rows: readonly { source: string; system?: string; siteName?: string; adapterType: string; connectionConfig?: any; scopeSpec?: any }[],\n meta: { at: string; domain?: string; note?: string }\n): ConnectionBundle {\n return {\n kind: 'operato-twin/connections',\n version: 1,\n exportedAt: meta.at,\n ...(meta.domain || meta.note ? { exportedFrom: { ...(meta.domain ? { domain: meta.domain } : {}), ...(meta.note ? { note: meta.note } : {}) } } : {}),\n connections: rows.map(r => {\n const cfg = (r.connectionConfig ?? {}) as Record<string, unknown>\n const kept: Record<string, unknown> = {}\n const secrets: string[] = []\n for (const [k, v] of Object.entries(cfg)) {\n if (isSecret(k)) {\n /* 비어 있던 칸은 적지 않습니다 — 「채워야 한다」와 「이 커넥터에 그런 칸이 있다」는 다릅니다. */\n if (String(v ?? '').trim()) secrets.push(k)\n continue\n }\n kept[k] = v\n }\n return {\n source: r.source,\n ...(r.system ? { system: r.system } : {}),\n ...(r.siteName ? { siteName: r.siteName } : {}),\n adapterType: r.adapterType,\n config: kept,\n secretsRequired: secrets.sort(),\n ...(producedOf(r.scopeSpec) ? { scope: producedOf(r.scopeSpec) } : {})\n }\n })\n }\n}\n\nexport type ImportAction =\n | { action: 'create'; connection: PortableConnection; missingSecrets: string[] }\n | { action: 'update'; connection: PortableConnection; missingSecrets: string[]; keptSecrets: string[] }\n | { action: 'skip'; source: string; why: string }\n\nexport interface ImportPlan {\n actions: ImportAction[]\n /** 사람이 연결 화면에서 채워야 하는 것 — 이것이 비어야 「시드만으로 끝」입니다. */\n /**\n * 사람이 환경에 넣어야 하는 비밀값.\n *\n * `env` 를 함께 냅니다 — **그 이름을 사람이 손으로 적기 때문**입니다. 화면이 이름을 따로 만들면\n * 규칙이 두 곳에 살고, 갈리는 날 「넣었는데 안 읽힌다」가 됩니다. 실제로 났습니다(2026-09-06):\n * 화면이 `TWIN_SECRET__MES-MIRATEK__HOOKSECRET` 을 알려 줬는데 서버가 읽는 이름은\n * `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET` 이었습니다. 하이픈과 낙타 경계 둘 다 달랐습니다.\n *\n * `keys` 와 순서가 같습니다 — `keys[i]` 의 이름이 `env[i]` 입니다.\n */\n needsHuman: { source: string; keys: string[]; env: string[] }[]\n /** 들여온 **뒤에** 두 연결이 같은 곳에 조치를 내게 되나 — 있으면 들여오면 안 됩니다. */\n collisions: { intake: string; sources: string[] }[]\n errors: string[]\n}\n\n/**\n * 들여오기 **계획**을 먼저 냅니다 — 쓰기 전에 무엇이 일어날지 사람이 봅니다.\n *\n * ── 이미 있는 연결을 덮지 않습니다 ──────────────────────────────────────────\n * 같은 이름이 이미 있으면 **선언만 갱신하고 비밀값은 그대로 둡니다.** 사람이 화면에서 넣은 값을\n * 파일이 지우면, 파일 하나 들여왔다가 도는 연결이 멈춥니다. 데모 시드도 같은 규율을 적어 두었습니다\n * (「옆 설정을 지우지 않는다 — 시드는 자기가 아는 것만 채운다」).\n *\n * ── 없는 비밀값을 지어내지 않습니다 ─────────────────────────────────────────\n * 빈 값으로 채워 두면 연결이 서고 **동기화할 때 처음 실패합니다.** 그때는 무엇이 없어서인지 화면에서\n * 멉니다. 여기서 이름을 대고 멈추는 것이 낫습니다.\n */\nexport function planImport(\n bundle: unknown,\n existing: readonly { source: string; connectionConfig?: any }[],\n /** 지금 환경이 줄 수 있는 비밀값 이름들 — 값은 안 받습니다(이 함수는 값을 만지지 않습니다). */\n availableSecrets: (source: string, key: string) => boolean = () => false\n): ImportPlan {\n const errors: string[] = []\n const b = bundle as ConnectionBundle\n if (!b || typeof b !== 'object') return { actions: [], needsHuman: [], collisions: [], errors: ['들여올 것이 객체가 아닙니다'] }\n if (b.kind !== 'operato-twin/connections') {\n return { actions: [], needsHuman: [], collisions: [], errors: [`이 파일은 연결 설정 파일가 아닙니다(kind: ${JSON.stringify((b as any).kind ?? null)})`] }\n }\n if (b.version !== 1) errors.push(`모르는 판입니다(version: ${JSON.stringify((b as any).version)}) — 읽을 수 있는 것만 읽습니다`)\n if (!Array.isArray(b.connections)) return { actions: [], needsHuman: [], collisions: [], errors: [...errors, 'connections 가 목록이 아닙니다'] }\n\n const have = new Map(existing.map(e => [e.source, e]))\n const seen = new Set<string>()\n const actions: ImportAction[] = []\n const needsHuman: { source: string; keys: string[]; env: string[] }[] = []\n\n for (const c of b.connections) {\n const source = String(c?.source ?? '').trim()\n if (!source) {\n errors.push('이름 없는 연결이 있습니다 — 건너뜁니다')\n continue\n }\n if (seen.has(source)) {\n actions.push({ action: 'skip', source, why: '같은 이름이 파일에 두 번 있습니다' })\n continue\n }\n seen.add(source)\n if (!String(c?.adapterType ?? '').trim()) {\n actions.push({ action: 'skip', source, why: '커넥터 종류가 없습니다 — 무엇으로 붙일지 모릅니다' })\n continue\n }\n\n const cur = have.get(source)\n const curCfg = (cur?.connectionConfig ?? {}) as Record<string, unknown>\n const wanted = Array.isArray(c.secretsRequired) ? c.secretsRequired.map(String) : []\n const kept = wanted.filter(k => String(curCfg[k] ?? '').trim())\n const missing = wanted.filter(k => !kept.includes(k) && !availableSecrets(source, k))\n\n if (missing.length) needsHuman.push({ source, keys: missing, env: missing.map(k => secretEnvName(source, k)) })\n actions.push(\n cur\n ? { action: 'update', connection: c, missingSecrets: missing, keptSecrets: kept }\n : { action: 'create', connection: c, missingSecrets: missing }\n )\n }\n\n /*\n * **들여온 뒤의 모습으로 봅니다.** 파일 안에서만 보면 「이미 있는 연결과 부딪히는 것」을 못\n * 잡습니다 — 그게 실제로 더 쉽게 나는 쪽입니다(하나는 화면에서, 하나는 파일에서 생깁니다).\n */\n const after = [\n /* 행 모양 그대로 넘긴다 — 옮기는 자리는 `intakeCollisions` 안 한 곳이다(ADR-0053 ①). */\n ...existing.filter(e => !seen.has(e.source)),\n ...actions\n .filter((a): a is Extract<ImportAction, { connection: PortableConnection }> => a.action !== 'skip')\n .map(a => ({ source: a.connection.source, config: a.connection.config }))\n ]\n return { actions, needsHuman, collisions: intakeCollisions(after), errors }\n}\n\n/** 사람이 읽는 한 줄 — 화면과 로그가 같은 문장을 쓰게. */\nexport function sayPlan(plan: ImportPlan): string {\n const n = (a: ImportAction['action']) => plan.actions.filter(x => x.action === a).length\n const head = `연결 ${plan.actions.length}개 — 새로 ${n('create')} · 갱신 ${n('update')} · 건너뜀 ${n('skip')}`\n /* 부딪힘을 먼저 말합니다 — 비밀값이 없는 것은 안 도는 것이고, 부딪히는 것은 **잘못 도는 것**입니다. */\n if (plan.collisions.length) {\n const who = plan.collisions.map(c => `${c.sources.join(' · ')} → ${c.intake}`).join(', ')\n return `${head}. ⚠ **두 연결이 같은 곳에 조치를 냅니다** — ${who}. 다른 공장의 작업지시가 한 공장에 섭니다`\n }\n if (!plan.needsHuman.length) return `${head}. 비밀값은 다 있습니다`\n const who = plan.needsHuman.map(h => `${h.source}(${h.keys.join(' · ')})`).join(', ')\n return `${head}. **비밀값을 사람이 넣어야 합니다** — ${who}. 연결 화면에서 채우면 그때부터 돕니다`\n}\n\n/**\n * 비밀값이 사는 **환경 변수 이름** — `mes-miratek` · `hookSecret` → `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET`.\n *\n * 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이\n * 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽\n * 판본입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.\n *\n * 낙타 이름의 경계를 밑줄로 가릅니다 — 안 그러면 `HOOKSECRET` 과 `HOOK_SECRET` 이 갈려 한쪽만 읽힙니다.\n */\nexport function secretEnvName(source: string, key: string): string {\n const up = (x: string) =>\n x\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toUpperCase()\n return `TWIN_SECRET__${up(source)}__${up(key)}`\n}\n\n/**\n * **두 연결이 같은 곳에 조치를 내지 않나** — 같으면 다른 공장의 작업지시가 한 공장에 섭니다.\n *\n * ── 왜 이 검사가 생겼나 (2026-09-05) ────────────────────────────────────────\n * `mes-miratek` 이 서면서 한 커넥터가 처음으로 **공장 둘**을 달았습니다. 조치가 나가는 주소는\n * `intakeSiteId` 가 정하는데(`operato-plant.ts:389`), 그 값이 트윈의 사이트 이름을 **이깁니다** —\n * 트윈에서는 `mes-line-a` 인데 MES 의 도메인은 `system` 이라 그 override 가 필요합니다.\n *\n * 그래서 **연결을 베껴 만들고 그 칸만 안 고치면** 두 트윈의 조치가 같은 공장으로 갑니다. 오류가\n * 안 납니다 — 저쪽은 정상적인 작업지시로 받고 **실제로 만듭니다.**\n *\n * 게이트에 이미 「사이트를 모르면 보내지 않는다」가 있는데(`no-site`), 그 이유가 적혀 있는 문장이\n * **「잘못된 공장이 실제로 만든다」** 입니다. 이것은 그 둘째 길입니다 — 사이트를 아는데 **틀린\n * 사이트를 압니다.**\n *\n * 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 패키지를 베껴 이름만 바꾸면 받는 자리가\n * 그대로 따라옵니다.\n */\nexport function intakeCollisions(\n connections: readonly {\n source: string\n adapterType?: string\n /** 패키지·계획이 쓰는 이름. */\n config?: Record<string, unknown> | null\n /** **DB 행이 쓰는 이름** — `TwinReference` 의 칸이다. 둘 다 받는다(ADR-0053 ①). */\n connectionConfig?: Record<string, unknown> | null\n }[]\n): { intake: string; sources: string[] }[] {\n const byIntake = new Map<string, string[]>()\n for (const c of connections) {\n /*\n * **모양이 둘인 것을 여기서 안다** (ADR-0053 ①, 2026-09-16).\n *\n * 패키지는 `config`, `TwinReference` 행은 `connectionConfig` 다. 전에는 `config` 만 읽어서, 행을\n * 그대로 넘기면 같은 충돌에도 `[]` 를 냈다 — 오류도 경고도 없이 **저장이 통과하고 가드는 선 채로\n * 아무 일도 안 한다.** 부르는 쪽마다 옮기게 두면 다음 소비처가 또 밟는다(이 함수는 화면이 쓰라고\n * 리졸버에서 다시 내보내진다).\n *\n * 「없다」가 편한 답인 자리라 더 위험하다 — 빨강이 아니라 **조용한 초록**으로 흐른다.\n */\n const cfg = c.config ?? c.connectionConfig ?? {}\n /* 받는 자리를 정하는 규칙은 커넥터의 것과 같아야 합니다 — 여기서 다시 지으면 두 벌이 됩니다. */\n const intake = String(cfg.intakeSiteId ?? cfg.siteId ?? '').trim()\n /* 조치를 낼 수 없는 연결은 부딪힐 것이 없습니다 — 주소가 없으면 아무 데도 안 갑니다. */\n if (!intake || !String(cfg.intakeEndpoint ?? '').trim()) continue\n const key = `${String(cfg.intakeEndpoint).trim().replace(/\\/$/, '')}::${intake}`\n byIntake.set(key, [...(byIntake.get(key) ?? []), c.source])\n }\n return [...byIntake.entries()]\n .filter(([, sources]) => sources.length > 1)\n .map(([intake, sources]) => ({ intake, sources: sources.sort() }))\n}\n\n/**\n * **화면 저장의 문** — 이 연결을 이 설정으로 저장하면 다른 연결과 받는 사이트가 겹치나 (ADR-0046 ②-1).\n *\n * 들여오기(`planImport`)는 `intakeCollisions` 로 거부하는데 화면 저장(`saveTwinReference`)은 부르지 않아서,\n * 연결을 베껴 이름만 바꾸면 경고 0 으로 두 트윈의 조치가 한 공장에 섰다. 판정은 같은 함수 하나로 한다.\n * 경고가 아니라 거부이고, 사람이 그 자리에서 고치도록 **이미 그 사이트를 쓰는 연결의 이름**을 싣는다.\n */\nexport function refuseIntakeCollision(\n source: string,\n config: Record<string, unknown> | undefined,\n others: readonly { source: string; siteName?: string | null; connectionConfig?: Record<string, unknown> | null }[]\n): { code: 'intake-site-taken'; message: string; params: { site: string; connection: string } } | null {\n const rest = others.filter(o => o.source !== source)\n /* `others` 는 DB 행이다 — 옮기지 않고 그대로 넘긴다(ADR-0053 ①). */\n const hit = intakeCollisions([...rest, { source, config: config ?? {} }]).find(c => c.sources.includes(source))\n if (!hit) return null\n const connection = rest\n .filter(o => hit.sources.includes(o.source))\n .map(o => o.siteName || o.source)\n .join(', ')\n const site = String(config?.intakeSiteId ?? config?.siteId ?? '').trim()\n return {\n code: 'intake-site-taken',\n message: `intake site ${site} is already used by ${connection}`,\n params: { site, connection }\n }\n}\n"]}
1
+ {"version":3,"file":"connection-portability.js","sourceRoot":"","sources":["../../../server/service/reference/connection-portability.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **연결을 내보내고 다시 들여옵니다** — 실제 셋업을 파일로 보관하고 되세웁니다.\n *\n * ── 이것은 데모가 아니라 제품 기능입니다 (2026-09-05) ───────────────────────\n * 데모 시드(`seed-demo-twins.ts`)와 **다른 것**입니다. 그쪽은 내장 가상 마스터로 보여 줄 트윈을\n * 만들고 `TWIN_PROVISION_DEMO` 뒤에 숨습니다. 이것은 **실제로 붙인 연결을 옮기는 길**이라\n * 데모 스위치 뒤에 두지 않습니다 — 백업·이관·복구가 데모일 수 없습니다.\n *\n * 그래서 특정 공장을 제품 코드에 적지 않습니다. 적으면 배포마다 남의 공장이 하나씩 따라가고,\n * 데모 시드 머리말이 적어 둔 「프로덕션 배포는 **트윈 0** 에서 시작한다」가 깨집니다. 무엇을 세울지는\n * **파일이 말하고 코드는 그 파일을 읽을 줄만 압니다.**\n *\n * ── 왜 필요했나 ────────────────────────────────────────────────────────────\n * 실 MES 두 공장을 트윈에 세웠는데(`mes-line-a` · `mes-miratek`) **그 셋업이 한 데이터베이스\n * 안에만 있었습니다.** 잃으면 다시 손으로 만들어야 하고, 다른 배포에 옮길 길도 없었습니다.\n *\n * ── 담는 것과 안 담는 것 ────────────────────────────────────────────────────\n *\n * 담는다 연결 선언 — 주소 · 도메인 · 사이트 · 치수 · 이름공간 · 받는 자리\n * 안 담는다 비밀값 — 토큰 · 웹훅 서명값 · 받는 쪽 비밀값\n * 안 담는다 사실(저널) — 원본이 다시 보내면 된다. 담으면 **가짜 과거가 배포마다 복제된다**\n * 안 담는다 마스터에서 온 트윈 모델 — 연결이 있으면 재동기화가 다시 만든다\n *\n * **비밀값은 이름만 적습니다.** 어느 칸이 채워져 있었는지는 알려 주되 값은 안 나갑니다 — 파일은\n * 주고받는 물건이고, 한 번 나간 비밀값은 되돌릴 수 없습니다. 들여올 때 「이 셋이 없다」고 말하고\n * 사람이 연결 화면에서 채웁니다.\n *\n * ── 왜 사실을 안 담나 ───────────────────────────────────────────────────────\n * 저널은 되풀이 읽히고 미러로 복제되고 구독으로 방송됩니다. 내보낸 사실을 다른 배포에 들여오면\n * **일어나지 않은 일이 그 공장의 지난 기록에 섭니다.** 커서만 옮겨 두면 원본이 그 뒤부터 다시\n * 보내 주므로, 사실은 원본에서 오는 것이 맞습니다.\n */\n\n/** 값이 아니라 **채워져 있었다는 사실**만 나가는 칸들. */\nexport const SECRET_KEYS = ['token', 'hookSecret', 'intakeSecret', 'password', 'apiKey'] as const\n\nexport interface PortableConnection {\n source: string\n system?: string\n siteName?: string\n adapterType: string\n /** 비밀값을 뺀 연결 설정. */\n config: Record<string, unknown>\n /** 내보낼 때 채워져 있던 비밀 칸 이름 — 값은 없습니다. */\n secretsRequired: string[]\n /** 이 연결이 무엇을 세우나. */\n scope?: unknown\n}\n\nexport interface ConnectionBundle {\n kind: 'operato-twin/connections'\n version: 1\n exportedAt: string\n /** 어디서 내보냈나 — 사람이 나중에 「이게 어느 배포 것인가」를 물을 때 답합니다. */\n exportedFrom?: { domain?: string; note?: string }\n connections: PortableConnection[]\n}\n\n/**\n * 무엇을 세우나 **선언만** 남깁니다 — 지난 동기화의 **측정값은 뺍니다.**\n *\n * `scopeSpec` 에는 `produced`(선언) 옆에 `assessments`(그때 몇 개가 섰나) · `lastFailures` 가 함께\n * 삽니다. 그것까지 내보내면 새 배포가 **다른 기계에서 잰 수**를 자기 것으로 보입니다 — 「설비 42」가\n * 아무것도 안 세운 배포의 화면에 서고, 그 수가 어디서 왔는지 아무도 모릅니다.\n *\n * 재는 것은 그 배포에서 다시 재야 합니다. 오늘 「설정을 고쳤다」와 「모델이 다시 섰다」를 가른 것과\n * 같은 구별입니다.\n */\nfunction producedOf(scopeSpec: any): { produced: unknown } | undefined {\n const produced = scopeSpec?.produced\n return Array.isArray(produced) && produced.length ? { produced } : undefined\n}\n\nconst isSecret = (key: string): boolean =>\n (SECRET_KEYS as readonly string[]).includes(key) || /secret|token|password|apikey/i.test(key)\n\n/**\n * 연결 행들을 옮길 수 있는 모양으로 — **비밀값을 빼고**.\n *\n * 빼는 판정을 이름으로 합니다. 목록으로만 하면 커넥터가 새 비밀 칸을 더할 때 그 값이 조용히\n * 파일로 나갑니다 — 이름 규칙을 함께 두어 **새 칸이 기본으로 안 나가게** 합니다.\n */\nexport function exportConnections(\n rows: readonly { source: string; system?: string; siteName?: string; adapterType: string; connectionConfig?: any; scopeSpec?: any }[],\n meta: { at: string; domain?: string; note?: string }\n): ConnectionBundle {\n return {\n kind: 'operato-twin/connections',\n version: 1,\n exportedAt: meta.at,\n ...(meta.domain || meta.note ? { exportedFrom: { ...(meta.domain ? { domain: meta.domain } : {}), ...(meta.note ? { note: meta.note } : {}) } } : {}),\n connections: rows.map(r => {\n const cfg = (r.connectionConfig ?? {}) as Record<string, unknown>\n const kept: Record<string, unknown> = {}\n const secrets: string[] = []\n for (const [k, v] of Object.entries(cfg)) {\n if (isSecret(k)) {\n /* 비어 있던 칸은 적지 않습니다 — 「채워야 한다」와 「이 커넥터에 그런 칸이 있다」는 다릅니다. */\n if (String(v ?? '').trim()) secrets.push(k)\n continue\n }\n kept[k] = v\n }\n return {\n source: r.source,\n ...(r.system ? { system: r.system } : {}),\n ...(r.siteName ? { siteName: r.siteName } : {}),\n adapterType: r.adapterType,\n config: kept,\n secretsRequired: secrets.sort(),\n ...(producedOf(r.scopeSpec) ? { scope: producedOf(r.scopeSpec) } : {})\n }\n })\n }\n}\n\nexport type ImportAction =\n | { action: 'create'; connection: PortableConnection; missingSecrets: string[] }\n | { action: 'update'; connection: PortableConnection; missingSecrets: string[]; keptSecrets: string[] }\n | { action: 'skip'; source: string; why: string }\n\nexport interface ImportPlan {\n actions: ImportAction[]\n /** 사람이 연결 화면에서 채워야 하는 것 — 이것이 비어야 「시드만으로 끝」입니다. */\n /**\n * 사람이 환경에 넣어야 하는 비밀값.\n *\n * `env` 를 함께 냅니다 — **그 이름을 사람이 손으로 적기 때문**입니다. 화면이 이름을 따로 만들면\n * 규칙이 두 곳에 살고, 갈리는 날 「넣었는데 안 읽힌다」가 됩니다. 실제로 났습니다(2026-09-06):\n * 화면이 `TWIN_SECRET__MES-MIRATEK__HOOKSECRET` 을 알려 줬는데 서버가 읽는 이름은\n * `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET` 이었습니다. 하이픈과 낙타 경계 둘 다 달랐습니다.\n *\n * `keys` 와 순서가 같습니다 — `keys[i]` 의 이름이 `env[i]` 입니다.\n */\n needsHuman: { source: string; keys: string[]; env: string[] }[]\n /** 들여온 **뒤에** 두 연결이 같은 곳에 조치를 내게 되나 — 있으면 들여오면 안 됩니다. */\n collisions: { intake: string; sources: string[] }[]\n errors: string[]\n}\n\n/**\n * 들여오기 **계획**을 먼저 냅니다 — 쓰기 전에 무엇이 일어날지 사람이 봅니다.\n *\n * ── 이미 있는 연결을 덮지 않습니다 ──────────────────────────────────────────\n * 같은 이름이 이미 있으면 **선언만 갱신하고 비밀값은 그대로 둡니다.** 사람이 화면에서 넣은 값을\n * 파일이 지우면, 파일 하나 들여왔다가 도는 연결이 멈춥니다. 데모 시드도 같은 규율을 적어 두었습니다\n * (「옆 설정을 지우지 않는다 — 시드는 자기가 아는 것만 채운다」).\n *\n * ── 없는 비밀값을 지어내지 않습니다 ─────────────────────────────────────────\n * 빈 값으로 채워 두면 연결이 서고 **동기화할 때 처음 실패합니다.** 그때는 무엇이 없어서인지 화면에서\n * 멉니다. 여기서 이름을 대고 멈추는 것이 낫습니다.\n */\nexport function planImport(\n bundle: unknown,\n existing: readonly { source: string; connectionConfig?: any }[],\n /** 지금 환경이 줄 수 있는 비밀값 이름들 — 값은 안 받습니다(이 함수는 값을 만지지 않습니다). */\n availableSecrets: (source: string, key: string) => boolean = () => false\n): ImportPlan {\n const errors: string[] = []\n const b = bundle as ConnectionBundle\n if (!b || typeof b !== 'object') return { actions: [], needsHuman: [], collisions: [], errors: ['들여올 것이 객체가 아닙니다'] }\n if (b.kind !== 'operato-twin/connections') {\n return { actions: [], needsHuman: [], collisions: [], errors: [`이 파일은 연결 설정 파일가 아닙니다(kind: ${JSON.stringify((b as any).kind ?? null)})`] }\n }\n if (b.version !== 1) errors.push(`모르는 버전입니다(version: ${JSON.stringify((b as any).version)}) — 읽을 수 있는 것만 읽습니다`)\n if (!Array.isArray(b.connections)) return { actions: [], needsHuman: [], collisions: [], errors: [...errors, 'connections 가 목록이 아닙니다'] }\n\n const have = new Map(existing.map(e => [e.source, e]))\n const seen = new Set<string>()\n const actions: ImportAction[] = []\n const needsHuman: { source: string; keys: string[]; env: string[] }[] = []\n\n for (const c of b.connections) {\n const source = String(c?.source ?? '').trim()\n if (!source) {\n errors.push('이름 없는 연결이 있습니다 — 건너뜁니다')\n continue\n }\n if (seen.has(source)) {\n actions.push({ action: 'skip', source, why: '같은 이름이 파일에 두 번 있습니다' })\n continue\n }\n seen.add(source)\n if (!String(c?.adapterType ?? '').trim()) {\n actions.push({ action: 'skip', source, why: '커넥터 종류가 없습니다 — 무엇으로 붙일지 모릅니다' })\n continue\n }\n\n const cur = have.get(source)\n const curCfg = (cur?.connectionConfig ?? {}) as Record<string, unknown>\n const wanted = Array.isArray(c.secretsRequired) ? c.secretsRequired.map(String) : []\n const kept = wanted.filter(k => String(curCfg[k] ?? '').trim())\n const missing = wanted.filter(k => !kept.includes(k) && !availableSecrets(source, k))\n\n if (missing.length) needsHuman.push({ source, keys: missing, env: missing.map(k => secretEnvName(source, k)) })\n actions.push(\n cur\n ? { action: 'update', connection: c, missingSecrets: missing, keptSecrets: kept }\n : { action: 'create', connection: c, missingSecrets: missing }\n )\n }\n\n /*\n * **들여온 뒤의 모습으로 봅니다.** 파일 안에서만 보면 「이미 있는 연결과 부딪히는 것」을 못\n * 잡습니다 — 그게 실제로 더 쉽게 나는 쪽입니다(하나는 화면에서, 하나는 파일에서 생깁니다).\n */\n const after = [\n /* 행 모양 그대로 넘긴다 — 옮기는 자리는 `intakeCollisions` 안 한 곳이다(ADR-0053 ①). */\n ...existing.filter(e => !seen.has(e.source)),\n ...actions\n .filter((a): a is Extract<ImportAction, { connection: PortableConnection }> => a.action !== 'skip')\n .map(a => ({ source: a.connection.source, config: a.connection.config }))\n ]\n return { actions, needsHuman, collisions: intakeCollisions(after), errors }\n}\n\n/** 사람이 읽는 한 줄 — 화면과 로그가 같은 문장을 쓰게. */\nexport function sayPlan(plan: ImportPlan): string {\n const n = (a: ImportAction['action']) => plan.actions.filter(x => x.action === a).length\n const head = `연결 ${plan.actions.length}개 — 새로 ${n('create')} · 갱신 ${n('update')} · 건너뜀 ${n('skip')}`\n /* 부딪힘을 먼저 말합니다 — 비밀값이 없는 것은 안 도는 것이고, 부딪히는 것은 **잘못 도는 것**입니다. */\n if (plan.collisions.length) {\n const who = plan.collisions.map(c => `${c.sources.join(' · ')} → ${c.intake}`).join(', ')\n return `${head}. ⚠ **두 연결이 같은 곳에 조치를 냅니다** — ${who}. 다른 공장의 작업지시가 한 공장에 섭니다`\n }\n if (!plan.needsHuman.length) return `${head}. 비밀값은 다 있습니다`\n const who = plan.needsHuman.map(h => `${h.source}(${h.keys.join(' · ')})`).join(', ')\n return `${head}. **비밀값을 사람이 넣어야 합니다** — ${who}. 연결 화면에서 채우면 그때부터 돕니다`\n}\n\n/**\n * 비밀값이 사는 **환경 변수 이름** — `mes-miratek` · `hookSecret` → `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET`.\n *\n * 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이\n * 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽\n * 버전입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.\n *\n * 낙타 이름의 경계를 밑줄로 가릅니다 — 안 그러면 `HOOKSECRET` 과 `HOOK_SECRET` 이 갈려 한쪽만 읽힙니다.\n */\nexport function secretEnvName(source: string, key: string): string {\n const up = (x: string) =>\n x\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toUpperCase()\n return `TWIN_SECRET__${up(source)}__${up(key)}`\n}\n\n/**\n * **두 연결이 같은 곳에 조치를 내지 않나** — 같으면 다른 공장의 작업지시가 한 공장에 섭니다.\n *\n * ── 왜 이 검사가 생겼나 (2026-09-05) ────────────────────────────────────────\n * `mes-miratek` 이 서면서 한 커넥터가 처음으로 **공장 둘**을 달았습니다. 조치가 나가는 주소는\n * `intakeSiteId` 가 정하는데(`operato-plant.ts:389`), 그 값이 트윈의 사이트 이름을 **이깁니다** —\n * 트윈에서는 `mes-line-a` 인데 MES 의 도메인은 `system` 이라 그 override 가 필요합니다.\n *\n * 그래서 **연결을 베껴 만들고 그 칸만 안 고치면** 두 트윈의 조치가 같은 공장으로 갑니다. 오류가\n * 안 납니다 — 저쪽은 정상적인 작업지시로 받고 **실제로 만듭니다.**\n *\n * 게이트에 이미 「사이트를 모르면 보내지 않는다」가 있는데(`no-site`), 그 이유가 적혀 있는 문장이\n * **「잘못된 공장이 실제로 만든다」** 입니다. 이것은 그 둘째 길입니다 — 사이트를 아는데 **틀린\n * 사이트를 압니다.**\n *\n * 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 패키지를 베껴 이름만 바꾸면 받는 자리가\n * 그대로 따라옵니다.\n */\nexport function intakeCollisions(\n connections: readonly {\n source: string\n adapterType?: string\n /** 패키지·계획이 쓰는 이름. */\n config?: Record<string, unknown> | null\n /** **DB 행이 쓰는 이름** — `TwinReference` 의 칸이다. 둘 다 받는다(ADR-0053 ①). */\n connectionConfig?: Record<string, unknown> | null\n }[]\n): { intake: string; sources: string[] }[] {\n const byIntake = new Map<string, string[]>()\n for (const c of connections) {\n /*\n * **모양이 둘인 것을 여기서 안다** (ADR-0053 ①, 2026-09-16).\n *\n * 패키지는 `config`, `TwinReference` 행은 `connectionConfig` 다. 전에는 `config` 만 읽어서, 행을\n * 그대로 넘기면 같은 충돌에도 `[]` 를 냈다 — 오류도 경고도 없이 **저장이 통과하고 가드는 선 채로\n * 아무 일도 안 한다.** 부르는 쪽마다 옮기게 두면 다음 소비처가 또 밟는다(이 함수는 화면이 쓰라고\n * 리졸버에서 다시 내보내진다).\n *\n * 「없다」가 편한 답인 자리라 더 위험하다 — 빨강이 아니라 **조용한 초록**으로 흐른다.\n */\n const cfg = c.config ?? c.connectionConfig ?? {}\n /* 받는 자리를 정하는 규칙은 커넥터의 것과 같아야 합니다 — 여기서 다시 지으면 두 벌이 됩니다. */\n const intake = String(cfg.intakeSiteId ?? cfg.siteId ?? '').trim()\n /* 조치를 낼 수 없는 연결은 부딪힐 것이 없습니다 — 주소가 없으면 아무 데도 안 갑니다. */\n if (!intake || !String(cfg.intakeEndpoint ?? '').trim()) continue\n const key = `${String(cfg.intakeEndpoint).trim().replace(/\\/$/, '')}::${intake}`\n byIntake.set(key, [...(byIntake.get(key) ?? []), c.source])\n }\n return [...byIntake.entries()]\n .filter(([, sources]) => sources.length > 1)\n .map(([intake, sources]) => ({ intake, sources: sources.sort() }))\n}\n\n/**\n * **화면 저장의 문** — 이 연결을 이 설정으로 저장하면 다른 연결과 받는 사이트가 겹치나 (ADR-0046 ②-1).\n *\n * 들여오기(`planImport`)는 `intakeCollisions` 로 거부하는데 화면 저장(`saveTwinReference`)은 부르지 않아서,\n * 연결을 베껴 이름만 바꾸면 경고 0 으로 두 트윈의 조치가 한 공장에 섰다. 판정은 같은 함수 하나로 한다.\n * 경고가 아니라 거부이고, 사람이 그 자리에서 고치도록 **이미 그 사이트를 쓰는 연결의 이름**을 싣는다.\n */\nexport function refuseIntakeCollision(\n source: string,\n config: Record<string, unknown> | undefined,\n others: readonly { source: string; siteName?: string | null; connectionConfig?: Record<string, unknown> | null }[]\n): { code: 'intake-site-taken'; message: string; params: { site: string; connection: string } } | null {\n const rest = others.filter(o => o.source !== source)\n /* `others` 는 DB 행이다 — 옮기지 않고 그대로 넘긴다(ADR-0053 ①). */\n const hit = intakeCollisions([...rest, { source, config: config ?? {} }]).find(c => c.sources.includes(source))\n if (!hit) return null\n const connection = rest\n .filter(o => hit.sources.includes(o.source))\n .map(o => o.siteName || o.source)\n .join(', ')\n const site = String(config?.intakeSiteId ?? config?.siteId ?? '').trim()\n return {\n code: 'intake-site-taken',\n message: `intake site ${site} is already used by ${connection}`,\n params: { site, connection }\n }\n}\n"]}
@@ -227,7 +227,7 @@ function takeInSequence(args) {
227
227
  if (verdict.kind === 'first') {
228
228
  firstSeq = item.seq;
229
229
  /*
230
- * 계약이 `unseenBefore` 를 내면 그것을 쓴다. 설치된 판이 그보다 오래됐으면 번호에서 센다 —
230
+ * 계약이 `unseenBefore` 를 내면 그것을 쓴다. 설치된 버전이 그보다 오래됐으면 번호에서 센다 —
231
231
  * 첫 봉투의 번호 아래로 그만큼이 있고 우리는 그것을 받은 적이 없다.
232
232
  *
233
233
  * ⚠ 이 두 번째 길은 계약 0.9.9 가 깔리면 지운다. 그때까지 두는 이유는, 계약을 기다리며
@@ -1 +1 @@
1
- {"version":3,"file":"hook-contract.js","sourceRoot":"","sources":["../../../server/service/reference/hook-contract.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 웹훅의 규약 — 상태 코드 · 비밀값 · 어느 트윈인가.\n *\n * 데이터베이스를 부르는 부분과 갈라 둔다(§`reference-hook`): 규약만 있는 자리라야 시험이 엔티티를\n * 끌고 오지 않는다. 실제로 그것 때문에 시험이 서지 않았다.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 지금 모든 커넥터가 주기적으로 물어봅니다(폴링). 그 방식은 두 가지를 치릅니다. 연결된 시스템이 값을\n * 갱신하는 주기보다 자주 물으면 같은 값을 되풀이 받고, 그쪽 서버에 그만큼 부담을 줍니다. 실제로\n * 태양광 발전소에 10초마다 물으면서 10분마다 바뀌는 값을 받아 왔습니다.\n *\n * 연결된 시스템이 「바뀌었다」를 말해 줄 수 있으면 그것을 받는 것이 낫습니다. 이 파일이 그 입구입니다.\n *\n * ── 규약 ────────────────────────────────────────────────────────────────────\n *\n * 주소 POST /twin/hook/<연결이름>/<트윈> — no /domain/<도메인> prefix: the route sits on the domain-public\n * router without one (server/routes.ts:31); a prefixed address falls through and answers 405\n * 도메인 X-Things-Factory-Domain: <도메인> — the webhook module sends it from the destination's\n * params.receivingDomain\n * 인증 연결 설정에 저장한 비밀값(`hookSecret`)을 헤더로 보낸다\n * X-Twin-Hook-Secret: <비밀값> 또는 Authorization: Bearer <비밀값>\n * 본문 연결된 시스템이 정한 모양 그대로. 우리 어휘로 옮기는 것은 커넥터의 일이다\n * 응답 아래 §`hookStatus`\n *\n * ── 왜 사람 인증(JWT)이 아닌가 ──────────────────────────────────────────────\n * 밀어 주는 쪽은 사람이 아닙니다. 우리 토큰을 발급받아 갱신하며 관리하라고 요구하면 대부분의 현장에서\n * 연동이 서지 않습니다. 그래서 연결마다 비밀값을 두고 그것으로 확인합니다.\n *\n * **비밀값이 없는 연결은 훅을 받지 않습니다.** 기본이 거부입니다 — 인증 없는 입구를 열어 두는 것보다\n * 훅을 못 쓰는 것이 낫습니다.\n *\n * ── 왜 커넥터가 옮기나 (시나리오가 아니라) ──────────────────────────────────\n * 연결된 시스템의 웹훅은 그쪽이 정한 고정된 모양으로 옵니다. 그 모양을 아는 것은 그 커넥터이고,\n * 옮기는 판단은 코드여야 테스트로 지킬 수 있습니다. 실제로 지금 커넥터들이 코드로 판단합니다 —\n * 통신이 끊긴 설비의 값은 보내지 않고, 잰 시각이 나아가지 않은 줄은 거릅니다. 그 판단을 변환식으로\n * 옮기면 약해지고 테스트가 없어집니다.\n *\n * ── 밀어 주기만으로는 안 된다 ───────────────────────────────────────────────\n * 훅은 반드시 놓칩니다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 그래서\n * 훅을 쓰는 연결도 **드문 폴링을 유지합니다.** 폴링의 역할이 「값을 가져오는 것」에서 「놓친 것이 있나\n * 확인하는 것」으로 바뀌는 것입니다. 그 주기는 연결 설정이 정합니다.\n *\n * ── 같은 것이 두 번 오는 것 ─────────────────────────────────────────────────\n * 밀어 주는 방식은 재전송이 정상입니다(그쪽이 우리 응답을 못 받으면 다시 보냅니다). 그것은 유입\n * 경계가 이미 거릅니다(§`FactDeduper`) — 이 파일이 따로 하지 않습니다. 응답에 몇 건이 중복이었는지\n * 함께 알립니다.\n */\n/*\n * `WebhookResponse` comes from the root, not from `@operato/ops-contract/webhook`.\n *\n * That subpath resolves to `webhook-signature`, which is a separate module kept out of the barrel\n * because it pulls in `node:crypto`. The module actually named `webhook.ts` — status codes, the\n * envelope, the response — reaches consumers through the root barrel. Importing the response type\n * from the subpath fails with TS2305, which is what happened here first.\n */\nimport { checkSequence, type WebhookResponse } from '@operato/ops-contract'\nimport { WEBHOOK_HEADER, verifyWebhookSignature } from '@operato/ops-contract/webhook'\n\n/*\n * The body is typed by the contract, not by this file (2026-09-07).\n *\n * It was `Record<string, unknown>`, which accepts anything — so nothing checked that what we reply\n * with is what a sender was told to expect. It was not: the contract declared `rejected` in prose\n * only, operato-plant guessed \"a list of envelope ids\", and a whole batch was stamped delivered\n * while one envelope never landed (mes_outbox_events seq 178 — SENT, attempts 0, no error).\n *\n * With `WebhookResponse` here, adding a field to a reply without declaring it fails the build on\n * this side, which is the side that would otherwise ship the surprise.\n */\n/** 훅 요청의 결과 — 상태 코드와 본문을 함께 정한다(부르는 쪽이 그대로 답한다). */\nexport interface HookOutcome {\n status: number\n body: WebhookResponse\n}\n\n/**\n * 응답 코드 규약 — **계약이 정하고 여기서는 그것을 쓴다**(2026-08-31).\n *\n * 예전에는 이 표가 여기 있었고 보내는 쪽(operato-plant)에 또 있었다. 그래서 같은 409 를 양쪽이 다른\n * 뜻으로 썼다 — 여기서는 「그 트윈이 실시간으로 돌지 않는다」, 저쪽에서는 「번호가 비었다」였다.\n * 붙였다면 트윈이 안 도는 동안 보내는 쪽이 같은 구간을 계속 다시 보냈다.\n *\n * 판정표는 만드는 쪽과 읽는 쪽이 합의해야 하는 것이라 계약 층에 있다(`@operato/ops-contract`).\n * `notLive` 가 409 에서 503 으로 옮겨진 이유도 그 파일에 적혀 있다.\n *\n * **여기서 이름을 바꿔 다시 내보내지 않는다.** 부르는 쪽은 `WEBHOOK_STATUS` 를 계약에서 바로 가져온다.\n * 별명을 두면 같은 표에 이름이 둘이 되고, 다음 사람이 어느 쪽이 정본인지 물어야 한다.\n */\n\n/** 헤더에서 비밀값을 꺼낸다 — 두 가지 방식을 받는다(밀어 주는 쪽의 관습이 갈린다). */\nexport function secretFromHeaders(headers: Record<string, unknown> | undefined): string {\n const pick = (k: string) => String((headers ?? {})[k] ?? '').trim()\n const direct = pick('x-twin-hook-secret')\n if (direct) return direct\n const auth = pick('authorization')\n return auth.toLowerCase().startsWith('bearer ') ? auth.slice(7).trim() : ''\n}\n\n/**\n * 비밀값 비교 — **길이가 다르면 바로 거절하고, 같으면 끝까지 비교한다.**\n *\n * 앞에서 다른 것을 발견하고 곧바로 답하면 응답 시간이 달라지고, 그 차이로 비밀값을 한 글자씩 알아낼 수\n * 있습니다. 그래서 같은 길이면 전부를 비교합니다.\n */\nexport function secretMatches(given: string, expected: string): boolean {\n if (!expected || !given || given.length !== expected.length) return false\n let diff = 0\n for (let i = 0; i < expected.length; i++) diff |= given.charCodeAt(i) ^ expected.charCodeAt(i)\n return diff === 0\n}\n\n/**\n * 인증 — **서명이 왔으면 서명을 보고, 아니면 비밀값을 본다.**\n *\n * 두 방식을 두는 이유는 밀어 주는 쪽의 사정이 다르기 때문이다. 이미 붙어 있는 커넥터는 비밀값을 헤더에\n * 싣는 방식으로 만들어졌고, 새로 만드는 것(operato-plant)은 서명을 쓴다. 서명 쪽이 낫다 — 비밀값을 그대로\n * 싣는 방식은 그 요청을 그대로 다시 보내는 것을 막지 못한다.\n *\n * **서명이 왔는데 확인할 수 없으면 거절한다.** 받은 바이트를 들고 있지 않으면(`rawBody` 가 없으면)\n * 파싱한 객체를 다시 문자열로 만들어 확인하고 싶어지는데, 키 순서와 공백이 달라져 어차피 맞지 않는다.\n * 맞지 않는 것을 통과시키면 서명이 있으나 마나 한 것이 된다.\n */\nexport function authorizeHook(args: {\n headers: Record<string, unknown> | undefined\n /** 받은 바이트 그대로. 서명을 쓰지 않는 연결에서는 없어도 된다. */\n rawBody: string | undefined\n /** 연결 설정에 저장된 비밀값. 서명과 비밀값이 같은 값을 쓴다. */\n secret: string\n nowMs: number\n}): { ok: true; method: 'signature' | 'secret' } | { ok: false; reason: string } {\n const { headers, rawBody, secret, nowMs } = args\n const pick = (k: string) => String((headers ?? {})[k] ?? '').trim()\n const signature = pick(WEBHOOK_HEADER.signature)\n\n if (signature) {\n if (rawBody === undefined) {\n return { ok: false, reason: 'signed request but the raw body was not kept — cannot verify' }\n }\n const verdict = verifyWebhookSignature({\n secret,\n timestamp: pick(WEBHOOK_HEADER.timestamp),\n signature,\n body: rawBody,\n nowMs\n })\n /* `=== false` 로 본다 — 이 저장소는 `strictNullChecks` 가 꺼져 있어 참·거짓만으로는 좁혀지지 않는다. */\n if (verdict.ok === false) return { ok: false, reason: `signature ${verdict.reason}` }\n return { ok: true, method: 'signature' }\n }\n\n return secretMatches(secretFromHeaders(headers), secret)\n ? { ok: true, method: 'secret' }\n : { ok: false, reason: 'hook secret does not match' }\n}\n\n/**\n * 커서에 적을 열쇠 — **트윈까지 넣는다.**\n *\n * ── 왜 (2026-08-31, MES 레인 질문에서 나옴) ───────────────────────────────\n * 커서는 **연결마다** 한 행(`TwinReference.liveCursor`)에 산다. 그런데 연결 하나가 트윈 여럿을\n * 만든다(`scopeSpec.produced` 가 목록이다). 보내는 쪽의 단위 이름만으로 열쇠를 잡으면, 같은 연결의\n * 두 공장이 같은 이름(`EVENTS`)을 쓸 때 **한 계수기를 함께 쓴다.**\n *\n * 그러면 A 공장이 5까지 올린 뒤 B 공장이 1을 보내면 이미 본 번호가 되어 버려지고, A 가 6을 보내면\n * B 의 번호와 어긋나 끊임없이 「빠졌다」가 된다. 같은 부류가 이 저장소에서 이미 났다 — 두 공장의\n * 같은 번호가 한 자리에 겹쳤다.\n *\n * 보내는 쪽에 트윈 이름을 넣으라고 하지 않는다. 어디로 갈지는 주소가 이미 말했고, 그것을 본문에도\n * 적으라고 하면 둘이 어긋나는 날 어느 쪽을 믿을지 정해야 한다.\n */\nexport function pushCursorKey(instanceId: string, scope: string): string {\n return `${instanceId}::${scope}`\n}\n\n/**\n * 커서에서 그 단위의 마지막 번호를 꺼낸다.\n *\n * 커서 모양은 `{ streams: { [흐름]: { since?, seen[], seq? } } }` 이고 **흐름 이름은 어댑터가 정한다**\n * (§`TwinReference.liveCursor`). 밀어 주는 연결에서는 그 이름이 곧 번호를 매기는 단위다.\n *\n * 자리를 따로 만들지 않는 이유는 「재기동을 넘어 사는 진행 위치」가 두 벌이 되기 때문이다. 폴링이 쓰는\n * `since` 와 밀어 주기가 쓰는 `seq` 는 둘 다 「이 흐름을 어디까지 읽었나」이고 원본이 되풀어 줄 수 있다.\n */\nexport function seqOf(cursor: unknown, scope: string): number | undefined {\n const streams = (cursor as any)?.streams\n const seq = streams?.[scope]?.seq\n return typeof seq === 'number' && Number.isInteger(seq) ? seq : undefined\n}\n\n/** 그 단위의 번호만 바꾼 커서를 만든다 — 다른 흐름과 `since`·`seen` 은 그대로 둔다. */\nexport function withSeq(cursor: unknown, scope: string, seq: number, firstSeq?: number): Record<string, unknown> {\n const base = (cursor && typeof cursor === 'object' ? (cursor as Record<string, unknown>) : {}) as Record<string, any>\n const streams = base.streams && typeof base.streams === 'object' ? { ...base.streams } : {}\n const prev = streams[scope] ?? {}\n /*\n * **`firstSeq` 는 한 번만 적힌다.** 이 연결이 이 단위를 몇 번부터 받기 시작했는지는 바뀌지 않는\n * 사실이고, 나중에 덮으면 「앞이 비었나」를 다시 물을 수 없다.\n *\n * 그래서 첫 봉투 판정이 있을 때만 넘어온다(`SequencedTake.firstSeq`). 커서가 이미 나아간 뒤에\n * 이 값을 만들면 그 번호는 첫 번호가 아니라 그때 마침 온 번호다.\n */\n streams[scope] = { ...prev, seq, ...(firstSeq !== undefined && prev.firstSeq === undefined ? { firstSeq } : {}) }\n return { ...base, streams }\n}\n\n/**\n * 이 연결이 이 단위를 **몇 번부터 받기 시작했나** — 적힌 적이 없으면 `undefined`.\n *\n * 「앞이 비었나」를 답하려면 이 값이 있어야 한다. 없으면 **모른다고 말해야 한다** — 0 으로 답하면\n * 1번부터 다 받은 것으로 읽힌다.\n */\nexport function firstSeqOf(cursor: unknown, scope: string): number | undefined {\n const seq = (cursor as any)?.streams?.[scope]?.firstSeq\n return typeof seq === 'number' && Number.isInteger(seq) ? seq : undefined\n}\n\n/**\n * **번호대로 받을 수 있는 데까지 자른다** — 밀어 받는 길과 backfill 하는 길이 이 함수 하나를 지난다.\n *\n * ── 왜 갈라 두나 (2026-08-31) ────────────────────────────────────────────\n * 이 판정이 훅 안에 박혀 있었다. backfill(`fetchSince`)이 같은 판정을 해야 하는데, 거기서 다시 만들면\n * **두 길이 다른 규칙으로 받는다** — 한쪽만 고쳐지는 날이 오고, 어긋난 그 길에서만 사실이 사라진다.\n *\n * 번호 확인을 커넥터가 아니라 프레임워크로 가져온 것과 같은 이유다. 자리가 하나여야 한다.\n *\n * ── 첫 구멍에서 멈춘다 ───────────────────────────────────────────────────\n * 구멍 뒤의 봉투를 받아들이면 그 구멍이 영영 메워지지 않는다. 보내는 쪽은 `expectedSeq` 부터 다시\n * 보내고, 뒤엣것은 그때 함께 온다.\n */\nexport interface SequencedTake {\n /** 받아들인 레코드 — 구멍 앞까지다. */\n records: unknown[]\n /** 번호를 확인했나. 번호를 매기지 않는 원본은 거짓이고, 그 사실을 응답에 남긴다. */\n numbered: boolean\n /** 커서에 적을 번호 — 받은 것이 없으면 없다. */\n advancedSeq?: number\n /** 구멍이 났다 — 그 앞부터 다시 받아야 한다. */\n gap?: { expectedSeq: number; lastSeq: number }\n /**\n * 이 단위의 **첫 봉투였다** — 그 앞으로 몇 개를 못 봤나.\n *\n * ── 왜 이 칸이 생겼나 (2026-09-06 실측) ──────────────────────────────────\n * plant 아웃박스의 1~20 이 400 으로 거절돼 그 연결에 커서가 없었다. 다음에 온 21번이 첫 봉투로\n * 앉으며 **커서가 21 로 뛰었다.** 커서가 0 이었다면 같은 21이 구멍이었다.\n *\n * 스무 건이 없는데 구멍으로도 안 세어지고, 경고도 없고, backfill 대상도 아니었다 — backfill 은 커서부터\n * 묻는다. 그 사실이 아무 데도 안 남았다.\n *\n * **손실이라고 말하지 않는다.** 원본이 오래 돌고 있는데 트윈을 나중에 붙이면 그 아래는 애초에\n * 안 받기로 한 것이다. 계약은 둘을 구별할 수 없으므로 수만 낸다.\n *\n * 0 이면 1번부터 시작한 것이라 이 칸을 내지 않는다 — 말할 것이 없다.\n */\n firstUnseenBefore?: number\n /**\n * 이 묶음의 첫 봉투가 **이 단위의 첫 봉투였다** — 그 번호.\n *\n * `firstUnseenBefore` 와 다르다: 이쪽은 1번부터 시작한 연결에도 있다(값 1). 커서에 적어 두면\n * 나중에 「앞이 비었나」를 답할 수 있다.\n */\n firstSeq?: number\n /**\n * 이 묶음에서 **가장 낮게 받아들인 번호** — 하나도 못 받았으면 없다.\n *\n * `firstSeq` 와 다르다. `firstSeq` 는 「이 단위의 첫 봉투였다」일 때만 있고, 이쪽은 언제나 있다.\n *\n * 밑에서부터 다시 받을 때 이것이 필요하다. `from: 0` 으로 부르면 커서가 0 이므로 seq 1 은\n * `next` 판정이고 `firstSeq` 가 안 생긴다 — 그런데 우리는 실제로 1번부터 갖게 된다. 그 사실을\n * 적어야 「앞이 비었나」에 답할 수 있다.\n */\n lowestSeq?: number\n /**\n * 받을 수 없는 묶음 — 그 이유.\n *\n * 번호가 있는 봉투와 없는 봉투가 섞였거나 번호가 정수가 아니다. 둘 다 보내는 쪽의 결함이고,\n * **반쯤 확인한 것을 확인했다고 말하지 않는다.**\n */\n refused?: string\n}\n\nexport function takeInSequence(args: {\n items: { record: unknown; seq?: number; at?: string }[]\n scope?: string\n /** 이 단위에서 마지막으로 받아들인 번호. 받은 적이 없으면 `undefined`. */\n lastSeq?: number\n}): SequencedTake {\n const { items, scope, lastSeq } = args\n\n const withSeq = items.filter(i => typeof i?.seq === 'number').length\n if (withSeq && withSeq !== items.length) {\n return { records: [], numbered: false, refused: `mixed batch — ${withSeq} of ${items.length} items carry a sequence number` }\n }\n const numbered = withSeq > 0 && !!scope\n\n if (!numbered) return { records: items.map(i => i.record), numbered: false }\n\n let cursor = lastSeq\n let take = 0\n let gap: { expectedSeq: number; lastSeq: number } | undefined\n /* 첫 봉투가 몇 번부터 시작했나 — 그 앞을 못 본 사실을 응답에 남긴다. */\n let firstUnseenBefore: number | undefined\n /* 이 단위를 몇 번부터 받기 시작했나 — 커서에 적어 두면 「앞이 비었나」를 나중에 답할 수 있다. */\n let firstSeq: number | undefined\n /* 이 묶음에서 가장 낮게 받아들인 번호 — 밑에서부터 다시 받을 때 이것이 firstSeq 가 된다. */\n let lowestSeq: number | undefined\n for (const item of items) {\n let verdict\n try {\n verdict = checkSequence(cursor, item.seq as number)\n } catch (e: any) {\n return { records: [], numbered: true, refused: e?.message ?? 'bad sequence number' }\n }\n if (verdict.kind === 'gap') {\n gap = { expectedSeq: verdict.expectedSeq, lastSeq: verdict.lastSeq }\n break\n }\n if (verdict.kind === 'first') {\n firstSeq = item.seq as number\n /*\n * 계약이 `unseenBefore` 를 내면 그것을 쓴다. 설치된 판이 그보다 오래됐으면 번호에서 센다 —\n * 첫 봉투의 번호 아래로 그만큼이 있고 우리는 그것을 받은 적이 없다.\n *\n * ⚠ 이 두 번째 길은 계약 0.9.9 가 깔리면 지운다. 그때까지 두는 이유는, 계약을 기다리며\n * 비워 두면 **코드는 있는데 아무 일도 안 하는 상태**가 되고 그것이 이 결함의 원인과 같은\n * 모양이기 때문이다.\n */\n const unseen = (verdict as any).unseenBefore ?? (item.seq as number) - 1\n if (unseen > 0) firstUnseenBefore = unseen\n }\n if (lowestSeq === undefined) lowestSeq = item.seq as number\n cursor = verdict.lastSeq\n take++\n }\n\n return {\n /*\n * **봉투의 시각을 레코드에 얹는다** — 레코드가 자기 시각을 말하면 그것이 이긴다.\n *\n * 시각을 안 싣는 레코드는 ingest 시각으로 떨어지고, 그 값이 실행할 때마다 달라서 fact identity 가\n * 매번 바뀐다. 그래서 같은 사실을 다시 받으면 dedupe 가 안 되고 새 사실로 앉는다 —\n * `fillTwinScope` 를 두 번 돌렸더니 저널이 40 → 69건이 됐다(2026-09-06 측정).\n */\n records: items.slice(0, take).map(i => {\n const r = i.record as any\n if (!i.at || r == null || typeof r !== 'object') return i.record\n return r.at === undefined && r.eventTime === undefined ? { ...r, at: i.at } : i.record\n }),\n numbered: true,\n /* 앞의 것을 하나도 못 받았으면 커서를 올릴 것이 없다. */\n ...(take > 0 && typeof cursor === 'number' ? { advancedSeq: cursor } : {}),\n ...(gap ? { gap } : {}),\n ...(firstUnseenBefore ? { firstUnseenBefore } : {}),\n ...(firstSeq !== undefined ? { firstSeq } : {}),\n ...(lowestSeq !== undefined ? { lowestSeq } : {})\n }\n}\n\n/** 이 연결이 이 트윈을 만들었나 — 남의 트윈에 밀어 넣지 못하게. */\nexport function producedInstance(ref: { scopeSpec?: any } | null | undefined, instanceId: string): { siteId: string } | undefined {\n const produced: any[] = (ref?.scopeSpec as any)?.produced ?? []\n const hit = produced.find(p => String(p?.instanceId ?? '') === instanceId)\n return hit?.siteId ? { siteId: String(hit.siteId) } : undefined\n}\n\n"]}
1
+ {"version":3,"file":"hook-contract.js","sourceRoot":"","sources":["../../../server/service/reference/hook-contract.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 웹훅의 규약 — 상태 코드 · 비밀값 · 어느 트윈인가.\n *\n * 데이터베이스를 부르는 부분과 갈라 둔다(§`reference-hook`): 규약만 있는 자리라야 시험이 엔티티를\n * 끌고 오지 않는다. 실제로 그것 때문에 시험이 서지 않았다.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 지금 모든 커넥터가 주기적으로 물어봅니다(폴링). 그 방식은 두 가지를 치릅니다. 연결된 시스템이 값을\n * 갱신하는 주기보다 자주 물으면 같은 값을 되풀이 받고, 그쪽 서버에 그만큼 부담을 줍니다. 실제로\n * 태양광 발전소에 10초마다 물으면서 10분마다 바뀌는 값을 받아 왔습니다.\n *\n * 연결된 시스템이 「바뀌었다」를 말해 줄 수 있으면 그것을 받는 것이 낫습니다. 이 파일이 그 입구입니다.\n *\n * ── 규약 ────────────────────────────────────────────────────────────────────\n *\n * 주소 POST /twin/hook/<연결이름>/<트윈> — no /domain/<도메인> prefix: the route sits on the domain-public\n * router without one (server/routes.ts:31); a prefixed address falls through and answers 405\n * 도메인 X-Things-Factory-Domain: <도메인> — the webhook module sends it from the destination's\n * params.receivingDomain\n * 인증 연결 설정에 저장한 비밀값(`hookSecret`)을 헤더로 보낸다\n * X-Twin-Hook-Secret: <비밀값> 또는 Authorization: Bearer <비밀값>\n * 본문 연결된 시스템이 정한 모양 그대로. 우리 어휘로 옮기는 것은 커넥터의 일이다\n * 응답 아래 §`hookStatus`\n *\n * ── 왜 사람 인증(JWT)이 아닌가 ──────────────────────────────────────────────\n * 밀어 주는 쪽은 사람이 아닙니다. 우리 토큰을 발급받아 갱신하며 관리하라고 요구하면 대부분의 현장에서\n * 연동이 서지 않습니다. 그래서 연결마다 비밀값을 두고 그것으로 확인합니다.\n *\n * **비밀값이 없는 연결은 훅을 받지 않습니다.** 기본이 거부입니다 — 인증 없는 입구를 열어 두는 것보다\n * 훅을 못 쓰는 것이 낫습니다.\n *\n * ── 왜 커넥터가 옮기나 (시나리오가 아니라) ──────────────────────────────────\n * 연결된 시스템의 웹훅은 그쪽이 정한 고정된 모양으로 옵니다. 그 모양을 아는 것은 그 커넥터이고,\n * 옮기는 판단은 코드여야 테스트로 지킬 수 있습니다. 실제로 지금 커넥터들이 코드로 판단합니다 —\n * 통신이 끊긴 설비의 값은 보내지 않고, 잰 시각이 나아가지 않은 줄은 거릅니다. 그 판단을 변환식으로\n * 옮기면 약해지고 테스트가 없어집니다.\n *\n * ── 밀어 주기만으로는 안 된다 ───────────────────────────────────────────────\n * 훅은 반드시 놓칩니다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 그래서\n * 훅을 쓰는 연결도 **드문 폴링을 유지합니다.** 폴링의 역할이 「값을 가져오는 것」에서 「놓친 것이 있나\n * 확인하는 것」으로 바뀌는 것입니다. 그 주기는 연결 설정이 정합니다.\n *\n * ── 같은 것이 두 번 오는 것 ─────────────────────────────────────────────────\n * 밀어 주는 방식은 재전송이 정상입니다(그쪽이 우리 응답을 못 받으면 다시 보냅니다). 그것은 유입\n * 경계가 이미 거릅니다(§`FactDeduper`) — 이 파일이 따로 하지 않습니다. 응답에 몇 건이 중복이었는지\n * 함께 알립니다.\n */\n/*\n * `WebhookResponse` comes from the root, not from `@operato/ops-contract/webhook`.\n *\n * That subpath resolves to `webhook-signature`, which is a separate module kept out of the barrel\n * because it pulls in `node:crypto`. The module actually named `webhook.ts` — status codes, the\n * envelope, the response — reaches consumers through the root barrel. Importing the response type\n * from the subpath fails with TS2305, which is what happened here first.\n */\nimport { checkSequence, type WebhookResponse } from '@operato/ops-contract'\nimport { WEBHOOK_HEADER, verifyWebhookSignature } from '@operato/ops-contract/webhook'\n\n/*\n * The body is typed by the contract, not by this file (2026-09-07).\n *\n * It was `Record<string, unknown>`, which accepts anything — so nothing checked that what we reply\n * with is what a sender was told to expect. It was not: the contract declared `rejected` in prose\n * only, operato-plant guessed \"a list of envelope ids\", and a whole batch was stamped delivered\n * while one envelope never landed (mes_outbox_events seq 178 — SENT, attempts 0, no error).\n *\n * With `WebhookResponse` here, adding a field to a reply without declaring it fails the build on\n * this side, which is the side that would otherwise ship the surprise.\n */\n/** 훅 요청의 결과 — 상태 코드와 본문을 함께 정한다(부르는 쪽이 그대로 답한다). */\nexport interface HookOutcome {\n status: number\n body: WebhookResponse\n}\n\n/**\n * 응답 코드 규약 — **계약이 정하고 여기서는 그것을 쓴다**(2026-08-31).\n *\n * 예전에는 이 표가 여기 있었고 보내는 쪽(operato-plant)에 또 있었다. 그래서 같은 409 를 양쪽이 다른\n * 뜻으로 썼다 — 여기서는 「그 트윈이 실시간으로 돌지 않는다」, 저쪽에서는 「번호가 비었다」였다.\n * 붙였다면 트윈이 안 도는 동안 보내는 쪽이 같은 구간을 계속 다시 보냈다.\n *\n * 판정표는 만드는 쪽과 읽는 쪽이 합의해야 하는 것이라 계약 층에 있다(`@operato/ops-contract`).\n * `notLive` 가 409 에서 503 으로 옮겨진 이유도 그 파일에 적혀 있다.\n *\n * **여기서 이름을 바꿔 다시 내보내지 않는다.** 부르는 쪽은 `WEBHOOK_STATUS` 를 계약에서 바로 가져온다.\n * 별명을 두면 같은 표에 이름이 둘이 되고, 다음 사람이 어느 쪽이 정본인지 물어야 한다.\n */\n\n/** 헤더에서 비밀값을 꺼낸다 — 두 가지 방식을 받는다(밀어 주는 쪽의 관습이 갈린다). */\nexport function secretFromHeaders(headers: Record<string, unknown> | undefined): string {\n const pick = (k: string) => String((headers ?? {})[k] ?? '').trim()\n const direct = pick('x-twin-hook-secret')\n if (direct) return direct\n const auth = pick('authorization')\n return auth.toLowerCase().startsWith('bearer ') ? auth.slice(7).trim() : ''\n}\n\n/**\n * 비밀값 비교 — **길이가 다르면 바로 거절하고, 같으면 끝까지 비교한다.**\n *\n * 앞에서 다른 것을 발견하고 곧바로 답하면 응답 시간이 달라지고, 그 차이로 비밀값을 한 글자씩 알아낼 수\n * 있습니다. 그래서 같은 길이면 전부를 비교합니다.\n */\nexport function secretMatches(given: string, expected: string): boolean {\n if (!expected || !given || given.length !== expected.length) return false\n let diff = 0\n for (let i = 0; i < expected.length; i++) diff |= given.charCodeAt(i) ^ expected.charCodeAt(i)\n return diff === 0\n}\n\n/**\n * 인증 — **서명이 왔으면 서명을 보고, 아니면 비밀값을 본다.**\n *\n * 두 방식을 두는 이유는 밀어 주는 쪽의 사정이 다르기 때문이다. 이미 붙어 있는 커넥터는 비밀값을 헤더에\n * 싣는 방식으로 만들어졌고, 새로 만드는 것(operato-plant)은 서명을 쓴다. 서명 쪽이 낫다 — 비밀값을 그대로\n * 싣는 방식은 그 요청을 그대로 다시 보내는 것을 막지 못한다.\n *\n * **서명이 왔는데 확인할 수 없으면 거절한다.** 받은 바이트를 들고 있지 않으면(`rawBody` 가 없으면)\n * 파싱한 객체를 다시 문자열로 만들어 확인하고 싶어지는데, 키 순서와 공백이 달라져 어차피 맞지 않는다.\n * 맞지 않는 것을 통과시키면 서명이 있으나 마나 한 것이 된다.\n */\nexport function authorizeHook(args: {\n headers: Record<string, unknown> | undefined\n /** 받은 바이트 그대로. 서명을 쓰지 않는 연결에서는 없어도 된다. */\n rawBody: string | undefined\n /** 연결 설정에 저장된 비밀값. 서명과 비밀값이 같은 값을 쓴다. */\n secret: string\n nowMs: number\n}): { ok: true; method: 'signature' | 'secret' } | { ok: false; reason: string } {\n const { headers, rawBody, secret, nowMs } = args\n const pick = (k: string) => String((headers ?? {})[k] ?? '').trim()\n const signature = pick(WEBHOOK_HEADER.signature)\n\n if (signature) {\n if (rawBody === undefined) {\n return { ok: false, reason: 'signed request but the raw body was not kept — cannot verify' }\n }\n const verdict = verifyWebhookSignature({\n secret,\n timestamp: pick(WEBHOOK_HEADER.timestamp),\n signature,\n body: rawBody,\n nowMs\n })\n /* `=== false` 로 본다 — 이 저장소는 `strictNullChecks` 가 꺼져 있어 참·거짓만으로는 좁혀지지 않는다. */\n if (verdict.ok === false) return { ok: false, reason: `signature ${verdict.reason}` }\n return { ok: true, method: 'signature' }\n }\n\n return secretMatches(secretFromHeaders(headers), secret)\n ? { ok: true, method: 'secret' }\n : { ok: false, reason: 'hook secret does not match' }\n}\n\n/**\n * 커서에 적을 열쇠 — **트윈까지 넣는다.**\n *\n * ── 왜 (2026-08-31, MES 레인 질문에서 나옴) ───────────────────────────────\n * 커서는 **연결마다** 한 행(`TwinReference.liveCursor`)에 산다. 그런데 연결 하나가 트윈 여럿을\n * 만든다(`scopeSpec.produced` 가 목록이다). 보내는 쪽의 단위 이름만으로 열쇠를 잡으면, 같은 연결의\n * 두 공장이 같은 이름(`EVENTS`)을 쓸 때 **한 계수기를 함께 쓴다.**\n *\n * 그러면 A 공장이 5까지 올린 뒤 B 공장이 1을 보내면 이미 본 번호가 되어 버려지고, A 가 6을 보내면\n * B 의 번호와 어긋나 끊임없이 「빠졌다」가 된다. 같은 부류가 이 저장소에서 이미 났다 — 두 공장의\n * 같은 번호가 한 자리에 겹쳤다.\n *\n * 보내는 쪽에 트윈 이름을 넣으라고 하지 않는다. 어디로 갈지는 주소가 이미 말했고, 그것을 본문에도\n * 적으라고 하면 둘이 어긋나는 날 어느 쪽을 믿을지 정해야 한다.\n */\nexport function pushCursorKey(instanceId: string, scope: string): string {\n return `${instanceId}::${scope}`\n}\n\n/**\n * 커서에서 그 단위의 마지막 번호를 꺼낸다.\n *\n * 커서 모양은 `{ streams: { [흐름]: { since?, seen[], seq? } } }` 이고 **흐름 이름은 어댑터가 정한다**\n * (§`TwinReference.liveCursor`). 밀어 주는 연결에서는 그 이름이 곧 번호를 매기는 단위다.\n *\n * 자리를 따로 만들지 않는 이유는 「재기동을 넘어 사는 진행 위치」가 두 벌이 되기 때문이다. 폴링이 쓰는\n * `since` 와 밀어 주기가 쓰는 `seq` 는 둘 다 「이 흐름을 어디까지 읽었나」이고 원본이 되풀어 줄 수 있다.\n */\nexport function seqOf(cursor: unknown, scope: string): number | undefined {\n const streams = (cursor as any)?.streams\n const seq = streams?.[scope]?.seq\n return typeof seq === 'number' && Number.isInteger(seq) ? seq : undefined\n}\n\n/** 그 단위의 번호만 바꾼 커서를 만든다 — 다른 흐름과 `since`·`seen` 은 그대로 둔다. */\nexport function withSeq(cursor: unknown, scope: string, seq: number, firstSeq?: number): Record<string, unknown> {\n const base = (cursor && typeof cursor === 'object' ? (cursor as Record<string, unknown>) : {}) as Record<string, any>\n const streams = base.streams && typeof base.streams === 'object' ? { ...base.streams } : {}\n const prev = streams[scope] ?? {}\n /*\n * **`firstSeq` 는 한 번만 적힌다.** 이 연결이 이 단위를 몇 번부터 받기 시작했는지는 바뀌지 않는\n * 사실이고, 나중에 덮으면 「앞이 비었나」를 다시 물을 수 없다.\n *\n * 그래서 첫 봉투 판정이 있을 때만 넘어온다(`SequencedTake.firstSeq`). 커서가 이미 나아간 뒤에\n * 이 값을 만들면 그 번호는 첫 번호가 아니라 그때 마침 온 번호다.\n */\n streams[scope] = { ...prev, seq, ...(firstSeq !== undefined && prev.firstSeq === undefined ? { firstSeq } : {}) }\n return { ...base, streams }\n}\n\n/**\n * 이 연결이 이 단위를 **몇 번부터 받기 시작했나** — 적힌 적이 없으면 `undefined`.\n *\n * 「앞이 비었나」를 답하려면 이 값이 있어야 한다. 없으면 **모른다고 말해야 한다** — 0 으로 답하면\n * 1번부터 다 받은 것으로 읽힌다.\n */\nexport function firstSeqOf(cursor: unknown, scope: string): number | undefined {\n const seq = (cursor as any)?.streams?.[scope]?.firstSeq\n return typeof seq === 'number' && Number.isInteger(seq) ? seq : undefined\n}\n\n/**\n * **번호대로 받을 수 있는 데까지 자른다** — 밀어 받는 길과 backfill 하는 길이 이 함수 하나를 지난다.\n *\n * ── 왜 갈라 두나 (2026-08-31) ────────────────────────────────────────────\n * 이 판정이 훅 안에 박혀 있었다. backfill(`fetchSince`)이 같은 판정을 해야 하는데, 거기서 다시 만들면\n * **두 길이 다른 규칙으로 받는다** — 한쪽만 고쳐지는 날이 오고, 어긋난 그 길에서만 사실이 사라진다.\n *\n * 번호 확인을 커넥터가 아니라 프레임워크로 가져온 것과 같은 이유다. 자리가 하나여야 한다.\n *\n * ── 첫 구멍에서 멈춘다 ───────────────────────────────────────────────────\n * 구멍 뒤의 봉투를 받아들이면 그 구멍이 영영 메워지지 않는다. 보내는 쪽은 `expectedSeq` 부터 다시\n * 보내고, 뒤엣것은 그때 함께 온다.\n */\nexport interface SequencedTake {\n /** 받아들인 레코드 — 구멍 앞까지다. */\n records: unknown[]\n /** 번호를 확인했나. 번호를 매기지 않는 원본은 거짓이고, 그 사실을 응답에 남긴다. */\n numbered: boolean\n /** 커서에 적을 번호 — 받은 것이 없으면 없다. */\n advancedSeq?: number\n /** 구멍이 났다 — 그 앞부터 다시 받아야 한다. */\n gap?: { expectedSeq: number; lastSeq: number }\n /**\n * 이 단위의 **첫 봉투였다** — 그 앞으로 몇 개를 못 봤나.\n *\n * ── 왜 이 칸이 생겼나 (2026-09-06 실측) ──────────────────────────────────\n * plant 아웃박스의 1~20 이 400 으로 거절돼 그 연결에 커서가 없었다. 다음에 온 21번이 첫 봉투로\n * 앉으며 **커서가 21 로 뛰었다.** 커서가 0 이었다면 같은 21이 구멍이었다.\n *\n * 스무 건이 없는데 구멍으로도 안 세어지고, 경고도 없고, backfill 대상도 아니었다 — backfill 은 커서부터\n * 묻는다. 그 사실이 아무 데도 안 남았다.\n *\n * **손실이라고 말하지 않는다.** 원본이 오래 돌고 있는데 트윈을 나중에 붙이면 그 아래는 애초에\n * 안 받기로 한 것이다. 계약은 둘을 구별할 수 없으므로 수만 낸다.\n *\n * 0 이면 1번부터 시작한 것이라 이 칸을 내지 않는다 — 말할 것이 없다.\n */\n firstUnseenBefore?: number\n /**\n * 이 묶음의 첫 봉투가 **이 단위의 첫 봉투였다** — 그 번호.\n *\n * `firstUnseenBefore` 와 다르다: 이쪽은 1번부터 시작한 연결에도 있다(값 1). 커서에 적어 두면\n * 나중에 「앞이 비었나」를 답할 수 있다.\n */\n firstSeq?: number\n /**\n * 이 묶음에서 **가장 낮게 받아들인 번호** — 하나도 못 받았으면 없다.\n *\n * `firstSeq` 와 다르다. `firstSeq` 는 「이 단위의 첫 봉투였다」일 때만 있고, 이쪽은 언제나 있다.\n *\n * 밑에서부터 다시 받을 때 이것이 필요하다. `from: 0` 으로 부르면 커서가 0 이므로 seq 1 은\n * `next` 판정이고 `firstSeq` 가 안 생긴다 — 그런데 우리는 실제로 1번부터 갖게 된다. 그 사실을\n * 적어야 「앞이 비었나」에 답할 수 있다.\n */\n lowestSeq?: number\n /**\n * 받을 수 없는 묶음 — 그 이유.\n *\n * 번호가 있는 봉투와 없는 봉투가 섞였거나 번호가 정수가 아니다. 둘 다 보내는 쪽의 결함이고,\n * **반쯤 확인한 것을 확인했다고 말하지 않는다.**\n */\n refused?: string\n}\n\nexport function takeInSequence(args: {\n items: { record: unknown; seq?: number; at?: string }[]\n scope?: string\n /** 이 단위에서 마지막으로 받아들인 번호. 받은 적이 없으면 `undefined`. */\n lastSeq?: number\n}): SequencedTake {\n const { items, scope, lastSeq } = args\n\n const withSeq = items.filter(i => typeof i?.seq === 'number').length\n if (withSeq && withSeq !== items.length) {\n return { records: [], numbered: false, refused: `mixed batch — ${withSeq} of ${items.length} items carry a sequence number` }\n }\n const numbered = withSeq > 0 && !!scope\n\n if (!numbered) return { records: items.map(i => i.record), numbered: false }\n\n let cursor = lastSeq\n let take = 0\n let gap: { expectedSeq: number; lastSeq: number } | undefined\n /* 첫 봉투가 몇 번부터 시작했나 — 그 앞을 못 본 사실을 응답에 남긴다. */\n let firstUnseenBefore: number | undefined\n /* 이 단위를 몇 번부터 받기 시작했나 — 커서에 적어 두면 「앞이 비었나」를 나중에 답할 수 있다. */\n let firstSeq: number | undefined\n /* 이 묶음에서 가장 낮게 받아들인 번호 — 밑에서부터 다시 받을 때 이것이 firstSeq 가 된다. */\n let lowestSeq: number | undefined\n for (const item of items) {\n let verdict\n try {\n verdict = checkSequence(cursor, item.seq as number)\n } catch (e: any) {\n return { records: [], numbered: true, refused: e?.message ?? 'bad sequence number' }\n }\n if (verdict.kind === 'gap') {\n gap = { expectedSeq: verdict.expectedSeq, lastSeq: verdict.lastSeq }\n break\n }\n if (verdict.kind === 'first') {\n firstSeq = item.seq as number\n /*\n * 계약이 `unseenBefore` 를 내면 그것을 쓴다. 설치된 버전이 그보다 오래됐으면 번호에서 센다 —\n * 첫 봉투의 번호 아래로 그만큼이 있고 우리는 그것을 받은 적이 없다.\n *\n * ⚠ 이 두 번째 길은 계약 0.9.9 가 깔리면 지운다. 그때까지 두는 이유는, 계약을 기다리며\n * 비워 두면 **코드는 있는데 아무 일도 안 하는 상태**가 되고 그것이 이 결함의 원인과 같은\n * 모양이기 때문이다.\n */\n const unseen = (verdict as any).unseenBefore ?? (item.seq as number) - 1\n if (unseen > 0) firstUnseenBefore = unseen\n }\n if (lowestSeq === undefined) lowestSeq = item.seq as number\n cursor = verdict.lastSeq\n take++\n }\n\n return {\n /*\n * **봉투의 시각을 레코드에 얹는다** — 레코드가 자기 시각을 말하면 그것이 이긴다.\n *\n * 시각을 안 싣는 레코드는 ingest 시각으로 떨어지고, 그 값이 실행할 때마다 달라서 fact identity 가\n * 매번 바뀐다. 그래서 같은 사실을 다시 받으면 dedupe 가 안 되고 새 사실로 앉는다 —\n * `fillTwinScope` 를 두 번 돌렸더니 저널이 40 → 69건이 됐다(2026-09-06 측정).\n */\n records: items.slice(0, take).map(i => {\n const r = i.record as any\n if (!i.at || r == null || typeof r !== 'object') return i.record\n return r.at === undefined && r.eventTime === undefined ? { ...r, at: i.at } : i.record\n }),\n numbered: true,\n /* 앞의 것을 하나도 못 받았으면 커서를 올릴 것이 없다. */\n ...(take > 0 && typeof cursor === 'number' ? { advancedSeq: cursor } : {}),\n ...(gap ? { gap } : {}),\n ...(firstUnseenBefore ? { firstUnseenBefore } : {}),\n ...(firstSeq !== undefined ? { firstSeq } : {}),\n ...(lowestSeq !== undefined ? { lowestSeq } : {})\n }\n}\n\n/** 이 연결이 이 트윈을 만들었나 — 남의 트윈에 밀어 넣지 못하게. */\nexport function producedInstance(ref: { scopeSpec?: any } | null | undefined, instanceId: string): { siteId: string } | undefined {\n const produced: any[] = (ref?.scopeSpec as any)?.produced ?? []\n const hit = produced.find(p => String(p?.instanceId ?? '') === instanceId)\n return hit?.siteId ? { siteId: String(hit.siteId) } : undefined\n}\n\n"]}
@@ -788,7 +788,7 @@ export type ReferenceGroundingLevel = 'synthetic' | 'mock' | 'instance' | 'produ
788
788
  export interface ReferenceGrounding {
789
789
  level: ReferenceGroundingLevel;
790
790
  /**
791
- * **무엇에 대고 확인했나** — 사람이 읽는 한 줄(원본의 판·인스턴스·목의 이름).
791
+ * **무엇에 대고 확인했나** — 사람이 읽는 한 줄(원본의 버전·인스턴스·목의 이름).
792
792
  *
793
793
  * `instance`·`product` 를 주장하면서 이것이 비어 있으면 `groundingOf` 가 `unknown` 으로 내린다 —
794
794
  * 가리키는 대상이 없는 주장은 주장이 아니고, 그런 주장은 낡아도 아무도 알 수 없다.