@things-factory/headless-twin 10.1.205 → 10.1.208
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.
- package/dist-server/service/actuation/rule-runner.js +6 -6
- package/dist-server/service/actuation/rule-runner.js.map +1 -1
- package/dist-server/service/reference/connection-portability-resolver.d.ts +3 -3
- package/dist-server/service/reference/connection-portability-resolver.js +7 -7
- package/dist-server/service/reference/connection-portability-resolver.js.map +1 -1
- package/dist-server/service/reference/connection-portability.d.ts +3 -3
- package/dist-server/service/reference/connection-portability.js +4 -4
- package/dist-server/service/reference/connection-portability.js.map +1 -1
- package/package.json +6 -6
- package/server/service/actuation/rule-runner.ts +6 -6
- package/server/service/reference/connection-portability-resolver.ts +7 -7
- package/server/service/reference/connection-portability.ts +5 -5
- package/test/actuation-rule-runner.test.ts +3 -3
- package/test/connection-portability-doors.test.ts +6 -6
- package/test/save-intake-collision.test.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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":";;AA+EA,oCAqBC;AA7ED,gDAAyD;AAEzD,mEAAoE;AACpE,yDAA6G;AAgD7G;;;;GAIG;AACI,KAAK,UAAU,YAAY,CAAC,IAAoB;IACrD,MAAM,GAAG,GAAoB,EAAE,CAAA;IAE/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;QACrC,MAAM,MAAM,GAAkB,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,cAAc,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;QACrF,IAAI,CAAC;YACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;QACrD,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAA;YACvC,IAAA,kBAAS,EAAC,gBAAgB,IAAI,CAAC,UAAU,kBAAkB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;QAC7E,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAEhB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAC3C,IAAA,iBAAQ,EAAC,gBAAgB,IAAI,CAAC,UAAU,sBAAsB,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CACrF,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,IAAe,EAAE,IAAoB;IAC7D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,mDAAmD;IACnD,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,EAAE,CAAA;IAE5B;;;;;OAKG;IACH,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;QAC3B,OAAO;YACL,KAAK,EAAE;gBACL;oBACE,MAAM,EAAE,MAAM;oBACd,MAAM,EACJ,sDAAsD;wBACtD,oCAAoC;iBACvC;aACF;SACF,CAAA;IACH,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO;YACL,KAAK,EAAE;gBACL;oBACE,MAAM,EAAE,MAAM;oBACd,MAAM,EACJ,IAAI,KAAK,KAAK;wBACZ,CAAC,CAAC,sCAAsC;4BACtC,sCAAsC;wBACxC,CAAC,CAAC,mDAAmD;iBAC1D;aACF;SACF,CAAA;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IAC1C,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B;;;WAGG;QACH,OAAO;YACL,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,oDAAoD,EAAE,CAAC;SAC1F,CAAA;IACH,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;IAC9E,qDAAqD;IACrD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;IAExF,MAAM,OAAO,GAAG,IAAA,gCAAa,EAAC;QAC5B,KAAK;QACL,UAAU;QACV,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU;QACV,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE;QACnB,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzD,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IAChC,MAAM,cAAc,GAA2B,EAAE,CAAA;IACjD,IAAI,QAAQ,GAAG,CAAC,CAAA;IAEhB,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,IAAA,+BAAO,EAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE;gBACnD,EAAE,EAAE,YAAY,EAAE;gBAClB,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,IAAI,EAAE,CAAC,CAAC,WAAW;gBACnB,iDAAiD;gBACjD,MAAM,EAAE,MAAM;gBACd,UAAU,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE;gBAChD,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,QAAQ,EAAE,CAAC,CAAC,WAAW;gBACvB,OAAO,EAAE,CAAC,CAAC,MAAM;aAClB,CAAC,CAAA;YACF,QAAQ,EAAE,CAAA;YACV,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;QAChE,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB;;;eAGG;YACH,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,CAAA;QAC/F,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AACzG,CAAC;AAED,wDAAwD;AACxD,SAAS,YAAY;IACnB,OAAO,UAAU,CAAC,MAAM,CAAC,UAAU,EAAE,CAAA;AACvC,CAAC","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
|
-
*
|
|
11
|
+
* 패키지를 받아 적용한다 — **`dryRun` 이 기본이다.**
|
|
12
12
|
*
|
|
13
13
|
* 쓰기가 기본이면 「무엇이 바뀌나」를 보기 위해 쓰게 된다. 계획을 먼저 보고 사람이 정하는 것이
|
|
14
14
|
* 이 문의 요점이고, 그래서 적용은 명시해야 일어난다.
|
|
@@ -14,7 +14,7 @@ const tslib_1 = require("tslib");
|
|
|
14
14
|
* 안 옮긴다 잰 값 그 서버에서 다시 세면 된다
|
|
15
15
|
* 안 옮긴다 비밀값 그 환경의 것이지 셋업이 아니다
|
|
16
16
|
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* **비밀값을 빈 값으로 안 채운다.**
|
|
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: [`이 파일은 연결
|
|
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
|
-
*
|
|
220
|
+
* 패키지는 `config`, `TwinReference` 행은 `connectionConfig` 다. 전에는 `config` 만 읽어서, 행을
|
|
221
221
|
* 그대로 넘기면 같은 충돌에도 `[]` 를 냈다 — 오류도 경고도 없이 **저장이 통과하고 가드는 선 채로
|
|
222
222
|
* 아무 일도 안 한다.** 부르는 쪽마다 옮기게 두면 다음 소비처가 또 밟는다(이 함수는 화면이 쓰라고
|
|
223
223
|
* 리졸버에서 다시 내보내진다).
|
|
@@ -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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/headless-twin",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.208",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist-server/index.js",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@operato/ops-contract": "^0.9.40",
|
|
43
43
|
"@operato/twin-kernel": "^0.11.29",
|
|
44
|
-
"@things-factory/auth-base": "^10.1.
|
|
45
|
-
"@things-factory/cache-service": "^10.1.
|
|
44
|
+
"@things-factory/auth-base": "^10.1.208",
|
|
45
|
+
"@things-factory/cache-service": "^10.1.208",
|
|
46
46
|
"@things-factory/env": "^10.1.149",
|
|
47
|
-
"@things-factory/ingest": "^10.1.
|
|
48
|
-
"@things-factory/shell": "^10.1.
|
|
47
|
+
"@things-factory/ingest": "^10.1.208",
|
|
48
|
+
"@things-factory/shell": "^10.1.208"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "ffc1ceaa9471b9f4a9b86f46c3021f2fed72d17d"
|
|
51
51
|
}
|
|
@@ -117,8 +117,8 @@ async function runOneTwin(twin: OwnedTwin, deps: RuleRunnerDeps): Promise<Partia
|
|
|
117
117
|
{
|
|
118
118
|
ruleId: '(전체)',
|
|
119
119
|
reason:
|
|
120
|
-
'
|
|
121
|
-
'CommandStore.openCauses
|
|
120
|
+
'기존 조치 요청의 근거를 조회할 수 없어 규칙을 평가하지 않았습니다. 중복 요청을 방지하려면 ' +
|
|
121
|
+
'CommandStore.openCauses를 구현해야 합니다.'
|
|
122
122
|
}
|
|
123
123
|
]
|
|
124
124
|
}
|
|
@@ -146,9 +146,9 @@ async function runOneTwin(twin: OwnedTwin, deps: RuleRunnerDeps): Promise<Partia
|
|
|
146
146
|
ruleId: '(전체)',
|
|
147
147
|
reason:
|
|
148
148
|
mode === 'sim'
|
|
149
|
-
? '이 트윈은
|
|
150
|
-
'
|
|
151
|
-
: '
|
|
149
|
+
? '이 트윈은 시뮬레이션 모드이므로 현장 지시를 생성하지 않습니다. ' +
|
|
150
|
+
'시뮬레이션·예측을 근거로 한 현장 지시는 현재 지원하지 않습니다.'
|
|
151
|
+
: '트윈의 실행 모드를 확인할 수 없어 실제 관측에 근거한 현장 지시를 생성할 수 없습니다.'
|
|
152
152
|
}
|
|
153
153
|
]
|
|
154
154
|
}
|
|
@@ -161,7 +161,7 @@ async function runOneTwin(twin: OwnedTwin, deps: RuleRunnerDeps): Promise<Partia
|
|
|
161
161
|
* 「조건이 하나도 없다」가 되고, 규칙이 도는 줄로 보인다.
|
|
162
162
|
*/
|
|
163
163
|
return {
|
|
164
|
-
skips: [{ ruleId: '(전체)', reason: '
|
|
164
|
+
skips: [{ ruleId: '(전체)', reason: '트윈의 상태를 조회할 수 없습니다. 실행이 중지되었거나 아직 시작되지 않았을 수 있습니다.' }]
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 안 옮긴다 잰 값 그 서버에서 다시 세면 된다
|
|
11
11
|
* 안 옮긴다 비밀값 그 환경의 것이지 셋업이 아니다
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* **패키지는 정본이 아니라 운반 수단이다.** 정본은 언제나 그 배포의 DB 이고, 패키지는 쓰고 버린다.
|
|
14
14
|
*
|
|
15
15
|
* ── 주체는 받는 쪽이다 ─────────────────────────────────────────────────────
|
|
16
16
|
* 운영이 개발에서 **당겨온다.** 개발이 운영으로 밀면 개발 환경에 운영 자격이 살고, 그 통로는 실수로도
|
|
@@ -38,13 +38,13 @@ function hasSecret(source: string, key: string): boolean {
|
|
|
38
38
|
@Resolver()
|
|
39
39
|
export class ConnectionPortabilityResolver {
|
|
40
40
|
/**
|
|
41
|
-
* 지금 연결을
|
|
41
|
+
* 지금 연결을 패키지로 낸다 — **비밀값은 안 나간다.**
|
|
42
42
|
*
|
|
43
43
|
* 이름 규칙으로 걸러서 **새 비밀 칸도 기본으로 안 나간다**(`SECRET_KEYS`). 나가지 않은 것은
|
|
44
|
-
*
|
|
44
|
+
* 패키지가 이름으로 말하고, 받는 쪽이 그 이름을 사람에게 보인다.
|
|
45
45
|
*/
|
|
46
46
|
/*
|
|
47
|
-
* **읽기인데 쓰기와 같은 문을 쓴다.** 이
|
|
47
|
+
* **읽기인데 쓰기와 같은 문을 쓴다.** 이 패키지는 연결 주소·도메인과 **비밀 칸의 이름**까지 낸다 —
|
|
48
48
|
* 그 이름만으로도 그 배포가 무엇에 붙어 있는지 드러난다. 권한 가드가 「쓰기는 GATE 하나」로
|
|
49
49
|
* 통일해 두었고, 이 문은 읽기지만 그 무게가 쓰기 쪽이다(ADR-0027).
|
|
50
50
|
*/
|
|
@@ -63,7 +63,7 @@ export class ConnectionPortabilityResolver {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* 패키지를 받아 적용한다 — **`dryRun` 이 기본이다.**
|
|
67
67
|
*
|
|
68
68
|
* 쓰기가 기본이면 「무엇이 바뀌나」를 보기 위해 쓰게 된다. 계획을 먼저 보고 사람이 정하는 것이
|
|
69
69
|
* 이 문의 요점이고, 그래서 적용은 명시해야 일어난다.
|
|
@@ -101,11 +101,11 @@ export class ConnectionPortabilityResolver {
|
|
|
101
101
|
|
|
102
102
|
const c = a.connection
|
|
103
103
|
/*
|
|
104
|
-
* **비밀값을 빈 값으로 안 채운다.**
|
|
104
|
+
* **비밀값을 빈 값으로 안 채운다.** 패키지에 비밀값이 없고, `planImport` 가 그 이름을
|
|
105
105
|
* `missingSecrets`·`needsHuman` 으로 이미 냈다. 여기서 그 칸을 `undefined` 로 쓰면 사람이
|
|
106
106
|
* 화면에서 넣어 둔 값을 파일 하나가 지운다.
|
|
107
107
|
*
|
|
108
|
-
* 그래서 갱신은 **선언 칸만** 건드린다. 이미 있는 연결의 `connectionConfig` 는
|
|
108
|
+
* 그래서 갱신은 **선언 칸만** 건드린다. 이미 있는 연결의 `connectionConfig` 는 패키지가 든
|
|
109
109
|
* 것만 덮어쓰고 나머지 키는 그대로 남긴다.
|
|
110
110
|
*/
|
|
111
111
|
const found = await repo.findOne({ where: { domain: { id: domainId }, source: c.source } })
|
|
@@ -160,7 +160,7 @@ export function planImport(
|
|
|
160
160
|
const b = bundle as ConnectionBundle
|
|
161
161
|
if (!b || typeof b !== 'object') return { actions: [], needsHuman: [], collisions: [], errors: ['들여올 것이 객체가 아닙니다'] }
|
|
162
162
|
if (b.kind !== 'operato-twin/connections') {
|
|
163
|
-
return { actions: [], needsHuman: [], collisions: [], errors: [`이 파일은 연결
|
|
163
|
+
return { actions: [], needsHuman: [], collisions: [], errors: [`이 파일은 연결 설정 파일가 아닙니다(kind: ${JSON.stringify((b as any).kind ?? null)})`] }
|
|
164
164
|
}
|
|
165
165
|
if (b.version !== 1) errors.push(`모르는 판입니다(version: ${JSON.stringify((b as any).version)}) — 읽을 수 있는 것만 읽습니다`)
|
|
166
166
|
if (!Array.isArray(b.connections)) return { actions: [], needsHuman: [], collisions: [], errors: [...errors, 'connections 가 목록이 아닙니다'] }
|
|
@@ -231,7 +231,7 @@ export function sayPlan(plan: ImportPlan): string {
|
|
|
231
231
|
/**
|
|
232
232
|
* 비밀값이 사는 **환경 변수 이름** — `mes-miratek` · `hookSecret` → `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET`.
|
|
233
233
|
*
|
|
234
|
-
*
|
|
234
|
+
* 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이
|
|
235
235
|
* 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽
|
|
236
236
|
* 판본입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다.
|
|
237
237
|
*
|
|
@@ -262,14 +262,14 @@ export function secretEnvName(source: string, key: string): string {
|
|
|
262
262
|
* **「잘못된 공장이 실제로 만든다」** 입니다. 이것은 그 둘째 길입니다 — 사이트를 아는데 **틀린
|
|
263
263
|
* 사이트를 압니다.**
|
|
264
264
|
*
|
|
265
|
-
* 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다:
|
|
265
|
+
* 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 패키지를 베껴 이름만 바꾸면 받는 자리가
|
|
266
266
|
* 그대로 따라옵니다.
|
|
267
267
|
*/
|
|
268
268
|
export function intakeCollisions(
|
|
269
269
|
connections: readonly {
|
|
270
270
|
source: string
|
|
271
271
|
adapterType?: string
|
|
272
|
-
/**
|
|
272
|
+
/** 패키지·계획이 쓰는 이름. */
|
|
273
273
|
config?: Record<string, unknown> | null
|
|
274
274
|
/** **DB 행이 쓰는 이름** — `TwinReference` 의 칸이다. 둘 다 받는다(ADR-0053 ①). */
|
|
275
275
|
connectionConfig?: Record<string, unknown> | null
|
|
@@ -280,7 +280,7 @@ export function intakeCollisions(
|
|
|
280
280
|
/*
|
|
281
281
|
* **모양이 둘인 것을 여기서 안다** (ADR-0053 ①, 2026-09-16).
|
|
282
282
|
*
|
|
283
|
-
*
|
|
283
|
+
* 패키지는 `config`, `TwinReference` 행은 `connectionConfig` 다. 전에는 `config` 만 읽어서, 행을
|
|
284
284
|
* 그대로 넘기면 같은 충돌에도 `[]` 를 냈다 — 오류도 경고도 없이 **저장이 통과하고 가드는 선 채로
|
|
285
285
|
* 아무 일도 안 한다.** 부르는 쪽마다 옮기게 두면 다음 소비처가 또 밟는다(이 함수는 화면이 쓰라고
|
|
286
286
|
* 리졸버에서 다시 내보내진다).
|