@things-factory/headless-twin 10.1.206 → 10.1.209

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.
@@ -218,8 +218,6 @@ export declare function settleByApproval(store: CommandStore, domainId: string,
218
218
  at: string;
219
219
  note?: string;
220
220
  }): Promise<StoredCommand>;
221
- /** 낸 사람이 자기 것을 승인하려 했다 — 워크리스트가 쓰는 낱말과 **같은 값**이어야 한다. */
222
- export declare const SELF_APPROVAL = "self-approval";
223
221
  /**
224
222
  * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**
225
223
  *
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SELF_APPROVAL = void 0;
4
3
  exports.propose = propose;
5
4
  exports.linkApproval = linkApproval;
6
5
  exports.approve = approve;
@@ -56,12 +55,9 @@ async function propose(store, domainId, specs, seed) {
56
55
  throw new Error(`선언되지 않은 조치 종류다 (${seed.type}) — 선언된 것: ${specs.map(s => s.type).join(', ') || '(없음)'}. ` +
57
56
  '되돌릴 수 있는지를 모르는 채로 승인 게이트를 지나게 할 수 없다');
58
57
  }
59
- /*
60
- * 사람이 낸 것이면 누구인지 있어야 한다. 없이 받으면 그 커맨드는 **아무나 승인할 수 있는 것**이 되고,
61
- * 자기 승인을 막는 규칙이 그 한 건만 비껴간다.
62
- */
58
+ /* 사람이 낸 것이면 누구인지 있어야 한다 — 되돌릴 수 없는 조치는 누가 냈는지가 기록에 남는다. */
63
59
  if (seed.origin === 'operator' && !seed.proposedBy?.trim()) {
64
- throw new Error('사람이 내는 조치는 낸 사람이 있어야 한다 — 없으면 자기 것을 자기가 승인하는 것을 막을 수 없다');
60
+ throw new Error('사람이 내는 조치는 낸 사람이 있어야 한다 — 누가 냈는지 남지 않는 조치는 받지 않는다');
65
61
  }
66
62
  /* 요구하는 칸이 없으면 계약이 던진다 — 빈 지시가 대기 줄에 쌓이면 승인하는 사람이 판단할 근거가 없다. */
67
63
  return await store.create(domainId, {
@@ -92,7 +88,6 @@ async function linkApproval(store, domainId, id, approvalRef) {
92
88
  async function approve(store, domainId, id, approval) {
93
89
  const command = await mustLoad(store, domainId, id);
94
90
  assertDirectDoorOpen(command);
95
- assertNotSelfApproval(command, approval.by);
96
91
  const approved = (0, ops_contract_1.approveCommand)(command, approval);
97
92
  await store.save(domainId, { ...approved, ...carried(command) });
98
93
  return approved;
@@ -128,11 +123,6 @@ async function settleByApproval(store, domainId, id, decision) {
128
123
  };
129
124
  if (!decision.approved)
130
125
  return await settle(store, domainId, command, false, approval);
131
- /*
132
- * 결재선을 지나온 것도 이 규칙을 지난다. 결재선이 비어 있으면 업무 목록이 스레드를 곧바로 종료하고
133
- * (§`submit.ts`), 승인자를 `Myself` 로 넣으면 낸 사람이 승인자다 — 두 경우 다 여기서 막힌다.
134
- */
135
- assertNotSelfApproval(command, decision.by);
136
126
  const approved = (0, ops_contract_1.approveCommand)(command, approval);
137
127
  await store.save(domainId, { ...approved, ...carried(command) });
138
128
  return approved;
@@ -150,48 +140,6 @@ function assertDirectDoorOpen(command) {
150
140
  throw new Error(`이 조치는 결재선에 올라가 있다 (${command.approvalRef}) — 승인·거절은 업무 목록에서 한다. ` +
151
141
  '여기서 결정하면 결재선에는 대기로 남고 조치는 나간 상태가 된다');
152
142
  }
153
- /**
154
- * **낸 사람은 자기 것을 승인하지 못한다.**
155
- *
156
- * ── 왜 필요한가 (2026-08-31) ─────────────────────────────────────────────
157
- * 결재선에 올리는 길을 만들고 나서 업무 목록의 `submit.ts` 를 읽었다. **결재선이 비어 있으면 스레드가
158
- * 곧바로 종료되고**, 그러면 우리 콜백이 「승인됐다」로 받는다. 승인자를 지정하지 않은 도메인에서는 낸
159
- * 사람이 그 자리에서 자기 조치를 승인한 것이 된다. 오류 한 줄 없이.
160
- *
161
- * 결재선을 지정한 경우에도 열린다 — `Myself` 를 승인자로 넣으면 낸 사람이 승인자다.
162
- *
163
- * 그래서 규칙을 게이트 안쪽에 둔다. 올리는 자리에서만 막으면 그 자리를 지나지 않는 길에서 다시 열린다.
164
- * 되돌릴 수 없는 조치에 두 사람이 필요하다는 것이 이 표의 존재 이유다.
165
- *
166
- * 거절에는 걸지 않는다 — 낸 사람이 자기 것을 물리는 것은 막을 이유가 없다.
167
- */
168
- function assertNotSelfApproval(command, by) {
169
- const proposer = command.proposedBy?.trim();
170
- if (!proposer)
171
- return;
172
- if (proposer.toLowerCase() === String(by ?? '').trim().toLowerCase()) {
173
- throw selfApprovalRefused(proposer);
174
- }
175
- }
176
- /**
177
- * 같은 사실에는 **같은 낱말** — `self-approval` (ADR-0052 ③, ADR-0042 ③).
178
- *
179
- * 워크리스트에도 같은 규칙이 선다(`worklist/server/controllers/activity-approval/own-request.ts`).
180
- * **두 벌이 아니라 두 문이다** — 커널은 업무 목록을 안 지나는 길(API · AI 발의)에서 자기 커맨드를
181
- * 지키고, 워크리스트는 결재 문을 지킨다. 커널↔호스트 경계를 넘어 합치지 않는다.
182
- *
183
- * 두 문이 위험한 것은 **서로 다른 답을 낼 때**다. 그래서 문장이 아니라 **코드를 맞춘다** — 문장은
184
- * 번역도 집계도 안 되고, 고치는 날 두 문이 다른 말을 한다. 붙잡는 쪽은 `code` 를 보고, 사람에게는
185
- * `message` 를 보인다.
186
- */
187
- function selfApprovalRefused(proposer) {
188
- const error = new Error(`낸 사람이 자기 조치를 승인할 수 없다 (${proposer}) — 되돌릴 수 없는 조치에는 다른 사람의 승인이 필요하다. ` +
189
- '승인자가 없다면 결재선에 승인자를 지정하거나, 다른 사람이 승인해야 한다');
190
- error.code = exports.SELF_APPROVAL;
191
- return error;
192
- }
193
- /** 낸 사람이 자기 것을 승인하려 했다 — 워크리스트가 쓰는 낱말과 **같은 값**이어야 한다. */
194
- exports.SELF_APPROVAL = 'self-approval';
195
143
  /** 승인 기록만 바뀌고 나머지는 그대로 간다 — 하나라도 흘리면 그 사실이 사라진다. */
196
144
  function carried(command) {
197
145
  return {
@@ -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 * 사람이 낸 것이면 누구인지 있어야 한다. 없이 받으면 그 커맨드는 **아무나 승인할 수 있는 것**이 되고,\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 assertNotSelfApproval(command, approval.by)\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 /*\n * 결재선을 지나온 것도 이 규칙을 지난다. 결재선이 비어 있으면 업무 목록이 스레드를 곧바로 종료하고\n * (§`submit.ts`), 승인자를 `Myself` 로 넣으면 낸 사람이 승인자다 — 두 경우 다 여기서 막힌다.\n */\n assertNotSelfApproval(command, decision.by)\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/**\n * **낸 사람은 자기 것을 승인하지 못한다.**\n *\n * ── 왜 필요한가 (2026-08-31) ─────────────────────────────────────────────\n * 결재선에 올리는 길을 만들고 나서 업무 목록의 `submit.ts` 를 읽었다. **결재선이 비어 있으면 스레드가\n * 곧바로 종료되고**, 그러면 우리 콜백이 「승인됐다」로 받는다. 승인자를 지정하지 않은 도메인에서는 낸\n * 사람이 그 자리에서 자기 조치를 승인한 것이 된다. 오류 한 줄 없이.\n *\n * 결재선을 지정한 경우에도 열린다 — `Myself` 를 승인자로 넣으면 낸 사람이 승인자다.\n *\n * 그래서 규칙을 게이트 안쪽에 둔다. 올리는 자리에서만 막으면 그 자리를 지나지 않는 길에서 다시 열린다.\n * 되돌릴 수 없는 조치에 두 사람이 필요하다는 것이 이 표의 존재 이유다.\n *\n * 거절에는 걸지 않는다 — 낸 사람이 자기 것을 물리는 것은 막을 이유가 없다.\n */\nfunction assertNotSelfApproval(command: StoredCommand, by: string): void {\n const proposer = command.proposedBy?.trim()\n if (!proposer) return\n\n if (proposer.toLowerCase() === String(by ?? '').trim().toLowerCase()) {\n throw selfApprovalRefused(proposer)\n }\n}\n\n/**\n * 같은 사실에는 **같은 낱말** — `self-approval` (ADR-0052 ③, ADR-0042 ③).\n *\n * 워크리스트에도 같은 규칙이 선다(`worklist/server/controllers/activity-approval/own-request.ts`).\n * **두 벌이 아니라 두 문이다** — 커널은 업무 목록을 안 지나는 길(API · AI 발의)에서 자기 커맨드를\n * 지키고, 워크리스트는 결재 문을 지킨다. 커널↔호스트 경계를 넘어 합치지 않는다.\n *\n * 두 문이 위험한 것은 **서로 다른 답을 낼 때**다. 그래서 문장이 아니라 **코드를 맞춘다** — 문장은\n * 번역도 집계도 안 되고, 고치는 날 두 문이 다른 말을 한다. 붙잡는 쪽은 `code` 를 보고, 사람에게는\n * `message` 를 보인다.\n */\nfunction selfApprovalRefused(proposer: string): Error & { code: string } {\n const error = new Error(\n `낸 사람이 자기 조치를 승인할 수 없다 (${proposer}) — 되돌릴 수 없는 조치에는 다른 사람의 승인이 필요하다. ` +\n '승인자가 없다면 결재선에 승인자를 지정하거나, 다른 사람이 승인해야 한다'\n ) as Error & { code: string }\n\n error.code = SELF_APPROVAL\n return error\n}\n\n/** 낸 사람이 자기 것을 승인하려 했다 — 워크리스트가 쓰는 낱말과 **같은 값**이어야 한다. */\nexport const SELF_APPROVAL = 'self-approval'\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"]}
@@ -43,8 +43,8 @@ async function runOneTwin(twin, deps) {
43
43
  skips: [
44
44
  {
45
45
  ruleId: '(전체)',
46
- reason: '이 저장소는 이미 올라간 근거를 말해 주지 않는다 — 판정하면 같은 조건이 주기마다 다시 올라간다. ' +
47
- 'CommandStore.openCauses 가 필요하다'
46
+ reason: '기존 조치 요청의 근거를 조회할 수 없어 규칙을 평가하지 않았습니다. 중복 요청을 방지하려면 ' +
47
+ 'CommandStore.openCauses를 구현해야 합니다.'
48
48
  }
49
49
  ]
50
50
  };
@@ -70,9 +70,9 @@ async function runOneTwin(twin, deps) {
70
70
  {
71
71
  ruleId: '(전체)',
72
72
  reason: mode === 'sim'
73
- ? '이 트윈은 시뮬레이션이다 — 시늉된 조건에서 현장 지시를 내지 않는다. ' +
74
- '예측을 근거로 미리 내려면 그것을 선언하는 자리가 필요하고, 아직 없다'
75
- : '이 트윈이 관측을 비추는지 알 수 없다 — 모름을 「관측이다」로 읽지 않는다'
73
+ ? '이 트윈은 시뮬레이션 모드이므로 현장 지시를 생성하지 않습니다. ' +
74
+ '시뮬레이션·예측을 근거로 한 현장 지시는 현재 지원하지 않습니다.'
75
+ : '트윈의 실행 모드를 확인할 수 없어 실제 관측에 근거한 현장 지시를 생성할 수 없습니다.'
76
76
  }
77
77
  ]
78
78
  };
@@ -84,7 +84,7 @@ async function runOneTwin(twin, deps) {
84
84
  * 「조건이 하나도 없다」가 되고, 규칙이 도는 줄로 보인다.
85
85
  */
86
86
  return {
87
- skips: [{ ruleId: '(전체)', reason: '이 트윈의 상태를 읽을 수 없다 — 멈춰 있거나 아직 서지 않았다' }]
87
+ skips: [{ ruleId: '(전체)', reason: '트윈의 상태를 조회할 수 없습니다. 실행이 중지되었거나 아직 시작되지 않았을 수 있습니다.' }]
88
88
  };
89
89
  }
90
90
  const openCauses = await deps.store.openCauses(twin.domainId, twin.instanceId);
@@ -1 +1 @@
1
- {"version":3,"file":"rule-runner.js","sourceRoot":"","sources":["../../../server/service/actuation/rule-runner.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **규칙을 주기로 돈다** — 판정을 저장소에 잇는 자리.\n *\n * ── 왜 틱이 아닌가 (2026-09-01) ──────────────────────────────────────────\n * 커널 틱 안에 넣으려다 `tickGuarded` 를 읽고 접었다. 그 자리는 **예외가 나면 트윈을 멈추고**, 굶김\n * 감시가 느린 틱 셋에 트윈을 내린다. 규칙 판정은 DB 를 두 번 건드리므로 그 감시에 정확히 걸린다.\n *\n * 그리고 필요하지도 않다. 「병목이 세 시간째」를 1초마다 다시 물을 이유가 없다.\n *\n * ── 규칙은 결재선에 올리지 않는다 ────────────────────────────────────────\n * 이것이 이 파일의 가장 중요한 규율이다.\n *\n * 업무 목록의 결재선은 **올린 사람 기준**으로 풀린다 — `Myself` · `MyDepartment` · `MySupervisor` 가\n * 전부 `context.state.user` 를 본다(§`getSystemUserFromOrgMemberItem`). 배경 루프에는 「나」가 없다.\n * 시스템 사용자를 지어내 넘기면 그 셋이 **시스템 사용자를 가리키게 되고**, 결재선이 뜻을 잃는다.\n *\n * 그래서 규칙은 **내는 것까지** 한다. 결재선에 올리는 것은 사람이 한다(§`raiseTwinCommand`) — 그 사람이\n * 「이것을 승인자들 앞에 놓았다」의 주체가 되고, 그 사람 기준으로 결재선이 풀린다.\n *\n * 올리기 전까지 그 조치는 나가지 못한다. 게이트는 그대로 서 있다.\n */\nimport type { CommandTypeSpec } from '@operato/ops-contract'\n\nimport { twinError, twinWarn } from '../../engine/log.js'\n\nimport { propose, type CommandStore } from './command-dispatcher.js'\nimport { evaluateRules, type ActuationRuleSpec, type AttentionLike, type RuleSkip } from './rule-evaluate.js'\n\n/** 이 프로세스가 든 트윈 하나. */\nexport interface OwnedTwin {\n domainId: string\n instanceId: string\n}\n\n/** 한 트윈을 돌고 나서 남는 사실. */\nexport interface TwinRunReport {\n domainId: string\n instanceId: string\n /** 실제로 적힌 커맨드 수. */\n proposed: number\n /** 규칙마다 몇 개를 냈나 — 규칙 행에 누계를 적으려면 나누어 세어야 한다. */\n proposedByRule: Record<string, number>\n /** 왜 안 올렸나 — 규칙이 조용히 안 도는 것과 조건이 없어서 안 내는 것을 구별한다. */\n skips: RuleSkip[]\n /** 상한에 닿아 두고 온 수. */\n withheld?: number\n /** 이 트윈을 아예 못 돌았으면 그 이유. */\n failed?: string\n}\n\n/** 밖에서 받는 것들 — 저장소도 엔진도 시계도 인자다. 그래야 시험이 재기동과 상한을 걸을 수 있다. */\nexport interface RuleRunnerDeps {\n /** 이 프로세스가 든 트윈들. 남의 프로세스가 든 트윈은 그쪽이 돈다. */\n ownedTwins(): OwnedTwin[]\n /** 이 트윈에 걸린 규칙 — 없으면 스냅샷을 읽지 않는다. */\n rulesOf(twin: OwnedTwin): Promise<ActuationRuleSpec[]>\n /** 지금 서 있는 주의들. */\n attentionsOf(twin: OwnedTwin): AttentionLike[] | undefined\n /**\n * 이 트윈이 **무엇을 비추고 있나** — 관측(`live`)인가 가정(`sim`)인가.\n *\n * 이 값이 없거나 `live` 가 아니면 조치를 내지 않는다(§`runOneTwin`). 모름을 통과로 읽지 않는다.\n */\n modeOf(twin: OwnedTwin): 'live' | 'sim' | undefined\n /** 커맨드가 사는 자리. */\n store: CommandStore\n /** 선언된 조치 종류 — 없는 종류를 가리키는 규칙은 판정이 걸러 낸다. */\n specs: readonly CommandTypeSpec[]\n nowMs(): number\n /** 돈 결과를 규칙 행에 남긴다 — 화면이 「이 규칙이 돌고 있나」를 말할 수 있게. */\n noteRun?(twin: OwnedTwin, report: TwinRunReport): Promise<void>\n maxPerRun?: number\n}\n\n/**\n * 한 바퀴 돈다 — **한 트윈이 실패해도 나머지는 돈다.**\n *\n * 트윈 하나의 규칙이 잘못됐다고 다른 공장의 규칙이 멈추면, 고치는 동안 전체가 멈춘 셈이 된다.\n */\nexport async function runRulesOnce(deps: RuleRunnerDeps): Promise<TwinRunReport[]> {\n const out: TwinRunReport[] = []\n\n for (const twin of deps.ownedTwins()) {\n const report: TwinRunReport = { ...twin, proposed: 0, proposedByRule: {}, skips: [] }\n try {\n Object.assign(report, await runOneTwin(twin, deps))\n } catch (e: any) {\n report.failed = String(e?.message ?? e)\n twinError(`[twin-rule] \"${twin.instanceId}\" 규칙을 돌지 못했다 — ${report.failed}`)\n }\n out.push(report)\n\n if (deps.noteRun) {\n await deps.noteRun(twin, report).catch(err =>\n twinWarn(`[twin-rule] \"${twin.instanceId}\" 규칙 기록을 남기지 못했다 — ${err?.message ?? err}`)\n )\n }\n }\n\n return out\n}\n\nasync function runOneTwin(twin: OwnedTwin, deps: RuleRunnerDeps): Promise<Partial<TwinRunReport>> {\n const rules = await deps.rulesOf(twin)\n /* 규칙이 없으면 스냅샷도 읽지 않는다 — 아무것도 안 할 일에 값을 만들지 않는다. */\n if (!rules.length) return {}\n\n /*\n * **이미 올라가 있는 근거를 저장소에서 읽는다.**\n *\n * 저장소가 그 길을 내주지 않으면 **판정하지 않는다.** 빈 집합을 만들어 주면 매 주기 같은 조건이 다시\n * 올라가 대기 줄에 같은 지시가 쌓인다 — 그것을 조용히 하지 않는다.\n */\n if (!deps.store.openCauses) {\n return {\n skips: [\n {\n ruleId: '(전체)',\n reason:\n '이 저장소는 이미 올라간 근거를 말해 주지 않는다 — 판정하면 같은 조건이 주기마다 다시 올라간다. ' +\n 'CommandStore.openCauses 가 필요하다'\n }\n ]\n }\n }\n\n /*\n * ── **시늉된 조건에서 현장 지시를 내지 않는다** (2026-09-01) ────────────\n *\n * 시뮬 트윈의 상태는 **가정**이다. 커널이 스스로 물품을 옮겨 만든 병목이고, 현장에서 관측된 것이\n * 아니다. 그 위에서 생산 지시를 내면 **지어낸 조건이 실제 지시가 된다** — 오늘 자극과 조치를 갈라\n * 놓은 것과 같은 부류의 잘못이고, 이쪽이 더 나쁘다(자극은 모델 안에서 끝나지만 이것은 현장으로 나간다).\n *\n * 실제로 그렇게 걸었다. 오늘 규칙을 확인한 트윈이 전부 `sim` 이었고, 시늉된 병목에서 생산 지시가\n * 올라왔다. 배선은 옳게 돌았지만 근거는 가정이었다.\n *\n * 예측을 근거로 미리 지시를 내는 것 자체는 있을 수 있는 일이다. 다만 그것은 **선언해서** 할 일이고,\n * 승인하는 사람이 「이것은 예측이다」를 알아야 한다. 그 선언이 아직 없으므로 지금은 막고, 왜 막았는지\n * 말한다 — 그 문장이 필요해진 사람이 무엇을 요청해야 하는지 알려 준다.\n */\n const mode = deps.modeOf(twin)\n if (mode !== 'live') {\n return {\n skips: [\n {\n ruleId: '(전체)',\n reason:\n mode === 'sim'\n ? '이 트윈은 시뮬레이션이다 — 시늉된 조건에서 현장 지시를 내지 않는다. ' +\n '예측을 근거로 미리 내려면 그것을 선언하는 자리가 필요하고, 아직 없다'\n : '이 트윈이 관측을 비추는지 알 수 없다 — 모름을 「관측이다」로 읽지 않는다'\n }\n ]\n }\n }\n\n const attentions = deps.attentionsOf(twin)\n if (attentions === undefined) {\n /*\n * 스냅샷이 없다 — 멈춘 트윈이거나 아직 안 선 트윈이다. **주의가 0건인 것과 다르다.** 0건으로 읽으면\n * 「조건이 하나도 없다」가 되고, 규칙이 도는 줄로 보인다.\n */\n return {\n skips: [{ ruleId: '(전체)', reason: '이 트윈의 상태를 읽을 수 없다 — 멈춰 있거나 아직 서지 않았다' }]\n }\n }\n\n const openCauses = await deps.store.openCauses(twin.domainId, twin.instanceId)\n /* 없으면 넘기지 않는다 — 판정이 「받지 못했다」와 「낸 적이 없다」를 구별해야 한다. */\n const lastProposedAt = await deps.store.lastProposedAt?.(twin.domainId, twin.instanceId)\n\n const verdict = evaluateRules({\n rules,\n attentions,\n specs: deps.specs,\n openCauses,\n ...(lastProposedAt ? { lastProposedAt } : {}),\n nowMs: deps.nowMs(),\n ...(deps.maxPerRun ? { maxPerRun: deps.maxPerRun } : {})\n })\n\n const skips = [...verdict.skips]\n const proposedByRule: Record<string, number> = {}\n let proposed = 0\n\n for (const p of verdict.proposals) {\n try {\n await propose(deps.store, twin.domainId, deps.specs, {\n id: newCommandId(),\n instanceId: twin.instanceId,\n type: p.commandType,\n /* 사람이 아니다. 그래서 낸 사람이 없고, 아무 승인자든 두 번째 사람이 된다. */\n origin: 'rule',\n proposedAt: new Date(deps.nowMs()).toISOString(),\n payload: p.payload,\n causeRef: p.attentionId,\n ruleRef: p.ruleId\n })\n proposed++\n proposedByRule[p.ruleId] = (proposedByRule[p.ruleId] ?? 0) + 1\n } catch (e: any) {\n /*\n * 하나가 거절돼도 나머지는 낸다. 그리고 **거절을 삼키지 않는다** — 규칙이 무언가를 내려 했는데\n * 못 냈다는 사실이 사라지면, 사람은 그 조건에 규칙이 안 걸린 줄로 읽는다.\n */\n skips.push({ ruleId: p.ruleId, attentionId: p.attentionId, reason: String(e?.message ?? e) })\n }\n }\n\n return { proposed, proposedByRule, skips, ...(verdict.withheld ? { withheld: verdict.withheld } : {}) }\n}\n\n/** 식별자 — 이 파일이 만든다. 저장소가 만들면 커맨드를 적기 전에 근거를 붙일 수 없다. */\nfunction newCommandId(): string {\n return globalThis.crypto.randomUUID()\n}\n"]}
1
+ {"version":3,"file":"rule-runner.js","sourceRoot":"","sources":["../../../server/service/actuation/rule-runner.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **규칙을 주기로 돈다** — 판정을 저장소에 잇는 자리.\n *\n * ── 왜 틱이 아닌가 (2026-09-01) ──────────────────────────────────────────\n * 커널 틱 안에 넣으려다 `tickGuarded` 를 읽고 접었다. 그 자리는 **예외가 나면 트윈을 멈추고**, 굶김\n * 감시가 느린 틱 셋에 트윈을 내린다. 규칙 판정은 DB 를 두 번 건드리므로 그 감시에 정확히 걸린다.\n *\n * 그리고 필요하지도 않다. 「병목이 세 시간째」를 1초마다 다시 물을 이유가 없다.\n *\n * ── 규칙은 결재선에 올리지 않는다 ────────────────────────────────────────\n * 이것이 이 파일의 가장 중요한 규율이다.\n *\n * 업무 목록의 결재선은 **올린 사람 기준**으로 풀린다 — `Myself` · `MyDepartment` · `MySupervisor` 가\n * 전부 `context.state.user` 를 본다(§`getSystemUserFromOrgMemberItem`). 배경 루프에는 「나」가 없다.\n * 시스템 사용자를 지어내 넘기면 그 셋이 **시스템 사용자를 가리키게 되고**, 결재선이 뜻을 잃는다.\n *\n * 그래서 규칙은 **내는 것까지** 한다. 결재선에 올리는 것은 사람이 한다(§`raiseTwinCommand`) — 그 사람이\n * 「이것을 승인자들 앞에 놓았다」의 주체가 되고, 그 사람 기준으로 결재선이 풀린다.\n *\n * 올리기 전까지 그 조치는 나가지 못한다. 게이트는 그대로 서 있다.\n */\nimport type { CommandTypeSpec } from '@operato/ops-contract'\n\nimport { twinError, twinWarn } from '../../engine/log.js'\n\nimport { propose, type CommandStore } from './command-dispatcher.js'\nimport { evaluateRules, type ActuationRuleSpec, type AttentionLike, type RuleSkip } from './rule-evaluate.js'\n\n/** 이 프로세스가 든 트윈 하나. */\nexport interface OwnedTwin {\n domainId: string\n instanceId: string\n}\n\n/** 한 트윈을 돌고 나서 남는 사실. */\nexport interface TwinRunReport {\n domainId: string\n instanceId: string\n /** 실제로 적힌 커맨드 수. */\n proposed: number\n /** 규칙마다 몇 개를 냈나 — 규칙 행에 누계를 적으려면 나누어 세어야 한다. */\n proposedByRule: Record<string, number>\n /** 왜 안 올렸나 — 규칙이 조용히 안 도는 것과 조건이 없어서 안 내는 것을 구별한다. */\n skips: RuleSkip[]\n /** 상한에 닿아 두고 온 수. */\n withheld?: number\n /** 이 트윈을 아예 못 돌았으면 그 이유. */\n failed?: string\n}\n\n/** 밖에서 받는 것들 — 저장소도 엔진도 시계도 인자다. 그래야 시험이 재기동과 상한을 걸을 수 있다. */\nexport interface RuleRunnerDeps {\n /** 이 프로세스가 든 트윈들. 남의 프로세스가 든 트윈은 그쪽이 돈다. */\n ownedTwins(): OwnedTwin[]\n /** 이 트윈에 걸린 규칙 — 없으면 스냅샷을 읽지 않는다. */\n rulesOf(twin: OwnedTwin): Promise<ActuationRuleSpec[]>\n /** 지금 서 있는 주의들. */\n attentionsOf(twin: OwnedTwin): AttentionLike[] | undefined\n /**\n * 이 트윈이 **무엇을 비추고 있나** — 관측(`live`)인가 가정(`sim`)인가.\n *\n * 이 값이 없거나 `live` 가 아니면 조치를 내지 않는다(§`runOneTwin`). 모름을 통과로 읽지 않는다.\n */\n modeOf(twin: OwnedTwin): 'live' | 'sim' | undefined\n /** 커맨드가 사는 자리. */\n store: CommandStore\n /** 선언된 조치 종류 — 없는 종류를 가리키는 규칙은 판정이 걸러 낸다. */\n specs: readonly CommandTypeSpec[]\n nowMs(): number\n /** 돈 결과를 규칙 행에 남긴다 — 화면이 「이 규칙이 돌고 있나」를 말할 수 있게. */\n noteRun?(twin: OwnedTwin, report: TwinRunReport): Promise<void>\n maxPerRun?: number\n}\n\n/**\n * 한 바퀴 돈다 — **한 트윈이 실패해도 나머지는 돈다.**\n *\n * 트윈 하나의 규칙이 잘못됐다고 다른 공장의 규칙이 멈추면, 고치는 동안 전체가 멈춘 셈이 된다.\n */\nexport async function runRulesOnce(deps: RuleRunnerDeps): Promise<TwinRunReport[]> {\n const out: TwinRunReport[] = []\n\n for (const twin of deps.ownedTwins()) {\n const report: TwinRunReport = { ...twin, proposed: 0, proposedByRule: {}, skips: [] }\n try {\n Object.assign(report, await runOneTwin(twin, deps))\n } catch (e: any) {\n report.failed = String(e?.message ?? e)\n twinError(`[twin-rule] \"${twin.instanceId}\" 규칙을 돌지 못했다 — ${report.failed}`)\n }\n out.push(report)\n\n if (deps.noteRun) {\n await deps.noteRun(twin, report).catch(err =>\n twinWarn(`[twin-rule] \"${twin.instanceId}\" 규칙 기록을 남기지 못했다 — ${err?.message ?? err}`)\n )\n }\n }\n\n return out\n}\n\nasync function runOneTwin(twin: OwnedTwin, deps: RuleRunnerDeps): Promise<Partial<TwinRunReport>> {\n const rules = await deps.rulesOf(twin)\n /* 규칙이 없으면 스냅샷도 읽지 않는다 — 아무것도 안 할 일에 값을 만들지 않는다. */\n if (!rules.length) return {}\n\n /*\n * **이미 올라가 있는 근거를 저장소에서 읽는다.**\n *\n * 저장소가 그 길을 내주지 않으면 **판정하지 않는다.** 빈 집합을 만들어 주면 매 주기 같은 조건이 다시\n * 올라가 대기 줄에 같은 지시가 쌓인다 — 그것을 조용히 하지 않는다.\n */\n if (!deps.store.openCauses) {\n return {\n skips: [\n {\n ruleId: '(전체)',\n reason:\n '기존 조치 요청의 근거를 조회할 수 없어 규칙을 평가하지 않았습니다. 중복 요청을 방지하려면 ' +\n 'CommandStore.openCauses를 구현해야 합니다.'\n }\n ]\n }\n }\n\n /*\n * ── **시늉된 조건에서 현장 지시를 내지 않는다** (2026-09-01) ────────────\n *\n * 시뮬 트윈의 상태는 **가정**이다. 커널이 스스로 물품을 옮겨 만든 병목이고, 현장에서 관측된 것이\n * 아니다. 그 위에서 생산 지시를 내면 **지어낸 조건이 실제 지시가 된다** — 오늘 자극과 조치를 갈라\n * 놓은 것과 같은 부류의 잘못이고, 이쪽이 더 나쁘다(자극은 모델 안에서 끝나지만 이것은 현장으로 나간다).\n *\n * 실제로 그렇게 걸었다. 오늘 규칙을 확인한 트윈이 전부 `sim` 이었고, 시늉된 병목에서 생산 지시가\n * 올라왔다. 배선은 옳게 돌았지만 근거는 가정이었다.\n *\n * 예측을 근거로 미리 지시를 내는 것 자체는 있을 수 있는 일이다. 다만 그것은 **선언해서** 할 일이고,\n * 승인하는 사람이 「이것은 예측이다」를 알아야 한다. 그 선언이 아직 없으므로 지금은 막고, 왜 막았는지\n * 말한다 — 그 문장이 필요해진 사람이 무엇을 요청해야 하는지 알려 준다.\n */\n const mode = deps.modeOf(twin)\n if (mode !== 'live') {\n return {\n skips: [\n {\n ruleId: '(전체)',\n reason:\n mode === 'sim'\n ? '이 트윈은 시뮬레이션 모드이므로 현장 지시를 생성하지 않습니다. ' +\n '시뮬레이션·예측을 근거로 한 현장 지시는 현재 지원하지 않습니다.'\n : '트윈의 실행 모드를 확인할 수 없어 실제 관측에 근거한 현장 지시를 생성할 수 없습니다.'\n }\n ]\n }\n }\n\n const attentions = deps.attentionsOf(twin)\n if (attentions === undefined) {\n /*\n * 스냅샷이 없다 — 멈춘 트윈이거나 아직 안 선 트윈이다. **주의가 0건인 것과 다르다.** 0건으로 읽으면\n * 「조건이 하나도 없다」가 되고, 규칙이 도는 줄로 보인다.\n */\n return {\n skips: [{ ruleId: '(전체)', reason: '트윈의 상태를 조회할 수 없습니다. 실행이 중지되었거나 아직 시작되지 않았을 수 있습니다.' }]\n }\n }\n\n const openCauses = await deps.store.openCauses(twin.domainId, twin.instanceId)\n /* 없으면 넘기지 않는다 — 판정이 「받지 못했다」와 「낸 적이 없다」를 구별해야 한다. */\n const lastProposedAt = await deps.store.lastProposedAt?.(twin.domainId, twin.instanceId)\n\n const verdict = evaluateRules({\n rules,\n attentions,\n specs: deps.specs,\n openCauses,\n ...(lastProposedAt ? { lastProposedAt } : {}),\n nowMs: deps.nowMs(),\n ...(deps.maxPerRun ? { maxPerRun: deps.maxPerRun } : {})\n })\n\n const skips = [...verdict.skips]\n const proposedByRule: Record<string, number> = {}\n let proposed = 0\n\n for (const p of verdict.proposals) {\n try {\n await propose(deps.store, twin.domainId, deps.specs, {\n id: newCommandId(),\n instanceId: twin.instanceId,\n type: p.commandType,\n /* 사람이 아니다. 그래서 낸 사람이 없고, 아무 승인자든 두 번째 사람이 된다. */\n origin: 'rule',\n proposedAt: new Date(deps.nowMs()).toISOString(),\n payload: p.payload,\n causeRef: p.attentionId,\n ruleRef: p.ruleId\n })\n proposed++\n proposedByRule[p.ruleId] = (proposedByRule[p.ruleId] ?? 0) + 1\n } catch (e: any) {\n /*\n * 하나가 거절돼도 나머지는 낸다. 그리고 **거절을 삼키지 않는다** — 규칙이 무언가를 내려 했는데\n * 못 냈다는 사실이 사라지면, 사람은 그 조건에 규칙이 안 걸린 줄로 읽는다.\n */\n skips.push({ ruleId: p.ruleId, attentionId: p.attentionId, reason: String(e?.message ?? e) })\n }\n }\n\n return { proposed, proposedByRule, skips, ...(verdict.withheld ? { withheld: verdict.withheld } : {}) }\n}\n\n/** 식별자 — 이 파일이 만든다. 저장소가 만들면 커맨드를 적기 전에 근거를 붙일 수 없다. */\nfunction newCommandId(): string {\n return globalThis.crypto.randomUUID()\n}\n"]}
@@ -1,14 +1,14 @@
1
1
  import { intakeCollisions } from './connection-portability.js';
2
2
  export declare class ConnectionPortabilityResolver {
3
3
  /**
4
- * 지금 연결을 꾸러미로 낸다 — **비밀값은 안 나간다.**
4
+ * 지금 연결을 패키지로 낸다 — **비밀값은 안 나간다.**
5
5
  *
6
6
  * 이름 규칙으로 걸러서 **새 비밀 칸도 기본으로 안 나간다**(`SECRET_KEYS`). 나가지 않은 것은
7
- * 꾸러미가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.
7
+ * 패키지가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.
8
8
  */
9
9
  twinConnectionBundle(context: ResolverContext, note?: string): Promise<any>;
10
10
  /**
11
- * 꾸러미를 받아 적용한다 — **`dryRun` 이 기본이다.**
11
+ * 패키지를 받아 적용한다 — **`dryRun` 이 기본이다.**
12
12
  *
13
13
  * 쓰기가 기본이면 「무엇이 바뀌나」를 보기 위해 쓰게 된다. 계획을 먼저 보고 사람이 정하는 것이
14
14
  * 이 문의 요점이고, 그래서 적용은 명시해야 일어난다.
@@ -14,7 +14,7 @@ const tslib_1 = require("tslib");
14
14
  * 안 옮긴다 잰 값 그 서버에서 다시 세면 된다
15
15
  * 안 옮긴다 비밀값 그 환경의 것이지 셋업이 아니다
16
16
  *
17
- * **꾸러미는 정본이 아니라 운반 수단이다.** 정본은 언제나 그 배포의 DB 이고, 꾸러미는 쓰고 버린다.
17
+ * **패키지는 정본이 아니라 운반 수단이다.** 정본은 언제나 그 배포의 DB 이고, 패키지는 쓰고 버린다.
18
18
  *
19
19
  * ── 주체는 받는 쪽이다 ─────────────────────────────────────────────────────
20
20
  * 운영이 개발에서 **당겨온다.** 개발이 운영으로 밀면 개발 환경에 운영 자격이 살고, 그 통로는 실수로도
@@ -39,13 +39,13 @@ function hasSecret(source, key) {
39
39
  }
40
40
  let ConnectionPortabilityResolver = class ConnectionPortabilityResolver {
41
41
  /**
42
- * 지금 연결을 꾸러미로 낸다 — **비밀값은 안 나간다.**
42
+ * 지금 연결을 패키지로 낸다 — **비밀값은 안 나간다.**
43
43
  *
44
44
  * 이름 규칙으로 걸러서 **새 비밀 칸도 기본으로 안 나간다**(`SECRET_KEYS`). 나가지 않은 것은
45
- * 꾸러미가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.
45
+ * 패키지가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.
46
46
  */
47
47
  /*
48
- * **읽기인데 쓰기와 같은 문을 쓴다.** 이 꾸러미는 연결 주소·도메인과 **비밀 칸의 이름**까지 낸다 —
48
+ * **읽기인데 쓰기와 같은 문을 쓴다.** 이 패키지는 연결 주소·도메인과 **비밀 칸의 이름**까지 낸다 —
49
49
  * 그 이름만으로도 그 배포가 무엇에 붙어 있는지 드러난다. 권한 가드가 「쓰기는 GATE 하나」로
50
50
  * 통일해 두었고, 이 문은 읽기지만 그 무게가 쓰기 쪽이다(ADR-0027).
51
51
  */
@@ -55,7 +55,7 @@ let ConnectionPortabilityResolver = class ConnectionPortabilityResolver {
55
55
  return (0, connection_portability_js_1.exportConnections)(rows, { at: new Date().toISOString(), domain: domain.subdomain ?? domain.name, ...(note ? { note } : {}) });
56
56
  }
57
57
  /**
58
- * 꾸러미를 받아 적용한다 — **`dryRun` 이 기본이다.**
58
+ * 패키지를 받아 적용한다 — **`dryRun` 이 기본이다.**
59
59
  *
60
60
  * 쓰기가 기본이면 「무엇이 바뀌나」를 보기 위해 쓰게 된다. 계획을 먼저 보고 사람이 정하는 것이
61
61
  * 이 문의 요점이고, 그래서 적용은 명시해야 일어난다.
@@ -82,11 +82,11 @@ let ConnectionPortabilityResolver = class ConnectionPortabilityResolver {
82
82
  continue;
83
83
  const c = a.connection;
84
84
  /*
85
- * **비밀값을 빈 값으로 안 채운다.** 꾸러미에 비밀값이 없고, `planImport` 가 그 이름을
85
+ * **비밀값을 빈 값으로 안 채운다.** 패키지에 비밀값이 없고, `planImport` 가 그 이름을
86
86
  * `missingSecrets`·`needsHuman` 으로 이미 냈다. 여기서 그 칸을 `undefined` 로 쓰면 사람이
87
87
  * 화면에서 넣어 둔 값을 파일 하나가 지운다.
88
88
  *
89
- * 그래서 갱신은 **선언 칸만** 건드린다. 이미 있는 연결의 `connectionConfig` 는 꾸러미가 든
89
+ * 그래서 갱신은 **선언 칸만** 건드린다. 이미 있는 연결의 `connectionConfig` 는 패키지가 든
90
90
  * 것만 덮어쓰고 나머지 키는 그대로 남긴다.
91
91
  */
92
92
  const found = await repo.findOne({ where: { domain: { id: domainId }, source: c.source } });
@@ -1 +1 @@
1
- {"version":3,"file":"connection-portability-resolver.js","sourceRoot":"","sources":["../../../server/service/reference/connection-portability-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **연결을 배포 사이로 옮기는 문 셋** — 몸통은 `connection-portability.ts` 의 순수 함수들이다.\n *\n * ── 무엇을 옮기나 (사용자와 맞춘 개념, 2026-09-05) ─────────────────────────\n * **옮기는 것은 「붙는 법」이지 「붙은 결과」가 아니다.**\n *\n * 옮긴다 연결 선언 — 주소 · 도메인 · 라인 크기 · 자재 이름공간\n * 안 옮긴다 마스터 원본에서 다시 읽으면 된다\n * 안 옮긴다 사실(저널) 원본이 다시 보내면 된다\n * 안 옮긴다 잰 값 그 서버에서 다시 세면 된다\n * 안 옮긴다 비밀값 그 환경의 것이지 셋업이 아니다\n *\n * **꾸러미는 정본이 아니라 운반 수단이다.** 정본은 언제나 그 배포의 DB 이고, 꾸러미는 쓰고 버린다.\n *\n * ── 주체는 받는 쪽이다 ─────────────────────────────────────────────────────\n * 운영이 개발에서 **당겨온다.** 개발이 운영으로 밀면 개발 환경에 운영 자격이 살고, 그 통로는 실수로도\n * 열린다. 그래서 `pullTwinConnectionsFrom` 이 본래 원하던 문이고, 나머지 둘은 저쪽에 못 닿을 때의\n * 길이다.\n *\n * ── 쓰기 전에 계획을 보인다 ────────────────────────────────────────────────\n * `planImport` 가 새로/갱신/건너뜀과 **사람이 채울 비밀값**을 먼저 낸다. 부딪힘(두 연결이 같은 공장에\n * 조치를 내는 것)이 하나라도 있으면 **아무것도 쓰지 않는다** — 반만 적용된 연결 설정은 어느 쪽이\n * 참인지 아무도 모른다.\n */\nimport { Arg, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { TwinReference } from './twin-reference.js'\nimport { exportConnections, intakeCollisions, planImport, sayPlan, secretEnvName } from './connection-portability.js'\n\n/** 그 환경이 이 비밀값을 줄 수 있나 — **값은 읽지 않는다.** 이름의 존재만 본다. */\nfunction hasSecret(source: string, key: string): boolean {\n const name = secretEnvName(source, key)\n const v = process.env[name]\n return typeof v === 'string' && v.trim().length > 0\n}\n\n@Resolver()\nexport class ConnectionPortabilityResolver {\n /**\n * 지금 연결을 꾸러미로 낸다 — **비밀값은 안 나간다.**\n *\n * 이름 규칙으로 걸러서 **새 비밀 칸도 기본으로 안 나간다**(`SECRET_KEYS`). 나가지 않은 것은\n * 꾸러미가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.\n */\n /*\n * **읽기인데 쓰기와 같은 문을 쓴다.** 이 꾸러미는 연결 주소·도메인과 **비밀 칸의 이름**까지 낸다 —\n * 그 이름만으로도 그 배포가 무엇에 붙어 있는지 드러난다. 권한 가드가 「쓰기는 GATE 하나」로\n * 통일해 두었고, 이 문은 읽기지만 그 무게가 쓰기 쪽이다(ADR-0027).\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => ScalarObject, {\n description:\n 'Export this deployment’s twin connection declarations as a portable bundle. Secrets are never included — the bundle names them so the receiving side can ask a human.'\n })\n async twinConnectionBundle(\n @Ctx() context: ResolverContext,\n @Arg('note', { nullable: true, description: 'Free note carried in the bundle (why this export was made).' }) note?: string\n ): Promise<any> {\n const domain = context.state.domain\n const rows = await getRepository(TwinReference).find({ where: { domain: { id: domain.id } } })\n return exportConnections(rows as any, { at: new Date().toISOString(), domain: domain.subdomain ?? domain.name, ...(note ? { note } : {}) })\n }\n\n /**\n * 꾸러미를 받아 적용한다 — **`dryRun` 이 기본이다.**\n *\n * 쓰기가 기본이면 「무엇이 바뀌나」를 보기 위해 쓰게 된다. 계획을 먼저 보고 사람이 정하는 것이\n * 이 문의 요점이고, 그래서 적용은 명시해야 일어난다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n 'Apply a connection bundle. Returns the plan (new / updated / skipped, secrets a human must supply, collisions). Defaults to dryRun — pass apply:true to write.'\n })\n async importTwinConnections(\n @Arg('bundle', type => ScalarObject) bundle: any,\n @Ctx() context: ResolverContext,\n @Arg('apply', { nullable: true, description: 'Write the plan. Omit or false to only see what would happen.' }) apply?: boolean\n ): Promise<any> {\n const domainId = context.state.domain.id\n const repo = getRepository(TwinReference)\n const existing = await repo.find({ where: { domain: { id: domainId } } })\n\n const plan = planImport(bundle, existing as any, hasSecret)\n const said = sayPlan(plan)\n\n /*\n * 부딪힘이 있으면 **아무것도 쓰지 않는다.** 두 연결이 같은 공장에 조치를 내면 어느 쪽이 그 라인의\n * 주인인지 정할 수 없고, 반만 적용하면 그 물음이 조용히 남는다.\n */\n if (plan.collisions.length || plan.errors.length) {\n return { ok: false, applied: 0, plan, said, refused: plan.collisions.length ? 'intake-collision' : 'bundle-error' }\n }\n if (!apply) return { ok: true, applied: 0, plan, said, dryRun: true }\n\n let applied = 0\n for (const a of plan.actions) {\n /* 건너뛴 것은 이유가 계획에 남아 있다 — 여기서 다시 판정하지 않는다. */\n if (a.action === 'skip') continue\n\n const c = a.connection\n /*\n * **비밀값을 빈 값으로 안 채운다.** 꾸러미에 비밀값이 없고, `planImport` 가 그 이름을\n * `missingSecrets`·`needsHuman` 으로 이미 냈다. 여기서 그 칸을 `undefined` 로 쓰면 사람이\n * 화면에서 넣어 둔 값을 파일 하나가 지운다.\n *\n * 그래서 갱신은 **선언 칸만** 건드린다. 이미 있는 연결의 `connectionConfig` 는 꾸러미가 든\n * 것만 덮어쓰고 나머지 키는 그대로 남긴다.\n */\n const found = await repo.findOne({ where: { domain: { id: domainId }, source: c.source } })\n const declared = {\n ...(c.system ? { system: c.system } : {}),\n ...(c.siteName ? { siteName: c.siteName } : {}),\n adapterType: c.adapterType,\n ...(c.scope ? { scopeSpec: c.scope } : {})\n } as any\n\n if (found) {\n const merged = { ...((found as any).connectionConfig ?? {}), ...(c.config ?? {}) }\n await repo.update({ id: (found as any).id }, { ...declared, connectionConfig: merged } as any)\n } else {\n await repo.save(repo.create({ source: c.source, ...declared, connectionConfig: c.config ?? {}, domain: { id: domainId } as any }))\n }\n applied++\n }\n return { ok: true, applied, plan, said }\n }\n\n /**\n * 저쪽 배포에서 당겨와 적용한다 — **본래 원한 문.**\n *\n * 받는 쪽이 주체다. 저쪽의 `twinConnectionBundle` 을 불러 이 배포에 적용한다 — 위 두 문을 잇는\n * 얇은 층이고, 판정과 쓰기는 `importTwinConnections` 와 같은 규칙을 쓴다.\n *\n * **자격은 부르는 사람이 준다.** 이 서버가 저쪽 자격을 들고 있으면 그 통로가 상시로 열려 있게 되고,\n * 그것은 옮기는 일에 필요한 것보다 넓다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n 'Pull connection declarations from another deployment and apply them here. The caller supplies that deployment’s URL and token — this server does not keep them.'\n })\n async pullTwinConnectionsFrom(\n @Arg('endpoint', { description: 'That deployment’s GraphQL endpoint.' }) endpoint: string,\n @Ctx() context: ResolverContext,\n @Arg('token', { nullable: true, description: 'Bearer token for that deployment. Not stored.' }) token?: string,\n @Arg('apply', { nullable: true }) apply?: boolean\n ): Promise<any> {\n let bundle: any\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },\n body: JSON.stringify({ query: '{ twinConnectionBundle }' })\n })\n const json: any = await res.json()\n /* 저쪽이 오류를 냈으면 그 말을 그대로 올린다 — 여기서 다시 지으면 원인이 흐려진다. */\n if (json?.errors?.length) return { ok: false, applied: 0, refused: 'remote-error', remote: json.errors.map((e: any) => e?.message) }\n bundle = json?.data?.twinConnectionBundle\n } catch (e: any) {\n return { ok: false, applied: 0, refused: 'unreachable', remote: [String(e?.message ?? e)] }\n }\n if (!bundle) return { ok: false, applied: 0, refused: 'empty-bundle' }\n\n return this.importTwinConnections(bundle, context, apply)\n }\n}\n\n/** 이 리졸버가 다루는 부딪힘 판정 — 화면이 같은 함수를 쓸 수 있게 다시 내보낸다. */\nexport { intakeCollisions }\n"]}
1
+ {"version":3,"file":"connection-portability-resolver.js","sourceRoot":"","sources":["../../../server/service/reference/connection-portability-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **연결을 배포 사이로 옮기는 문 셋** — 몸통은 `connection-portability.ts` 의 순수 함수들이다.\n *\n * ── 무엇을 옮기나 (사용자와 맞춘 개념, 2026-09-05) ─────────────────────────\n * **옮기는 것은 「붙는 법」이지 「붙은 결과」가 아니다.**\n *\n * 옮긴다 연결 선언 — 주소 · 도메인 · 라인 크기 · 자재 이름공간\n * 안 옮긴다 마스터 원본에서 다시 읽으면 된다\n * 안 옮긴다 사실(저널) 원본이 다시 보내면 된다\n * 안 옮긴다 잰 값 그 서버에서 다시 세면 된다\n * 안 옮긴다 비밀값 그 환경의 것이지 셋업이 아니다\n *\n * **패키지는 정본이 아니라 운반 수단이다.** 정본은 언제나 그 배포의 DB 이고, 패키지는 쓰고 버린다.\n *\n * ── 주체는 받는 쪽이다 ─────────────────────────────────────────────────────\n * 운영이 개발에서 **당겨온다.** 개발이 운영으로 밀면 개발 환경에 운영 자격이 살고, 그 통로는 실수로도\n * 열린다. 그래서 `pullTwinConnectionsFrom` 이 본래 원하던 문이고, 나머지 둘은 저쪽에 못 닿을 때의\n * 길이다.\n *\n * ── 쓰기 전에 계획을 보인다 ────────────────────────────────────────────────\n * `planImport` 가 새로/갱신/건너뜀과 **사람이 채울 비밀값**을 먼저 낸다. 부딪힘(두 연결이 같은 공장에\n * 조치를 내는 것)이 하나라도 있으면 **아무것도 쓰지 않는다** — 반만 적용된 연결 설정은 어느 쪽이\n * 참인지 아무도 모른다.\n */\nimport { Arg, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'\nimport { ScalarObject, getRepository } from '@things-factory/shell'\n\nimport { TwinReference } from './twin-reference.js'\nimport { exportConnections, intakeCollisions, planImport, sayPlan, secretEnvName } from './connection-portability.js'\n\n/** 그 환경이 이 비밀값을 줄 수 있나 — **값은 읽지 않는다.** 이름의 존재만 본다. */\nfunction hasSecret(source: string, key: string): boolean {\n const name = secretEnvName(source, key)\n const v = process.env[name]\n return typeof v === 'string' && v.trim().length > 0\n}\n\n@Resolver()\nexport class ConnectionPortabilityResolver {\n /**\n * 지금 연결을 패키지로 낸다 — **비밀값은 안 나간다.**\n *\n * 이름 규칙으로 걸러서 **새 비밀 칸도 기본으로 안 나간다**(`SECRET_KEYS`). 나가지 않은 것은\n * 패키지가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.\n */\n /*\n * **읽기인데 쓰기와 같은 문을 쓴다.** 이 패키지는 연결 주소·도메인과 **비밀 칸의 이름**까지 낸다 —\n * 그 이름만으로도 그 배포가 무엇에 붙어 있는지 드러난다. 권한 가드가 「쓰기는 GATE 하나」로\n * 통일해 두었고, 이 문은 읽기지만 그 무게가 쓰기 쪽이다(ADR-0027).\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => ScalarObject, {\n description:\n 'Export this deployment’s twin connection declarations as a portable bundle. Secrets are never included — the bundle names them so the receiving side can ask a human.'\n })\n async twinConnectionBundle(\n @Ctx() context: ResolverContext,\n @Arg('note', { nullable: true, description: 'Free note carried in the bundle (why this export was made).' }) note?: string\n ): Promise<any> {\n const domain = context.state.domain\n const rows = await getRepository(TwinReference).find({ where: { domain: { id: domain.id } } })\n return exportConnections(rows as any, { at: new Date().toISOString(), domain: domain.subdomain ?? domain.name, ...(note ? { note } : {}) })\n }\n\n /**\n * 패키지를 받아 적용한다 — **`dryRun` 이 기본이다.**\n *\n * 쓰기가 기본이면 「무엇이 바뀌나」를 보기 위해 쓰게 된다. 계획을 먼저 보고 사람이 정하는 것이\n * 이 문의 요점이고, 그래서 적용은 명시해야 일어난다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n 'Apply a connection bundle. Returns the plan (new / updated / skipped, secrets a human must supply, collisions). Defaults to dryRun — pass apply:true to write.'\n })\n async importTwinConnections(\n @Arg('bundle', type => ScalarObject) bundle: any,\n @Ctx() context: ResolverContext,\n @Arg('apply', { nullable: true, description: 'Write the plan. Omit or false to only see what would happen.' }) apply?: boolean\n ): Promise<any> {\n const domainId = context.state.domain.id\n const repo = getRepository(TwinReference)\n const existing = await repo.find({ where: { domain: { id: domainId } } })\n\n const plan = planImport(bundle, existing as any, hasSecret)\n const said = sayPlan(plan)\n\n /*\n * 부딪힘이 있으면 **아무것도 쓰지 않는다.** 두 연결이 같은 공장에 조치를 내면 어느 쪽이 그 라인의\n * 주인인지 정할 수 없고, 반만 적용하면 그 물음이 조용히 남는다.\n */\n if (plan.collisions.length || plan.errors.length) {\n return { ok: false, applied: 0, plan, said, refused: plan.collisions.length ? 'intake-collision' : 'bundle-error' }\n }\n if (!apply) return { ok: true, applied: 0, plan, said, dryRun: true }\n\n let applied = 0\n for (const a of plan.actions) {\n /* 건너뛴 것은 이유가 계획에 남아 있다 — 여기서 다시 판정하지 않는다. */\n if (a.action === 'skip') continue\n\n const c = a.connection\n /*\n * **비밀값을 빈 값으로 안 채운다.** 패키지에 비밀값이 없고, `planImport` 가 그 이름을\n * `missingSecrets`·`needsHuman` 으로 이미 냈다. 여기서 그 칸을 `undefined` 로 쓰면 사람이\n * 화면에서 넣어 둔 값을 파일 하나가 지운다.\n *\n * 그래서 갱신은 **선언 칸만** 건드린다. 이미 있는 연결의 `connectionConfig` 는 패키지가 든\n * 것만 덮어쓰고 나머지 키는 그대로 남긴다.\n */\n const found = await repo.findOne({ where: { domain: { id: domainId }, source: c.source } })\n const declared = {\n ...(c.system ? { system: c.system } : {}),\n ...(c.siteName ? { siteName: c.siteName } : {}),\n adapterType: c.adapterType,\n ...(c.scope ? { scopeSpec: c.scope } : {})\n } as any\n\n if (found) {\n const merged = { ...((found as any).connectionConfig ?? {}), ...(c.config ?? {}) }\n await repo.update({ id: (found as any).id }, { ...declared, connectionConfig: merged } as any)\n } else {\n await repo.save(repo.create({ source: c.source, ...declared, connectionConfig: c.config ?? {}, domain: { id: domainId } as any }))\n }\n applied++\n }\n return { ok: true, applied, plan, said }\n }\n\n /**\n * 저쪽 배포에서 당겨와 적용한다 — **본래 원한 문.**\n *\n * 받는 쪽이 주체다. 저쪽의 `twinConnectionBundle` 을 불러 이 배포에 적용한다 — 위 두 문을 잇는\n * 얇은 층이고, 판정과 쓰기는 `importTwinConnections` 와 같은 규칙을 쓴다.\n *\n * **자격은 부르는 사람이 준다.** 이 서버가 저쪽 자격을 들고 있으면 그 통로가 상시로 열려 있게 되고,\n * 그것은 옮기는 일에 필요한 것보다 넓다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n 'Pull connection declarations from another deployment and apply them here. The caller supplies that deployment’s URL and token — this server does not keep them.'\n })\n async pullTwinConnectionsFrom(\n @Arg('endpoint', { description: 'That deployment’s GraphQL endpoint.' }) endpoint: string,\n @Ctx() context: ResolverContext,\n @Arg('token', { nullable: true, description: 'Bearer token for that deployment. Not stored.' }) token?: string,\n @Arg('apply', { nullable: true }) apply?: boolean\n ): Promise<any> {\n let bundle: any\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },\n body: JSON.stringify({ query: '{ twinConnectionBundle }' })\n })\n const json: any = await res.json()\n /* 저쪽이 오류를 냈으면 그 말을 그대로 올린다 — 여기서 다시 지으면 원인이 흐려진다. */\n if (json?.errors?.length) return { ok: false, applied: 0, refused: 'remote-error', remote: json.errors.map((e: any) => e?.message) }\n bundle = json?.data?.twinConnectionBundle\n } catch (e: any) {\n return { ok: false, applied: 0, refused: 'unreachable', remote: [String(e?.message ?? e)] }\n }\n if (!bundle) return { ok: false, applied: 0, refused: 'empty-bundle' }\n\n return this.importTwinConnections(bundle, context, apply)\n }\n}\n\n/** 이 리졸버가 다루는 부딪힘 판정 — 화면이 같은 함수를 쓸 수 있게 다시 내보낸다. */\nexport { intakeCollisions }\n"]}
@@ -103,7 +103,7 @@ export declare function sayPlan(plan: ImportPlan): string;
103
103
  /**
104
104
  * 비밀값이 사는 **환경 변수 이름** — `mes-miratek` · `hookSecret` → `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET`.
105
105
  *
106
- * 꾸러미에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이
106
+ * 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이
107
107
  * 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽
108
108
  * 판본입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.
109
109
  *
@@ -125,13 +125,13 @@ export declare function secretEnvName(source: string, key: string): string;
125
125
  * **「잘못된 공장이 실제로 만든다」** 입니다. 이것은 그 둘째 길입니다 — 사이트를 아는데 **틀린
126
126
  * 사이트를 압니다.**
127
127
  *
128
- * 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 꾸러미를 베껴 이름만 바꾸면 받는 자리가
128
+ * 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 패키지를 베껴 이름만 바꾸면 받는 자리가
129
129
  * 그대로 따라옵니다.
130
130
  */
131
131
  export declare function intakeCollisions(connections: readonly {
132
132
  source: string;
133
133
  adapterType?: string;
134
- /** 꾸러미·계획이 쓰는 이름. */
134
+ /** 패키지·계획이 쓰는 이름. */
135
135
  config?: Record<string, unknown> | null;
136
136
  /** **DB 행이 쓰는 이름** — `TwinReference` 의 칸이다. 둘 다 받는다(ADR-0053 ①). */
137
137
  connectionConfig?: Record<string, unknown> | null;
@@ -113,7 +113,7 @@ availableSecrets = () => false) {
113
113
  if (!b || typeof b !== 'object')
114
114
  return { actions: [], needsHuman: [], collisions: [], errors: ['들여올 것이 객체가 아닙니다'] };
115
115
  if (b.kind !== 'operato-twin/connections') {
116
- return { actions: [], needsHuman: [], collisions: [], errors: [`이 파일은 연결 꾸러미가 아닙니다(kind: ${JSON.stringify(b.kind ?? null)})`] };
116
+ return { actions: [], needsHuman: [], collisions: [], errors: [`이 파일은 연결 설정 파일가 아닙니다(kind: ${JSON.stringify(b.kind ?? null)})`] };
117
117
  }
118
118
  if (b.version !== 1)
119
119
  errors.push(`모르는 판입니다(version: ${JSON.stringify(b.version)}) — 읽을 수 있는 것만 읽습니다`);
@@ -179,7 +179,7 @@ function sayPlan(plan) {
179
179
  /**
180
180
  * 비밀값이 사는 **환경 변수 이름** — `mes-miratek` · `hookSecret` → `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET`.
181
181
  *
182
- * 꾸러미에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이
182
+ * 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이
183
183
  * 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽
184
184
  * 판본입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.
185
185
  *
@@ -208,7 +208,7 @@ function secretEnvName(source, key) {
208
208
  * **「잘못된 공장이 실제로 만든다」** 입니다. 이것은 그 둘째 길입니다 — 사이트를 아는데 **틀린
209
209
  * 사이트를 압니다.**
210
210
  *
211
- * 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 꾸러미를 베껴 이름만 바꾸면 받는 자리가
211
+ * 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 패키지를 베껴 이름만 바꾸면 받는 자리가
212
212
  * 그대로 따라옵니다.
213
213
  */
214
214
  function intakeCollisions(connections) {
@@ -217,7 +217,7 @@ function intakeCollisions(connections) {
217
217
  /*
218
218
  * **모양이 둘인 것을 여기서 안다** (ADR-0053 ①, 2026-09-16).
219
219
  *
220
- * 꾸러미는 `config`, `TwinReference` 행은 `connectionConfig` 다. 전에는 `config` 만 읽어서, 행을
220
+ * 패키지는 `config`, `TwinReference` 행은 `connectionConfig` 다. 전에는 `config` 만 읽어서, 행을
221
221
  * 그대로 넘기면 같은 충돌에도 `[]` 를 냈다 — 오류도 경고도 없이 **저장이 통과하고 가드는 선 채로
222
222
  * 아무 일도 안 한다.** 부르는 쪽마다 옮기게 두면 다음 소비처가 또 밟는다(이 함수는 화면이 쓰라고
223
223
  * 리졸버에서 다시 내보내진다).