@things-factory/headless-twin 10.0.10 → 10.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +25 -2
  2. package/dist-server/engine/canonical-ingest.js +48 -10
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/declared-stimulus.d.ts +43 -0
  5. package/dist-server/engine/declared-stimulus.js +57 -0
  6. package/dist-server/engine/declared-stimulus.js.map +1 -0
  7. package/dist-server/engine/index.d.ts +2 -0
  8. package/dist-server/engine/index.js +2 -0
  9. package/dist-server/engine/index.js.map +1 -1
  10. package/dist-server/engine/kpi-fold.d.ts +12 -0
  11. package/dist-server/engine/kpi-fold.js +21 -1
  12. package/dist-server/engine/kpi-fold.js.map +1 -1
  13. package/dist-server/engine/kpi-query.d.ts +3 -3
  14. package/dist-server/engine/kpi-query.js +4 -4
  15. package/dist-server/engine/kpi-query.js.map +1 -1
  16. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  17. package/dist-server/engine/live-feed-registry.js +1 -1
  18. package/dist-server/engine/live-feed-registry.js.map +1 -1
  19. package/dist-server/engine/local-declarations.d.ts +3 -6
  20. package/dist-server/engine/local-declarations.js +97 -16
  21. package/dist-server/engine/local-declarations.js.map +1 -1
  22. package/dist-server/engine/measured-yield.d.ts +42 -0
  23. package/dist-server/engine/measured-yield.js +75 -0
  24. package/dist-server/engine/measured-yield.js.map +1 -0
  25. package/dist-server/engine/operation-basis.d.ts +16 -0
  26. package/dist-server/engine/operation-basis.js +20 -2
  27. package/dist-server/engine/operation-basis.js.map +1 -1
  28. package/dist-server/engine/property-effects.js +17 -0
  29. package/dist-server/engine/property-effects.js.map +1 -1
  30. package/dist-server/engine/restart-policy.d.ts +13 -0
  31. package/dist-server/engine/restart-policy.js +52 -0
  32. package/dist-server/engine/restart-policy.js.map +1 -0
  33. package/dist-server/engine/spec-coverage.d.ts +8 -0
  34. package/dist-server/engine/spec-coverage.js +3 -1
  35. package/dist-server/engine/spec-coverage.js.map +1 -1
  36. package/dist-server/engine/twin-engine.d.ts +48 -15
  37. package/dist-server/engine/twin-engine.js +179 -39
  38. package/dist-server/engine/twin-engine.js.map +1 -1
  39. package/dist-server/index.js +1 -1
  40. package/dist-server/index.js.map +1 -1
  41. package/dist-server/service/index.d.ts +1 -1
  42. package/dist-server/service/reference/control-routing.d.ts +14 -0
  43. package/dist-server/service/reference/control-routing.js +64 -0
  44. package/dist-server/service/reference/control-routing.js.map +1 -0
  45. package/dist-server/service/reference/index.d.ts +1 -0
  46. package/dist-server/service/reference/index.js +2 -0
  47. package/dist-server/service/reference/index.js.map +1 -1
  48. package/dist-server/service/reference/reference-adapter.d.ts +67 -0
  49. package/dist-server/service/reference/reference-adapter.js +18 -0
  50. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  51. package/dist-server/service/reference/reference-live.js +3 -3
  52. package/dist-server/service/reference/reference-live.js.map +1 -1
  53. package/dist-server/service/reference/reference-resolver.d.ts +3 -3
  54. package/dist-server/service/reference/reference-resolver.js +35 -18
  55. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  56. package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
  57. package/dist-server/service/twin-audit/command-audit.js +15 -1
  58. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  59. package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
  60. package/dist-server/service/twin-audit/twin-audit-event.js +28 -2
  61. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  62. package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
  63. package/dist-server/service/twin-control/twin-control-mutation.js +85 -34
  64. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  65. package/dist-server/service/twin-event/twin-event.js +1 -1
  66. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  67. package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
  68. package/dist-server/service/twin-instance/twin-instance.js +2 -2
  69. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  70. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +2 -1
  71. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  72. package/dist-server/service/twin-model/twin-equipment.js +1 -1
  73. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  74. package/dist-server/service/twin-model/twin-location.js +1 -1
  75. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  76. package/dist-server/service/twin-model/twin-model-query.js +1 -1
  77. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  78. package/dist-server/service/twin-model/twin-operation.js +1 -1
  79. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  80. package/package.json +3 -3
  81. package/server/engine/canonical-ingest.ts +78 -14
  82. package/server/engine/declared-stimulus.ts +66 -0
  83. package/server/engine/index.ts +2 -0
  84. package/server/engine/kpi-fold.ts +29 -1
  85. package/server/engine/kpi-query.ts +8 -8
  86. package/server/engine/live-feed-registry.ts +2 -2
  87. package/server/engine/local-declarations.ts +97 -19
  88. package/server/engine/measured-yield.ts +89 -0
  89. package/server/engine/operation-basis.ts +33 -2
  90. package/server/engine/property-effects.ts +17 -0
  91. package/server/engine/restart-policy.ts +55 -0
  92. package/server/engine/spec-coverage.ts +23 -3
  93. package/server/engine/twin-engine.ts +201 -42
  94. package/server/index.ts +1 -1
  95. package/server/service/reference/control-routing.ts +62 -0
  96. package/server/service/reference/index.ts +1 -0
  97. package/server/service/reference/reference-adapter.ts +78 -0
  98. package/server/service/reference/reference-live.ts +3 -3
  99. package/server/service/reference/reference-resolver.ts +46 -7
  100. package/server/service/twin-audit/command-audit.ts +34 -1
  101. package/server/service/twin-audit/twin-audit-event.ts +50 -4
  102. package/server/service/twin-control/twin-control-mutation.ts +75 -28
  103. package/server/service/twin-event/twin-event.ts +10 -2
  104. package/server/service/twin-instance/twin-instance.ts +16 -10
  105. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +2 -1
  106. package/server/service/twin-model/twin-equipment.ts +8 -1
  107. package/server/service/twin-model/twin-location.ts +8 -1
  108. package/server/service/twin-model/twin-model-query.ts +1 -1
  109. package/server/service/twin-model/twin-operation.ts +8 -1
  110. package/test/adopt-structure-live.test.ts +7 -7
  111. package/test/boot-resume.test.ts +3 -3
  112. package/test/column-type-portability.test.ts +122 -0
  113. package/test/control-capability.test.ts +103 -0
  114. package/test/declared-stimulus.test.ts +88 -0
  115. package/test/ingest-running-guard.test.ts +5 -5
  116. package/test/instance-cache-lifecycle.test.ts +1 -1
  117. package/test/kpi-baseline-db.test.ts +1 -1
  118. package/test/kpi-query-bench.test.ts +1 -1
  119. package/test/lineage-survives-restart.test.ts +3 -3
  120. package/test/live-feed-registry.test.ts +6 -6
  121. package/test/local-declarations.test.ts +108 -1
  122. package/test/measured-yield.test.ts +90 -0
  123. package/test/operation-basis.test.ts +28 -1
  124. package/test/operational-vocabulary.test.ts +108 -0
  125. package/test/operations-capability-db.test.ts +4 -4
  126. package/test/project-structure-db.test.ts +1 -1
  127. package/test/projection-reaches-screen.test.ts +1 -1
  128. package/test/property-effects.test.ts +28 -0
  129. package/test/restart-policy.test.ts +111 -0
  130. package/test/resync-origin-site.test.ts +7 -1
  131. package/test/source-outcome-audit.test.ts +104 -0
  132. package/test/structure-revision-db.test.ts +27 -27
  133. package/test/tenant-registry-db.test.ts +2 -2
  134. package/test/twin-model-item-db.test.ts +3 -3
  135. package/test/twin-model-tree-db.test.ts +7 -7
  136. package/test/twin-origin-resync.test.ts +4 -4
  137. package/test/yield-loop.test.ts +144 -0
  138. package/tsconfig.tsbuildinfo +1 -1
@@ -13,7 +13,7 @@ const reference_live_js_1 = require("./service/reference/reference-live.js");
13
13
  /*
14
14
  * 모듈 부팅 — **도는 트윈을 되살린다.**
15
15
  *
16
- * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `realityMode` 그대로 되살리고
16
+ * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `restartPolicy` 그대로 되살리고
17
17
  * (`bootstrap`), ② 레퍼런스 계층이 미러 트윈의 커넥터 피드를 다시 붙인다. 순서가 뒤집히면 피드가
18
18
  * 아직 없는 커널에 계측을 붓게 된다.
19
19
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,4DAAiC;AACjC,6DAAkC;AAClC,qEAAqE;AACrE,kDAAkD;AAAzC,sGAAA,UAAU,OAAA;AAEnB,uBAAoB;AAEpB,gDAA8C;AAC9C,6EAAgF;AAEhF;;;;;;;;GAQG;AACH,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,IAAI,CAAC;QACH,MAAM,qBAAU,CAAC,SAAS,EAAE,CAAA;QAC5B,MAAM,IAAA,4CAAwB,GAAE,CAAA;QAChC,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAA;IAChD,CAAC;IAAC,OAAO,EAAE,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,EAAE,CAAC,CAAA;IAC1D,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["export * from './engine/index.js'\nexport * from './service/index.js'\n/* 스키마 이력 — 프레임워크가 `migrations` export 를 모아 실행한다(`run migration`). */\nexport { migrations } from './migrations/index.js'\n\nimport './routes.js'\n\nimport { TwinEngine } from './engine/index.js'\nimport { resumeReferenceLiveFeeds } from './service/reference/reference-live.js'\n\n/*\n * 모듈 부팅 — **도는 트윈을 되살린다.**\n *\n * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `realityMode` 그대로 되살리고\n * (`bootstrap`), ② 레퍼런스 계층이 미러 트윈의 커넥터 피드를 다시 붙인다. 순서가 뒤집히면 피드가\n * 아직 없는 커널에 계측을 붓게 된다.\n *\n * 예전에는 ①도 ②도 없었다 — 재기동 한 번에 등록부만 「도는 중」이라 말하는 트윈이 남았다.\n */\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n try {\n await TwinEngine.bootstrap()\n await resumeReferenceLiveFeeds()\n console.log('Headless Twin host has started.')\n } catch (ex) {\n console.error('Headless Twin host failed to start.', ex)\n }\n})\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,4DAAiC;AACjC,6DAAkC;AAClC,qEAAqE;AACrE,kDAAkD;AAAzC,sGAAA,UAAU,OAAA;AAEnB,uBAAoB;AAEpB,gDAA8C;AAC9C,6EAAgF;AAEhF;;;;;;;;GAQG;AACH,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,IAAI,CAAC;QACH,MAAM,qBAAU,CAAC,SAAS,EAAE,CAAA;QAC5B,MAAM,IAAA,4CAAwB,GAAE,CAAA;QAChC,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAA;IAChD,CAAC;IAAC,OAAO,EAAE,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,EAAE,CAAC,CAAA;IAC1D,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["export * from './engine/index.js'\nexport * from './service/index.js'\n/* 스키마 이력 — 프레임워크가 `migrations` export 를 모아 실행한다(`run migration`). */\nexport { migrations } from './migrations/index.js'\n\nimport './routes.js'\n\nimport { TwinEngine } from './engine/index.js'\nimport { resumeReferenceLiveFeeds } from './service/reference/reference-live.js'\n\n/*\n * 모듈 부팅 — **도는 트윈을 되살린다.**\n *\n * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `restartPolicy` 그대로 되살리고\n * (`bootstrap`), ② 레퍼런스 계층이 미러 트윈의 커넥터 피드를 다시 붙인다. 순서가 뒤집히면 피드가\n * 아직 없는 커널에 계측을 붓게 된다.\n *\n * 예전에는 ①도 ②도 없었다 — 재기동 한 번에 등록부만 「도는 중」이라 말하는 트윈이 남았다.\n */\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n try {\n await TwinEngine.bootstrap()\n await resumeReferenceLiveFeeds()\n console.log('Headless Twin host has started.')\n } catch (ex) {\n console.error('Headless Twin host failed to start.', ex)\n }\n})\n"]}
@@ -14,7 +14,7 @@ export * from './reference/reference-live.js';
14
14
  export * from './reference/twin-reference.js';
15
15
  export * from './reference/template-registry.js';
16
16
  export * from './twin-lifecycle/domain-catalog.js';
17
- export declare const entities: (typeof import("./twin-event/twin-event.js").TwinEvent | typeof import("./twin-instance/twin-instance.js").TwinInstance | typeof import("./twin-structure/twin-structure.js").TwinStructure | typeof import("./twin-space/twin-space.js").TwinSpace | typeof import("./twin-space/twin-space-representation.js").TwinSpaceRepresentation | typeof import("./twin-space/twin-space-area.js").TwinSpaceArea | typeof import("./twin-space/twin-area.js").TwinArea | typeof import("./twin-model/twin-location.js").TwinLocation | typeof import("./twin-model/twin-equipment.js").TwinEquipment | typeof import("./twin-model/twin-operation.js").TwinOperation | typeof import("./twin-target/twin-target.js").TwinTarget | typeof import("./twin-audit/twin-audit-event.js").TwinAuditEvent | typeof import("./reference/twin-reference.js").TwinReference)[];
17
+ export declare const entities: (typeof import("./twin-event/twin-event.js").TwinEvent | typeof import("./twin-instance/twin-instance.js").TwinInstance | typeof import("./reference/twin-reference.js").TwinReference | typeof import("./twin-structure/twin-structure.js").TwinStructure | typeof import("./twin-space/twin-space.js").TwinSpace | typeof import("./twin-space/twin-space-representation.js").TwinSpaceRepresentation | typeof import("./twin-space/twin-space-area.js").TwinSpaceArea | typeof import("./twin-space/twin-area.js").TwinArea | typeof import("./twin-model/twin-location.js").TwinLocation | typeof import("./twin-model/twin-equipment.js").TwinEquipment | typeof import("./twin-model/twin-operation.js").TwinOperation | typeof import("./twin-target/twin-target.js").TwinTarget | typeof import("./twin-audit/twin-audit-event.js").TwinAuditEvent)[];
18
18
  export declare const subscribers: any[];
19
19
  export declare const schema: {
20
20
  typeDefs: {};
@@ -0,0 +1,14 @@
1
+ import { type ControlCommand, type ControlResult } from './reference-adapter.js';
2
+ /**
3
+ * 이 트윈의 원본에 구동 명령을 보낸다 — **원본이 없거나 능력을 선언하지 않으면 거절한다.**
4
+ *
5
+ * 거절을 조용히 하지 않는다: 사유 코드를 돌려주고 화면이 그것을 사람 말로 옮긴다. 「아무 일도 일어나지
6
+ * 않았다」가 가장 비싼 답이다.
7
+ */
8
+ export declare function routeControl(domainId: string, instanceId: string, command: ControlCommand): Promise<ControlResult>;
9
+ /** 이 트윈의 원본이 선언한 능력 — 화면이 「무엇을 할 수 있나」를 먼저 알 수 있게. */
10
+ export declare function controlCapabilityOf(domainId: string, instanceId: string): Promise<{
11
+ source?: string;
12
+ adapterType?: string;
13
+ capabilities: string[];
14
+ }>;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routeControl = routeControl;
4
+ exports.controlCapabilityOf = controlCapabilityOf;
5
+ /*
6
+ * **구동은 원본을 지난다** — 트윈을 우회하지 않는다 (ADR-0029 §8, 2026-08-19).
7
+ *
8
+ * ── 무엇이 뚫려 있었나 ──────────────────────────────────────────────────────
9
+ * `controlTwinScenario` 가 `TwinEngine.runtime(...).scenario` 를 **직접** 잡았다. 그래서 시뮬만 할 수 있는
10
+ * 일(자극·배속·정지)이 계약 밖의 지름길로 들어왔고, 실 시스템에는 그 자리가 아예 없었다 — ADR-0029 의
11
+ * 판정 문장에 걸리는 상태였다.
12
+ *
13
+ * 이제 길은 하나다: **트윈 → 그 트윈의 원본 → 어댑터가 선언한 `control` 능력.** 선언하지 않은 원본에는
14
+ * 문이 열리지 않고, 그 사실을 코드로 말한다(`not-declared`). 실 WMS 가 정지되지 않는 것은 결손이 아니다.
15
+ *
16
+ * ── 전송은 여기서 묻지 않는다 (§7) ──────────────────────────────────────────
17
+ * 인프로세스 시뮬레이터는 같은 계약을 인프로세스로 이행한다. 나중에 그것이 다른 프로세스로 나가도 이 경로는
18
+ * 바뀌지 않는다 — 「계약을 지나는가」만 보고 「네트워크를 타는가」는 묻지 않는다.
19
+ */
20
+ const shell_1 = require("@things-factory/shell");
21
+ const twin_instance_js_1 = require("../twin-instance/twin-instance.js");
22
+ const twin_reference_js_1 = require("./twin-reference.js");
23
+ const reference_adapter_js_1 = require("./reference-adapter.js");
24
+ /**
25
+ * 이 트윈의 원본에 구동 명령을 보낸다 — **원본이 없거나 능력을 선언하지 않으면 거절한다.**
26
+ *
27
+ * 거절을 조용히 하지 않는다: 사유 코드를 돌려주고 화면이 그것을 사람 말로 옮긴다. 「아무 일도 일어나지
28
+ * 않았다」가 가장 비싼 답이다.
29
+ */
30
+ async function routeControl(domainId, instanceId, command) {
31
+ const inst = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } });
32
+ if (!inst)
33
+ return { ok: false, code: 'unknown-twin', detail: instanceId };
34
+ const ref = await (0, shell_1.getRepository)(twin_reference_js_1.TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } });
35
+ if (!ref) {
36
+ /* 원본을 모르는 트윈 — 구동할 대상이 없다. 트윈을 직접 흔드는 길은 두지 않는다(그것이 지름길이었다). */
37
+ return { ok: false, code: 'no-source', detail: `twin "${instanceId}" does not remember a source to drive` };
38
+ }
39
+ const adapter = (0, reference_adapter_js_1.getAdapter)(ref.adapterType);
40
+ if (!adapter)
41
+ return { ok: false, code: 'no-adapter', detail: ref.adapterType };
42
+ if (!(0, reference_adapter_js_1.capabilitiesOf)(adapter).includes('control')) {
43
+ return {
44
+ ok: false,
45
+ code: 'not-declared',
46
+ detail: `source "${ref.source}" (${ref.adapterType}) does not declare the control capability — a real system is not paused or seeded, and that is a fact rather than a gap`
47
+ };
48
+ }
49
+ /*
50
+ * 사이트를 함께 넘긴다 — 원본 하나가 여러 사이트를 낼 수 있다(`discoverSites`). 우리가 아는 것은
51
+ * 이 트윈이 어느 사이트에서 왔는지이고(`origin.siteId`), 모르면 넘기지 않는다(지어내지 않는다).
52
+ */
53
+ const siteId = inst.origin?.siteId;
54
+ const site = siteId ? { siteId } : undefined;
55
+ return adapter.control(ref.connectionConfig ?? {}, site, { ...command, instanceId, domainId });
56
+ }
57
+ /** 이 트윈의 원본이 선언한 능력 — 화면이 「무엇을 할 수 있나」를 먼저 알 수 있게. */
58
+ async function controlCapabilityOf(domainId, instanceId) {
59
+ const ref = await (0, shell_1.getRepository)(twin_reference_js_1.TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } });
60
+ if (!ref)
61
+ return { capabilities: [] };
62
+ return { source: ref.source, adapterType: ref.adapterType, capabilities: (0, reference_adapter_js_1.capabilitiesOf)((0, reference_adapter_js_1.getAdapter)(ref.adapterType)) };
63
+ }
64
+ //# sourceMappingURL=control-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control-routing.js","sourceRoot":"","sources":["../../../server/service/reference/control-routing.ts"],"names":[],"mappings":";;AA2BA,oCA2BC;AAGD,kDAIC;AA7DD;;;;;;;;;;;;;;GAcG;AACH,iDAAqD;AAErD,wEAAgE;AAChE,2DAAmD;AACnD,iEAAiI;AAEjI;;;;;GAKG;AACI,KAAK,UAAU,YAAY,CAAC,QAAgB,EAAE,UAAkB,EAAE,OAAuB;IAC9F,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IAC3G,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;IAEzE,MAAM,GAAG,GAAG,MAAM,IAAA,qBAAa,EAAC,iCAAa,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IACnH,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,gEAAgE;QAChE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,UAAU,uCAAuC,EAAE,CAAA;IAC7G,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,iCAAU,EAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,CAAC,WAAW,EAAE,CAAA;IAC/E,IAAI,CAAC,IAAA,qCAAc,EAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACjD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,WAAW,GAAG,CAAC,MAAM,MAAM,GAAG,CAAC,WAAW,yHAAyH;SAC5K,CAAA;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,MAAM,GAAI,IAAI,CAAC,MAAc,EAAE,MAAM,CAAA;IAC3C,MAAM,IAAI,GAA+B,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IACxE,OAAO,OAAO,CAAC,OAAQ,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAA;AACjG,CAAC;AAED,sDAAsD;AAC/C,KAAK,UAAU,mBAAmB,CAAC,QAAgB,EAAE,UAAkB;IAC5E,MAAM,GAAG,GAAG,MAAM,IAAA,qBAAa,EAAC,iCAAa,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IACnH,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,CAAA;IACrC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,YAAY,EAAE,IAAA,qCAAc,EAAC,IAAA,iCAAU,EAAC,GAAG,CAAC,WAAW,CAAC,CAAC,EAAE,CAAA;AACxH,CAAC","sourcesContent":["/*\n * **구동은 원본을 지난다** — 트윈을 우회하지 않는다 (ADR-0029 §8, 2026-08-19).\n *\n * ── 무엇이 뚫려 있었나 ──────────────────────────────────────────────────────\n * `controlTwinScenario` 가 `TwinEngine.runtime(...).scenario` 를 **직접** 잡았다. 그래서 시뮬만 할 수 있는\n * 일(자극·배속·정지)이 계약 밖의 지름길로 들어왔고, 실 시스템에는 그 자리가 아예 없었다 — ADR-0029 의\n * 판정 문장에 걸리는 상태였다.\n *\n * 이제 길은 하나다: **트윈 → 그 트윈의 원본 → 어댑터가 선언한 `control` 능력.** 선언하지 않은 원본에는\n * 문이 열리지 않고, 그 사실을 코드로 말한다(`not-declared`). 실 WMS 가 정지되지 않는 것은 결손이 아니다.\n *\n * ── 전송은 여기서 묻지 않는다 (§7) ──────────────────────────────────────────\n * 인프로세스 시뮬레이터는 같은 계약을 인프로세스로 이행한다. 나중에 그것이 다른 프로세스로 나가도 이 경로는\n * 바뀌지 않는다 — 「계약을 지나는가」만 보고 「네트워크를 타는가」는 묻지 않는다.\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\nimport { capabilitiesOf, getAdapter, type ControlCommand, type ControlResult, type SiteDescriptor } from './reference-adapter.js'\n\n/**\n * 이 트윈의 원본에 구동 명령을 보낸다 — **원본이 없거나 능력을 선언하지 않으면 거절한다.**\n *\n * 거절을 조용히 하지 않는다: 사유 코드를 돌려주고 화면이 그것을 사람 말로 옮긴다. 「아무 일도 일어나지\n * 않았다」가 가장 비싼 답이다.\n */\nexport async function routeControl(domainId: string, instanceId: string, command: ControlCommand): Promise<ControlResult> {\n const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })\n if (!inst) return { ok: false, code: 'unknown-twin', detail: instanceId }\n\n const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })\n if (!ref) {\n /* 원본을 모르는 트윈 — 구동할 대상이 없다. 트윈을 직접 흔드는 길은 두지 않는다(그것이 지름길이었다). */\n return { ok: false, code: 'no-source', detail: `twin \"${instanceId}\" does not remember a source to drive` }\n }\n\n const adapter = getAdapter(ref.adapterType)\n if (!adapter) return { ok: false, code: 'no-adapter', detail: ref.adapterType }\n if (!capabilitiesOf(adapter).includes('control')) {\n return {\n ok: false,\n code: 'not-declared',\n detail: `source \"${ref.source}\" (${ref.adapterType}) does not declare the control capability — a real system is not paused or seeded, and that is a fact rather than a gap`\n }\n }\n\n /*\n * 사이트를 함께 넘긴다 — 원본 하나가 여러 사이트를 낼 수 있다(`discoverSites`). 우리가 아는 것은\n * 이 트윈이 어느 사이트에서 왔는지이고(`origin.siteId`), 모르면 넘기지 않는다(지어내지 않는다).\n */\n const siteId = (inst.origin as any)?.siteId\n const site: SiteDescriptor | undefined = siteId ? { siteId } : undefined\n return adapter.control!(ref.connectionConfig ?? {}, site, { ...command, instanceId, domainId })\n}\n\n/** 이 트윈의 원본이 선언한 능력 — 화면이 「무엇을 할 수 있나」를 먼저 알 수 있게. */\nexport async function controlCapabilityOf(domainId: string, instanceId: string): Promise<{ source?: string; adapterType?: string; capabilities: string[] }> {\n const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })\n if (!ref) return { capabilities: [] }\n return { source: ref.source, adapterType: ref.adapterType, capabilities: capabilitiesOf(getAdapter(ref.adapterType)) }\n}\n"]}
@@ -2,3 +2,4 @@ import { TwinReferenceResolver } from './reference-resolver.js';
2
2
  import { TwinReference } from './twin-reference.js';
3
3
  export declare const entities: (typeof TwinReference)[];
4
4
  export declare const resolvers: (typeof TwinReferenceResolver)[];
5
+ export * from './control-routing.js';
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolvers = exports.entities = void 0;
4
+ const tslib_1 = require("tslib");
4
5
  const reference_resolver_js_1 = require("./reference-resolver.js");
5
6
  const twin_reference_js_1 = require("./twin-reference.js");
6
7
  exports.entities = [twin_reference_js_1.TwinReference];
7
8
  exports.resolvers = [reference_resolver_js_1.TwinReferenceResolver];
9
+ tslib_1.__exportStar(require("./control-routing.js"), exports);
8
10
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/reference/index.ts"],"names":[],"mappings":";;;AAAA,mEAA+D;AAC/D,2DAAmD;AAEtC,QAAA,QAAQ,GAAG,CAAC,iCAAa,CAAC,CAAA;AAC1B,QAAA,SAAS,GAAG,CAAC,6CAAqB,CAAC,CAAA","sourcesContent":["import { TwinReferenceResolver } from './reference-resolver.js'\nimport { TwinReference } from './twin-reference.js'\n\nexport const entities = [TwinReference]\nexport const resolvers = [TwinReferenceResolver]\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/reference/index.ts"],"names":[],"mappings":";;;;AAAA,mEAA+D;AAC/D,2DAAmD;AAEtC,QAAA,QAAQ,GAAG,CAAC,iCAAa,CAAC,CAAA;AAC1B,QAAA,SAAS,GAAG,CAAC,6CAAqB,CAAC,CAAA;AAChD,+DAAoC","sourcesContent":["import { TwinReferenceResolver } from './reference-resolver.js'\nimport { TwinReference } from './twin-reference.js'\n\nexport const entities = [TwinReference]\nexport const resolvers = [TwinReferenceResolver]\nexport * from './control-routing.js'\n"]}
@@ -35,6 +35,53 @@ export interface AdapterMeta {
35
35
  grounding?: 'vendor-doc' | 'standard' | 'facsimile';
36
36
  docUrl?: string;
37
37
  }
38
+ /**
39
+ * 원본이 **선언하는 능력** — 「이 원본으로 무엇을 할 수 있나」 (ADR-0029 §8, 2026-08-19).
40
+ *
41
+ * ── 왜 능력으로 선언하나 ────────────────────────────────────────────────────
42
+ * 실 WMS 는 스스로 사실을 낸다. 시뮬레이터는 **구동해 줘야** 움직인다 — 자극·속도·정지·초기화. 그런데
43
+ * 계약에 그 자리가 없어서 통제가 `TwinEngine` 안에 뚫려 있었다(`controlTwinScenario` 가 런타임의 시나리오
44
+ * 엔진을 직접 잡았다). 그러면 ADR-0029 의 판정 문장(*"실물이 같은 계약으로 할 수 있는가"*)에 걸린다:
45
+ * 실 WMS 는 일시정지될 수 없으니 그 통제면은 영원히 「어댑터 규약의 결손」으로 남고, 규칙이 그 면을
46
+ * **숨게** 만든다.
47
+ *
48
+ * 그래서 뒤집는다: 통제는 **선언된 능력**이다. 선언하지 않은 원본에는 그 문이 **아예 열리지 않는다** —
49
+ * 그것이 결손이 아니라 **사실**이다(실 WMS 는 정지되지 않는다). 이 레포가 이미 그 규율로 굴러간다
50
+ * (`capability-contract`: concrete 강결합 대신 능력 선언).
51
+ *
52
+ * · `live` — 돌아가는 피드를 낸다(`openLiveFeed`).
53
+ * · `control` — 구동을 받는다(`control`) — 자극·속도·정지·초기화.
54
+ */
55
+ export type ReferenceCapability = 'live' | 'control';
56
+ /** 원본에 보내는 구동 명령 — **시뮬레이터를 움직이는 말**이지 트윈을 고치는 말이 아니다. */
57
+ export interface ControlCommand {
58
+ /**
59
+ * · `stimulus` — 자극을 싣는다(`scenario`). 선언은 원본 설정에 살고 이 명령은 지금 태우는 것이다.
60
+ * · `start`·`pause` — 구동을 켜고 멈춘다.
61
+ * · `speed` — 시각의 배속(`speed`).
62
+ * · `reset` — 씨앗부터 다시(원본의 상태를 되돌린다 — 트윈의 저널은 재기동 정책이 정한다).
63
+ */
64
+ action: 'stimulus' | 'start' | 'pause' | 'speed' | 'reset';
65
+ /** 자극 선언 — `action: 'stimulus'` 에만. 커널 규약(`validateScenario`)으로 검사된 값이어야 한다. */
66
+ scenario?: any;
67
+ /** 배속 — `action: 'speed'` 에만. */
68
+ speed?: number;
69
+ /**
70
+ * 어느 트윈을 위한 구동인가 — 원본이 여러 트윈에 먹일 수 있으므로 대상을 함께 말한다.
71
+ *
72
+ * 인프로세스 시뮬레이터는 이 둘로 자기 런타임을 찾는다(테넌트까지 있어야 같은 id 의 두 트윈이 갈린다).
73
+ * 원격 원본은 대개 무시한다(자기 세계가 하나다).
74
+ */
75
+ instanceId?: string;
76
+ domainId?: string;
77
+ }
78
+ /** 구동 결과 — **왜 안 됐는지**를 코드로 말한다(화면이 옮긴다). */
79
+ export interface ControlResult {
80
+ ok: boolean;
81
+ /** 언어중립 사유 — `not-declared`(능력 없음) · `unsupported-action` · `invalid`(선언 거절) · `no-target`. */
82
+ code?: string;
83
+ detail?: string;
84
+ }
38
85
  export interface ReferenceAdapter {
39
86
  /** 레지스트리 키 (예: 'virtual' | 'sap-ewm' | 'custom-rest'). */
40
87
  type: string;
@@ -51,7 +98,27 @@ export interface ReferenceAdapter {
51
98
  fetchMaster(cfg: ConnectionConfig, site: SiteDescriptor): Promise<ReferenceMaster>;
52
99
  /** (선택) 사이트별 실 이벤트 스트림 — 없으면 sim. onRecords 로 레코드 push, unsubscribe 반환. */
53
100
  openLiveFeed?(cfg: ConnectionConfig, site: SiteDescriptor, onRecords: (records: unknown[]) => void): () => void;
101
+ /**
102
+ * 이 원본이 **선언하는 능력** — 없으면 「구조만 준다」는 뜻이다(ADR-0029 §8).
103
+ *
104
+ * 선언과 구현이 어긋나지 않게 `capabilitiesOf` 가 둘을 함께 본다: `control` 을 선언했는데 `control`
105
+ * 구현이 없으면 그 능력은 **없는 것으로 읽는다**(선언만 있는 능력은 사용자에게 거짓말이 된다).
106
+ */
107
+ capabilities?: ReferenceCapability[];
108
+ /**
109
+ * (선택) 구동 — 자극·속도·정지·초기화. **`capabilities` 에 `control` 을 선언한 원본만** 가진다.
110
+ *
111
+ * 실 시스템은 이것을 갖지 않는다(실 WMS 는 일시정지되지 않는다). 그 사실이 결손이 아니라 이 축의 요점이다.
112
+ */
113
+ control?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, command: ControlCommand): Promise<ControlResult>;
54
114
  }
115
+ /**
116
+ * 이 원본이 **실제로 할 수 있는 것** — 선언과 구현의 교집합.
117
+ *
118
+ * 선언만 있고 구현이 없으면 화면이 「할 수 있다」고 말한 뒤 아무 일도 일어나지 않는다. 반대로 구현이
119
+ * 있는데 선언이 없으면 그 능력은 **의도적으로 닫아 둔 것**이므로 열지 않는다 — 선언이 권위다.
120
+ */
121
+ export declare function capabilitiesOf(adapter: ReferenceAdapter | undefined): ReferenceCapability[];
55
122
  export declare function registerAdapterType(adapter: ReferenceAdapter): void;
56
123
  export declare function getAdapter(type: string): ReferenceAdapter | undefined;
57
124
  export declare function listAdapterTypes(): string[];
@@ -1,9 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capabilitiesOf = capabilitiesOf;
3
4
  exports.registerAdapterType = registerAdapterType;
4
5
  exports.getAdapter = getAdapter;
5
6
  exports.listAdapterTypes = listAdapterTypes;
6
7
  exports.listAdapters = listAdapters;
8
+ /**
9
+ * 이 원본이 **실제로 할 수 있는 것** — 선언과 구현의 교집합.
10
+ *
11
+ * 선언만 있고 구현이 없으면 화면이 「할 수 있다」고 말한 뒤 아무 일도 일어나지 않는다. 반대로 구현이
12
+ * 있는데 선언이 없으면 그 능력은 **의도적으로 닫아 둔 것**이므로 열지 않는다 — 선언이 권위다.
13
+ */
14
+ function capabilitiesOf(adapter) {
15
+ if (!adapter)
16
+ return [];
17
+ const declared = new Set(adapter.capabilities ?? []);
18
+ const out = [];
19
+ if (declared.has('live') && typeof adapter.openLiveFeed === 'function')
20
+ out.push('live');
21
+ if (declared.has('control') && typeof adapter.control === 'function')
22
+ out.push('control');
23
+ return out;
24
+ }
7
25
  /* 어댑터 "타입" 레지스트리(전역·인메모리) — 레퍼런스 데이터는 DB(TwinReference), 어댑터 동작은 여기. */
8
26
  const adapters = new Map();
9
27
  function registerAdapterType(adapter) {
@@ -1 +1 @@
1
- {"version":3,"file":"reference-adapter.js","sourceRoot":"","sources":["../../../server/service/reference/reference-adapter.ts"],"names":[],"mappings":";;AAwEA,kDAEC;AACD,gCAEC;AACD,4CAEC;AAED,oCAEC;AAfD,wEAAwE;AACxE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAA;AAEpD,SAAgB,mBAAmB,CAAC,OAAyB;IAC3D,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AACrC,CAAC;AACD,SAAgB,UAAU,CAAC,IAAY;IACrC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAC3B,CAAC;AACD,SAAgB,gBAAgB;IAC9B,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;AAC7B,CAAC;AACD,kCAAkC;AAClC,SAAgB,YAAY;IAC1B,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;AAC1E,CAAC","sourcesContent":["import type { ReferenceMaster } from './reference-master.js'\n\n/*\n * 레퍼런스 어댑터 계약 (reference-management P1) — 정적 마스터 등록(`registerReference`)을\n * \"어댑터 타입\" 등록으로 일반화한다. 레퍼런스 = 데이터(TwinReference 행), 어댑터 = 동작.\n * inbound 전용(구조·상태 유입). 아웃바운드 액추에이션은 command-routing(별도 ActuationAdapter) 소유.\n * seam: openLiveFeed 레코드 → 커널 face2-adapter.ingest → CanonicalEnvelope 까지가 여기,\n * 이후 projector 구동은 face2-inbound-live 소유.\n * 설계 SoT: operato-twin/design/plans/reference-management.md §2.2·§4.2.\n */\n\n/** 연결 설정(어댑터별 스키마) — 엔드포인트·자격·파라미터. */\nexport type ConnectionConfig = Record<string, any>\n\n/** discoverSites 가 반환하는 사이트/공장 서술자 — N 발견(§2.2). 1:1 은 길이 1 배열. */\nexport interface SiteDescriptor {\n /** 안정적 사이트 정체성 키(소스의 plant code 등) — 공간 정합 1차 키. */\n siteId: string\n name?: string\n /** 사이트별 시스템(소스가 시스템을 넘나들 때). 없으면 레퍼런스 system 사용. */\n system?: string\n geo?: { lat: number; lon: number }\n hint?: any\n}\n\n/** 연결 폼 필드 서술 — UI 가 이 스키마로 connectionConfig 입력 폼을 렌더한다(어댑터가 스키마 제공). */\nexport interface AdapterConfigField {\n key: string\n label: string\n placeholder?: string\n required?: boolean\n secret?: boolean // 비밀번호/토큰 — 마스킹 입력\n multiline?: boolean // JSON/매핑 등 여러 줄 입력(textarea)\n help?: string // 필드 아래 도움말\n default?: string // 폼 프리필 기본값(예시 매핑/URL 등)\n}\n/** 커넥터 메타데이터 — picker·연결 폼용 표시 정보. */\nexport interface AdapterMeta {\n label?: string\n domain?: string // 'wms' | 'yms' | 'mes' (분류)\n available?: boolean // false = 준비 중(picker 비활성)\n configSchema?: AdapterConfigField[]\n ootbNote?: string // OOTB 매핑 안내 문구\n /*\n * 근거 수준(정직성 1급 표기 — \"실제 API 명세에 근거하는가\"를 UI·피치가 오해 없이 보이게).\n * 'vendor-doc' = 벤더 공개 API 문서의 엔드포인트·필드명에 정합(docUrl 로 출처 명시). 실 시스템 검증은 별도.\n * 'standard' = 개방 표준(EPCIS 2.0 / ISA-95)에 정합 — 벤더 독립.\n * 'facsimile' = 시스템 어휘를 흉내낸 근사치(공개 문서 미정합). 실 연동 시 매핑 재정합 필요.\n * 미지정은 facsimile 로 간주(가장 보수적).\n */\n grounding?: 'vendor-doc' | 'standard' | 'facsimile'\n docUrl?: string // grounding='vendor-doc' 의 근거 문서 URL(출처 추적)\n}\n\nexport interface ReferenceAdapter {\n /** 레지스트리 키 (예: 'virtual' | 'sap-ewm' | 'custom-rest'). */\n type: string\n /** picker·연결 폼용 메타데이터(선택). 없으면 type 만 노출. */\n meta?: AdapterMeta\n /** 연결 확인 — UI '연결 테스트'. */\n testConnection(cfg: ConnectionConfig): Promise<{ ok: boolean; error?: string }>\n /** N 발견 — 사이트/공장 열거. 단일 소스는 길이 1 반환. */\n discoverSites(cfg: ConnectionConfig): Promise<SiteDescriptor[]>\n /** 사이트별 마스터(구조) — 기존 masterToTwin 이 소비. */\n fetchMaster(cfg: ConnectionConfig, site: SiteDescriptor): Promise<ReferenceMaster>\n /** (선택) 사이트별 실 이벤트 스트림 — 없으면 sim. onRecords 로 레코드 push, unsubscribe 반환. */\n openLiveFeed?(cfg: ConnectionConfig, site: SiteDescriptor, onRecords: (records: unknown[]) => void): () => void\n}\n\n/* 어댑터 \"타입\" 레지스트리(전역·인메모리) — 레퍼런스 데이터는 DB(TwinReference), 어댑터 동작은 여기. */\nconst adapters = new Map<string, ReferenceAdapter>()\n\nexport function registerAdapterType(adapter: ReferenceAdapter): void {\n adapters.set(adapter.type, adapter)\n}\nexport function getAdapter(type: string): ReferenceAdapter | undefined {\n return adapters.get(type)\n}\nexport function listAdapterTypes(): string[] {\n return [...adapters.keys()]\n}\n/** 커넥터 메타데이터 목록(picker·연결 폼용). */\nexport function listAdapters(): { type: string; meta?: AdapterMeta }[] {\n return [...adapters.values()].map(a => ({ type: a.type, meta: a.meta }))\n}\n"]}
1
+ {"version":3,"file":"reference-adapter.js","sourceRoot":"","sources":["../../../server/service/reference/reference-adapter.ts"],"names":[],"mappings":";;AA0IA,wCAOC;AAKD,kDAEC;AACD,gCAEC;AACD,4CAEC;AAED,oCAEC;AA9BD;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,OAAqC;IAClE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAA;IACvB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,CAAA;IACpD,MAAM,GAAG,GAA0B,EAAE,CAAA;IACrC,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,OAAO,CAAC,YAAY,KAAK,UAAU;QAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACxF,IAAI,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU;QAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACzF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,wEAAwE;AACxE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAA;AAEpD,SAAgB,mBAAmB,CAAC,OAAyB;IAC3D,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AACrC,CAAC;AACD,SAAgB,UAAU,CAAC,IAAY;IACrC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAC3B,CAAC;AACD,SAAgB,gBAAgB;IAC9B,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;AAC7B,CAAC;AACD,kCAAkC;AAClC,SAAgB,YAAY;IAC1B,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;AAC1E,CAAC","sourcesContent":["import type { ReferenceMaster } from './reference-master.js'\n\n/*\n * 레퍼런스 어댑터 계약 (reference-management P1) — 정적 마스터 등록(`registerReference`)을\n * \"어댑터 타입\" 등록으로 일반화한다. 레퍼런스 = 데이터(TwinReference 행), 어댑터 = 동작.\n * inbound 전용(구조·상태 유입). 아웃바운드 액추에이션은 command-routing(별도 ActuationAdapter) 소유.\n * seam: openLiveFeed 레코드 → 커널 face2-adapter.ingest → CanonicalEnvelope 까지가 여기,\n * 이후 projector 구동은 face2-inbound-live 소유.\n * 설계 SoT: operato-twin/design/plans/reference-management.md §2.2·§4.2.\n */\n\n/** 연결 설정(어댑터별 스키마) — 엔드포인트·자격·파라미터. */\nexport type ConnectionConfig = Record<string, any>\n\n/** discoverSites 가 반환하는 사이트/공장 서술자 — N 발견(§2.2). 1:1 은 길이 1 배열. */\nexport interface SiteDescriptor {\n /** 안정적 사이트 정체성 키(소스의 plant code 등) — 공간 정합 1차 키. */\n siteId: string\n name?: string\n /** 사이트별 시스템(소스가 시스템을 넘나들 때). 없으면 레퍼런스 system 사용. */\n system?: string\n geo?: { lat: number; lon: number }\n hint?: any\n}\n\n/** 연결 폼 필드 서술 — UI 가 이 스키마로 connectionConfig 입력 폼을 렌더한다(어댑터가 스키마 제공). */\nexport interface AdapterConfigField {\n key: string\n label: string\n placeholder?: string\n required?: boolean\n secret?: boolean // 비밀번호/토큰 — 마스킹 입력\n multiline?: boolean // JSON/매핑 등 여러 줄 입력(textarea)\n help?: string // 필드 아래 도움말\n default?: string // 폼 프리필 기본값(예시 매핑/URL 등)\n}\n/** 커넥터 메타데이터 — picker·연결 폼용 표시 정보. */\nexport interface AdapterMeta {\n label?: string\n domain?: string // 'wms' | 'yms' | 'mes' (분류)\n available?: boolean // false = 준비 중(picker 비활성)\n configSchema?: AdapterConfigField[]\n ootbNote?: string // OOTB 매핑 안내 문구\n /*\n * 근거 수준(정직성 1급 표기 — \"실제 API 명세에 근거하는가\"를 UI·피치가 오해 없이 보이게).\n * 'vendor-doc' = 벤더 공개 API 문서의 엔드포인트·필드명에 정합(docUrl 로 출처 명시). 실 시스템 검증은 별도.\n * 'standard' = 개방 표준(EPCIS 2.0 / ISA-95)에 정합 — 벤더 독립.\n * 'facsimile' = 시스템 어휘를 흉내낸 근사치(공개 문서 미정합). 실 연동 시 매핑 재정합 필요.\n * 미지정은 facsimile 로 간주(가장 보수적).\n */\n grounding?: 'vendor-doc' | 'standard' | 'facsimile'\n docUrl?: string // grounding='vendor-doc' 의 근거 문서 URL(출처 추적)\n}\n\n/**\n * 원본이 **선언하는 능력** — 「이 원본으로 무엇을 할 수 있나」 (ADR-0029 §8, 2026-08-19).\n *\n * ── 왜 능력으로 선언하나 ────────────────────────────────────────────────────\n * 실 WMS 는 스스로 사실을 낸다. 시뮬레이터는 **구동해 줘야** 움직인다 — 자극·속도·정지·초기화. 그런데\n * 계약에 그 자리가 없어서 통제가 `TwinEngine` 안에 뚫려 있었다(`controlTwinScenario` 가 런타임의 시나리오\n * 엔진을 직접 잡았다). 그러면 ADR-0029 의 판정 문장(*\"실물이 같은 계약으로 할 수 있는가\"*)에 걸린다:\n * 실 WMS 는 일시정지될 수 없으니 그 통제면은 영원히 「어댑터 규약의 결손」으로 남고, 규칙이 그 면을\n * **숨게** 만든다.\n *\n * 그래서 뒤집는다: 통제는 **선언된 능력**이다. 선언하지 않은 원본에는 그 문이 **아예 열리지 않는다** —\n * 그것이 결손이 아니라 **사실**이다(실 WMS 는 정지되지 않는다). 이 레포가 이미 그 규율로 굴러간다\n * (`capability-contract`: concrete 강결합 대신 능력 선언).\n *\n * · `live` — 돌아가는 피드를 낸다(`openLiveFeed`).\n * · `control` — 구동을 받는다(`control`) — 자극·속도·정지·초기화.\n */\nexport type ReferenceCapability = 'live' | 'control'\n\n/** 원본에 보내는 구동 명령 — **시뮬레이터를 움직이는 말**이지 트윈을 고치는 말이 아니다. */\nexport interface ControlCommand {\n /**\n * · `stimulus` — 자극을 싣는다(`scenario`). 선언은 원본 설정에 살고 이 명령은 지금 태우는 것이다.\n * · `start`·`pause` — 구동을 켜고 멈춘다.\n * · `speed` — 시각의 배속(`speed`).\n * · `reset` — 씨앗부터 다시(원본의 상태를 되돌린다 — 트윈의 저널은 재기동 정책이 정한다).\n */\n action: 'stimulus' | 'start' | 'pause' | 'speed' | 'reset'\n /** 자극 선언 — `action: 'stimulus'` 에만. 커널 규약(`validateScenario`)으로 검사된 값이어야 한다. */\n scenario?: any\n /** 배속 — `action: 'speed'` 에만. */\n speed?: number\n /**\n * 어느 트윈을 위한 구동인가 — 원본이 여러 트윈에 먹일 수 있으므로 대상을 함께 말한다.\n *\n * 인프로세스 시뮬레이터는 이 둘로 자기 런타임을 찾는다(테넌트까지 있어야 같은 id 의 두 트윈이 갈린다).\n * 원격 원본은 대개 무시한다(자기 세계가 하나다).\n */\n instanceId?: string\n domainId?: string\n}\n\n/** 구동 결과 — **왜 안 됐는지**를 코드로 말한다(화면이 옮긴다). */\nexport interface ControlResult {\n ok: boolean\n /** 언어중립 사유 — `not-declared`(능력 없음) · `unsupported-action` · `invalid`(선언 거절) · `no-target`. */\n code?: string\n detail?: string\n}\n\nexport interface ReferenceAdapter {\n /** 레지스트리 키 (예: 'virtual' | 'sap-ewm' | 'custom-rest'). */\n type: string\n /** picker·연결 폼용 메타데이터(선택). 없으면 type 만 노출. */\n meta?: AdapterMeta\n /** 연결 확인 — UI '연결 테스트'. */\n testConnection(cfg: ConnectionConfig): Promise<{ ok: boolean; error?: string }>\n /** N 발견 — 사이트/공장 열거. 단일 소스는 길이 1 반환. */\n discoverSites(cfg: ConnectionConfig): Promise<SiteDescriptor[]>\n /** 사이트별 마스터(구조) — 기존 masterToTwin 이 소비. */\n fetchMaster(cfg: ConnectionConfig, site: SiteDescriptor): Promise<ReferenceMaster>\n /** (선택) 사이트별 실 이벤트 스트림 — 없으면 sim. onRecords 로 레코드 push, unsubscribe 반환. */\n openLiveFeed?(cfg: ConnectionConfig, site: SiteDescriptor, onRecords: (records: unknown[]) => void): () => void\n /**\n * 이 원본이 **선언하는 능력** — 없으면 「구조만 준다」는 뜻이다(ADR-0029 §8).\n *\n * 선언과 구현이 어긋나지 않게 `capabilitiesOf` 가 둘을 함께 본다: `control` 을 선언했는데 `control`\n * 구현이 없으면 그 능력은 **없는 것으로 읽는다**(선언만 있는 능력은 사용자에게 거짓말이 된다).\n */\n capabilities?: ReferenceCapability[]\n /**\n * (선택) 구동 — 자극·속도·정지·초기화. **`capabilities` 에 `control` 을 선언한 원본만** 가진다.\n *\n * 실 시스템은 이것을 갖지 않는다(실 WMS 는 일시정지되지 않는다). 그 사실이 결손이 아니라 이 축의 요점이다.\n */\n control?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, command: ControlCommand): Promise<ControlResult>\n}\n\n/**\n * 이 원본이 **실제로 할 수 있는 것** — 선언과 구현의 교집합.\n *\n * 선언만 있고 구현이 없으면 화면이 「할 수 있다」고 말한 뒤 아무 일도 일어나지 않는다. 반대로 구현이\n * 있는데 선언이 없으면 그 능력은 **의도적으로 닫아 둔 것**이므로 열지 않는다 — 선언이 권위다.\n */\nexport function capabilitiesOf(adapter: ReferenceAdapter | undefined): ReferenceCapability[] {\n if (!adapter) return []\n const declared = new Set(adapter.capabilities ?? [])\n const out: ReferenceCapability[] = []\n if (declared.has('live') && typeof adapter.openLiveFeed === 'function') out.push('live')\n if (declared.has('control') && typeof adapter.control === 'function') out.push('control')\n return out\n}\n\n/* 어댑터 \"타입\" 레지스트리(전역·인메모리) — 레퍼런스 데이터는 DB(TwinReference), 어댑터 동작은 여기. */\nconst adapters = new Map<string, ReferenceAdapter>()\n\nexport function registerAdapterType(adapter: ReferenceAdapter): void {\n adapters.set(adapter.type, adapter)\n}\nexport function getAdapter(type: string): ReferenceAdapter | undefined {\n return adapters.get(type)\n}\nexport function listAdapterTypes(): string[] {\n return [...adapters.keys()]\n}\n/** 커넥터 메타데이터 목록(picker·연결 폼용). */\nexport function listAdapters(): { type: string; meta?: AdapterMeta }[] {\n return [...adapters.values()].map(a => ({ type: a.type, meta: a.meta }))\n}\n"]}
@@ -10,7 +10,7 @@ const index_js_1 = require("../../engine/index.js");
10
10
  const twin_instance_js_1 = require("../twin-instance/twin-instance.js");
11
11
  const twin_reference_js_1 = require("./twin-reference.js");
12
12
  /*
13
- * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① realityMode='mirror').
13
+ * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① restartPolicy='resync').
14
14
  * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).
15
15
  * 커넥터가 "벤더 이벤트 → 정규 레코드" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와
16
16
  * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).
@@ -29,7 +29,7 @@ async function startReferenceLiveFeed(domainId, adapterType, cfg, site, instance
29
29
  const det = await index_js_1.TwinEngine.detail(domainId, instanceId);
30
30
  if (!det?.model)
31
31
  throw new Error(`instance "${instanceId}" not provisioned (no model) — ingest master before live start`);
32
- // mirror 모드 기동 — 커널 tick 없이 projector 가 실 이벤트를 미러(startLive 가 realityMode='mirror' 설정).
32
+ // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).
33
33
  // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.
34
34
  index_js_1.TwinEngine.startLive(instanceId, domainId, det.kind, await index_js_1.TwinEngine.withSpaceTimeBase(det.model, domainId));
35
35
  stopReferenceLiveFeed(instanceId); // 재기동 시 이전 피드 정리
@@ -117,7 +117,7 @@ async function resumeReferenceLiveFeeds() {
117
117
  const rows = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).find({
118
118
  where: produced.map((p) => ({ domain: { id: domainId }, instanceId: p.instanceId }))
119
119
  });
120
- const live = produced.filter((p) => rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.realityMode === 'mirror'));
120
+ const live = produced.filter((p) => rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.restartPolicy === 'resync'));
121
121
  if (!live.length)
122
122
  continue;
123
123
  let sites;
@@ -1 +1 @@
1
- {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type SiteDescriptor } from './reference-adapter.js'\nimport { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe, type CanonicalRecord } from '../../engine/index.js'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① realityMode='mirror').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.model) throw new Error(`instance \"${instanceId}\" not provisioned (no model) — ingest master before live start`)\n\n // mirror 모드 기동 — 커널 tick 없이 projector 가 실 이벤트를 미러(startLive 가 realityMode='mirror' 설정).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const { accepted, rejected } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())\n if (accepted.length) TwinEngine.ingestLive(domainId, instanceId, accepted)\n /*\n * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 지금은 이유와 함께 남긴다. 화면까지 나르는 것은 다음 일이다(라이브 계측은 초당 여러 건이라\n * 건마다 방송하면 그 자체가 부하가 된다 — 집계해 낼 자리가 필요하다).\n */\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n console.warn(`[twin-live] \"${instanceId}\": ${rejected.length} record(s) rejected — e.g. ${why}`)\n }\n })\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/**\n * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.\n *\n * ── 무엇이 조용히 끊겼나 (2026-08-14) ───────────────────────────────────────\n * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번\n * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,\n * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.\n *\n * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────\n * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.\n * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가\n * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.\n *\n * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을\n * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.\n */\nexport async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {\n const resumed: string[] = []\n const failed: { instanceId: string; error: string }[] = []\n /*\n * **한 트윈에 피드는 하나다** (2026-08-18).\n *\n * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로\n * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를\n * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다\n * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).\n *\n * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 조용히 하나를\n * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.\n */\n const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스\n try {\n const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })\n for (const ref of refs) {\n const domainId = (ref as any).domain?.id ?? (ref as any).domainId\n const adapter = getAdapter(ref.adapterType)\n if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다\n\n const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)\n if (!produced.length) continue\n\n /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */\n const rows = await getRepository(TwinInstance).find({\n where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any\n })\n const live = produced.filter((p: any) =>\n rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.realityMode === 'mirror')\n )\n if (!live.length) continue\n\n let sites: SiteDescriptor[]\n try {\n sites = await adapter.discoverSites(ref.connectionConfig ?? {})\n } catch (e: any) {\n /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */\n for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })\n continue\n }\n\n for (const p of live) {\n const site = sites.find(s => s.siteId === p.siteId)\n if (!site) {\n failed.push({ instanceId: p.instanceId, error: `site \"${p.siteId}\" no longer discoverable` })\n continue\n }\n const owner = attached.get(p.instanceId)\n if (owner) {\n /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */\n failed.push({\n instanceId: p.instanceId,\n error: `also claimed by reference \"${ref.source}\" — kept the feed from \"${owner}\" (two connections feeding one twin would reset the mirror)`\n })\n continue\n }\n attached.set(p.instanceId, ref.source)\n try {\n const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId)\n if (ok) resumed.push(p.instanceId)\n else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })\n } catch (e: any) {\n failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })\n }\n }\n }\n } catch (e: any) {\n console.error('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) console.log(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) console.warn(`[twin-live] \"${f.instanceId}\" feed NOT reattached — ${f.error}`)\n return { resumed, failed }\n}\n\n/*\n * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.\n * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)\n */\nregisterLiveFeedProbe(() => [...feeds.keys()])\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}
1
+ {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type SiteDescriptor } from './reference-adapter.js'\nimport { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe, type CanonicalRecord } from '../../engine/index.js'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① restartPolicy='resync').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.model) throw new Error(`instance \"${instanceId}\" not provisioned (no model) — ingest master before live start`)\n\n // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const { accepted, rejected } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())\n if (accepted.length) TwinEngine.ingestLive(domainId, instanceId, accepted)\n /*\n * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 지금은 이유와 함께 남긴다. 화면까지 나르는 것은 다음 일이다(라이브 계측은 초당 여러 건이라\n * 건마다 방송하면 그 자체가 부하가 된다 — 집계해 낼 자리가 필요하다).\n */\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n console.warn(`[twin-live] \"${instanceId}\": ${rejected.length} record(s) rejected — e.g. ${why}`)\n }\n })\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/**\n * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.\n *\n * ── 무엇이 조용히 끊겼나 (2026-08-14) ───────────────────────────────────────\n * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번\n * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,\n * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.\n *\n * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────\n * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.\n * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가\n * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.\n *\n * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을\n * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.\n */\nexport async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {\n const resumed: string[] = []\n const failed: { instanceId: string; error: string }[] = []\n /*\n * **한 트윈에 피드는 하나다** (2026-08-18).\n *\n * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로\n * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를\n * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다\n * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).\n *\n * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 조용히 하나를\n * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.\n */\n const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스\n try {\n const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })\n for (const ref of refs) {\n const domainId = (ref as any).domain?.id ?? (ref as any).domainId\n const adapter = getAdapter(ref.adapterType)\n if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다\n\n const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)\n if (!produced.length) continue\n\n /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */\n const rows = await getRepository(TwinInstance).find({\n where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any\n })\n const live = produced.filter((p: any) =>\n rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.restartPolicy === 'resync')\n )\n if (!live.length) continue\n\n let sites: SiteDescriptor[]\n try {\n sites = await adapter.discoverSites(ref.connectionConfig ?? {})\n } catch (e: any) {\n /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */\n for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })\n continue\n }\n\n for (const p of live) {\n const site = sites.find(s => s.siteId === p.siteId)\n if (!site) {\n failed.push({ instanceId: p.instanceId, error: `site \"${p.siteId}\" no longer discoverable` })\n continue\n }\n const owner = attached.get(p.instanceId)\n if (owner) {\n /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */\n failed.push({\n instanceId: p.instanceId,\n error: `also claimed by reference \"${ref.source}\" — kept the feed from \"${owner}\" (two connections feeding one twin would reset the mirror)`\n })\n continue\n }\n attached.set(p.instanceId, ref.source)\n try {\n const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId)\n if (ok) resumed.push(p.instanceId)\n else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })\n } catch (e: any) {\n failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })\n }\n }\n }\n } catch (e: any) {\n console.error('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) console.log(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) console.warn(`[twin-live] \"${f.instanceId}\" feed NOT reattached — ${f.error}`)\n return { resumed, failed }\n}\n\n/*\n * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.\n * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)\n */\nregisterLiveFeedProbe(() => [...feeds.keys()])\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}
@@ -2,7 +2,7 @@ export declare class TwinReferenceResolver {
2
2
  /** 등록된 레퍼런스 소스 목록(요약 + 이미 인제스트됐는지). */
3
3
  referenceSources(context: ResolverContext): Promise<any[]>;
4
4
  /** 소스를 인제스트 → 공간 + 인스턴스 생성(provision). start 면 기동까지. */
5
- importFromReference(source: string, context: ResolverContext, start?: boolean): Promise<any>;
5
+ importFromReference(source: string, restartPolicy: string, context: ResolverContext, start?: boolean): Promise<any>;
6
6
  /**
7
7
  * **원본에서 다시 읽는다** — 이 트윈의 구조를 지금의 원본 모습으로 갱신한다.
8
8
  *
@@ -27,7 +27,7 @@ export declare class TwinReferenceResolver {
27
27
  /** 등록된 트윈 템플릿(파라미터화 아키타입) 목록 — 노브 스키마 포함(클라이언트 폼용). */
28
28
  twinTemplates(_context: ResolverContext): Promise<any[]>;
29
29
  /** 템플릿 + 노브 → 합성 마스터 → 인제스트(+기동+시나리오). 레퍼런스 import 와 같은 경로, 원천만 합성. */
30
- createFromTemplate(templateId: string, params: any, context: ResolverContext, start?: boolean, spaceId?: string): Promise<any>;
30
+ createFromTemplate(templateId: string, params: any, restartPolicy: string, context: ResolverContext, start?: boolean, spaceId?: string): Promise<any>;
31
31
  twinReferences(context: ResolverContext): Promise<any[]>;
32
32
  referenceAdapterTypes(): string[];
33
33
  /**
@@ -64,5 +64,5 @@ export declare class TwinReferenceResolver {
64
64
  * 판정은 그 순수 모듈이 갖는다: 없는 공간은 거절(`resolveIngestSpace`), 사이트 여럿을 한 공간에
65
65
  * 넣는 것도 거절(`refuseImportSpace`) — 서로 다른 현장이 조용히 합쳐지는 것을 막는다.
66
66
  */
67
- importTwinReference(source: string, context: ResolverContext, siteIds?: string[], start?: boolean, live?: boolean, spaceId?: string): Promise<any>;
67
+ importTwinReference(source: string, context: ResolverContext, siteIds?: string[], start?: boolean, live?: boolean, spaceId?: string, restartPolicy?: string): Promise<any>;
68
68
  }
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
5
5
  const type_graphql_1 = require("type-graphql");
6
6
  const shell_1 = require("@things-factory/shell");
7
7
  const index_js_1 = require("../../engine/index.js");
8
+ const restart_policy_js_1 = require("../../engine/restart-policy.js");
8
9
  const twin_kernel_1 = require("@operato/twin-kernel");
9
10
  const knob_defaults_js_1 = require("./knob-defaults.js");
10
11
  const reference_master_js_1 = require("./reference-master.js");
@@ -121,7 +122,7 @@ let TwinReferenceResolver = class TwinReferenceResolver {
121
122
  return out;
122
123
  }
123
124
  /** 소스를 인제스트 → 공간 + 인스턴스 생성(provision). start 면 기동까지. */
124
- async importFromReference(source, context, start) {
125
+ async importFromReference(source, restartPolicy, context, start) {
125
126
  const master = (0, reference_registry_js_1.getReference)(source);
126
127
  if (!master)
127
128
  return { ok: false, error: `unknown reference source: ${source}` };
@@ -130,7 +131,7 @@ let TwinReferenceResolver = class TwinReferenceResolver {
130
131
  /* 어디서 왔는지 남긴다 — 이것이 없으면 이 트윈은 두 번 다시 읽을 수 없다.
131
132
  이 경로는 **메모리에 등록된 마스터 하나**를 그대로 쓰므로 사이트 서술자가 없다(사이트 정체를
132
133
  적을 것이 없다). 다시 읽기도 같은 등록본을 집으므로 사이트를 찾을 필요가 없다. */
133
- const res = await index_js_1.TwinEngine.ingestMaster(domainId, { ...master, origin: { kind: 'reference', source } }, undefined, context.state.user);
134
+ const res = await index_js_1.TwinEngine.ingestMaster(domainId, { ...master, origin: { kind: 'reference', source } }, (0, restart_policy_js_1.readRestartPolicy)(restartPolicy, 'importFromReference'), undefined, context.state.user);
134
135
  if (start)
135
136
  await index_js_1.TwinEngine.startFromRegistry(domainId, res.instanceId);
136
137
  return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings: res.warnings };
@@ -176,7 +177,8 @@ let TwinReferenceResolver = class TwinReferenceResolver {
176
177
  * 기본 공간으로 옮겨 간다 — 조용히 현장을 떠나고 그 현장의 값도 잃는다).
177
178
  */
178
179
  const keepSpace = origin.spaceId || inst.spaceId;
179
- const res = await index_js_1.TwinEngine.ingestMaster(domainId, master, keepSpace ? { spaceId: keepSpace } : undefined, context.state.user);
180
+ /* 다시 읽기는 **구조만** 새로 그린다 — 재기동 정책은 그 트윈의 선언이므로 그대로 지킨다. */
181
+ const res = await index_js_1.TwinEngine.ingestMaster(domainId, master, (0, restart_policy_js_1.readRestartPolicy)(inst.restartPolicy, `resync of twin "${instanceId}"`), keepSpace ? { spaceId: keepSpace } : undefined, context.state.user);
180
182
  return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings: res.warnings };
181
183
  }
182
184
  catch (e) {
@@ -239,7 +241,7 @@ let TwinReferenceResolver = class TwinReferenceResolver {
239
241
  return (0, template_registry_js_1.listTemplates)().map(t => ({ id: t.id, label: t.label, system: t.system, description: t.description, detail: t.detail, knobs: t.knobs }));
240
242
  }
241
243
  /** 템플릿 + 노브 → 합성 마스터 → 인제스트(+기동+시나리오). 레퍼런스 import 와 같은 경로, 원천만 합성. */
242
- async createFromTemplate(templateId, params, context, start, spaceId) {
244
+ async createFromTemplate(templateId, params, restartPolicy, context, start, spaceId) {
243
245
  const tpl = (0, template_registry_js_1.getTemplate)(templateId);
244
246
  if (!tpl)
245
247
  return { ok: false, error: `unknown template: ${templateId}` };
@@ -270,7 +272,7 @@ let TwinReferenceResolver = class TwinReferenceResolver {
270
272
  master.origin = { kind: 'template', templateId, params: effective, ...(spaceId ? { spaceId } : {}) };
271
273
  /* 공간 선택은 템플릿을 거치지 않는다 — 템플릿마다 이 노브를 알고 있어야 한다면 새 템플릿이
272
274
  조용히 무시하게 된다. 합성된 마스터에 한 곳에서 얹는다(레퍼런스 인제스트와 같은 경로). */
273
- const res = await index_js_1.TwinEngine.ingestMaster(domainId, master, { spaceId }, context.state.user);
275
+ const res = await index_js_1.TwinEngine.ingestMaster(domainId, master, (0, restart_policy_js_1.readRestartPolicy)(restartPolicy, `createFromTemplate("${templateId}")`), { spaceId }, context.state.user);
274
276
  if (start !== false) {
275
277
  await index_js_1.TwinEngine.startFromRegistry(domainId, res.instanceId);
276
278
  if (scenario) {
@@ -482,7 +484,7 @@ let TwinReferenceResolver = class TwinReferenceResolver {
482
484
  * 판정은 그 순수 모듈이 갖는다: 없는 공간은 거절(`resolveIngestSpace`), 사이트 여럿을 한 공간에
483
485
  * 넣는 것도 거절(`refuseImportSpace`) — 서로 다른 현장이 조용히 합쳐지는 것을 막는다.
484
486
  */
485
- async importTwinReference(source, context, siteIds, start, live, spaceId) {
487
+ async importTwinReference(source, context, siteIds, start, live, spaceId, restartPolicy) {
486
488
  const domainId = context.state.domain.id;
487
489
  const repo = (0, shell_1.getRepository)(twin_reference_js_1.TwinReference);
488
490
  const ref = await repo.findOne({ where: { domain: { id: domainId }, source } });
@@ -507,7 +509,13 @@ let TwinReferenceResolver = class TwinReferenceResolver {
507
509
  const master = await adapter.fetchMaster(cfg, site);
508
510
  /* 어느 레퍼런스의 **어느 사이트**에서 왔는지 남긴다 — 다시 읽기가 같은 어댑터·같은 사이트로 돌아오려면
509
511
  사이트 정체가 있어야 한다(`source` 는 참조 이름이라 사이트 전부에 같다). */
510
- const res = await index_js_1.TwinEngine.ingestMaster(domainId, { ...master, origin: { kind: 'reference', source, siteId: site.siteId, ...(spaceId ? { spaceId } : {}) } }, { spaceId }, context.state.user);
512
+ /*
513
+ * 정책은 **선언이거나 의도에서 곧바로 따라오는 것**이다: `live=true` 는 미러를 띄우겠다는 뜻이고
514
+ * 미러는 정의상 `resync` 다(외부가 진실이므로 우리 과거를 이어 붙이지 않는다). 시뮬로 띄우려면
515
+ * 스스로 선언해야 한다 — 그 자리에서 우리가 고르면 재기동마다 이력을 지우는 트윈이 조용히 생긴다.
516
+ */
517
+ const policy = (0, restart_policy_js_1.readRestartPolicy)(restartPolicy ?? (live ? 'resync' : undefined), `importTwinReference("${source}")`);
518
+ const res = await index_js_1.TwinEngine.ingestMaster(domainId, { ...master, origin: { kind: 'reference', source, siteId: site.siteId, ...(spaceId ? { spaceId } : {}) } }, policy, { spaceId }, context.state.user);
511
519
  if (start !== false) {
512
520
  // live=true + 어댑터가 openLiveFeed 지원 → mirror(실 이벤트 미러). 아니면 sim 기동(fallback).
513
521
  const wentLive = live ? await (0, reference_live_js_1.startReferenceLiveFeed)(domainId, ref.adapterType, cfg, site, res.instanceId) : false;
@@ -557,12 +565,15 @@ tslib_1.__decorate([
557
565
  ], TwinReferenceResolver.prototype, "referenceSources", null);
558
566
  tslib_1.__decorate([
559
567
  (0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
560
- (0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, { description: 'Ingest a registered reference source into a twin (space + instance). Optionally start it.' }),
568
+ (0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, {
569
+ description: "Ingest a registered reference source into a twin (space + instance). Optionally start it. restartPolicy declares how the new twin treats its own past on reboot: 'resync' (read the source again) | 'resume' (continue its journal) | 'reset' (wipe and replay from the seed). It has no default — a twin cannot exist without the declaration."
570
+ }),
561
571
  tslib_1.__param(0, (0, type_graphql_1.Arg)('source')),
562
- tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
563
- tslib_1.__param(2, (0, type_graphql_1.Arg)('start', { nullable: true })),
572
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('restartPolicy')),
573
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
574
+ tslib_1.__param(3, (0, type_graphql_1.Arg)('start', { nullable: true })),
564
575
  tslib_1.__metadata("design:type", Function),
565
- tslib_1.__metadata("design:paramtypes", [String, Object, Boolean]),
576
+ tslib_1.__metadata("design:paramtypes", [String, String, Object, Boolean]),
566
577
  tslib_1.__metadata("design:returntype", Promise)
567
578
  ], TwinReferenceResolver.prototype, "importFromReference", null);
568
579
  tslib_1.__decorate([
@@ -585,14 +596,17 @@ tslib_1.__decorate([
585
596
  ], TwinReferenceResolver.prototype, "twinTemplates", null);
586
597
  tslib_1.__decorate([
587
598
  (0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
588
- (0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, { description: 'Instantiate a twin template with params → synthetic master → ingest (+start+scenario). Returns {ok, instanceId, spaceId}.' }),
599
+ (0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, {
600
+ description: "Instantiate a twin template with params → synthetic master → ingest (+start+scenario). restartPolicy declares how the new twin treats its own past on reboot ('resync' | 'resume' | 'reset') — there is no default. Returns {ok, instanceId, spaceId}."
601
+ }),
589
602
  tslib_1.__param(0, (0, type_graphql_1.Arg)('templateId')),
590
603
  tslib_1.__param(1, (0, type_graphql_1.Arg)('params', type => shell_1.ScalarObject)),
591
- tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
592
- tslib_1.__param(3, (0, type_graphql_1.Arg)('start', { nullable: true })),
593
- tslib_1.__param(4, (0, type_graphql_1.Arg)('spaceId', { nullable: true, description: 'Existing space to co-locate this twin into (add a lens to a site). Omit to create its own space.' })),
604
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('restartPolicy')),
605
+ tslib_1.__param(3, (0, type_graphql_1.Ctx)()),
606
+ tslib_1.__param(4, (0, type_graphql_1.Arg)('start', { nullable: true })),
607
+ tslib_1.__param(5, (0, type_graphql_1.Arg)('spaceId', { nullable: true, description: 'Existing space to co-locate this twin into (add a lens to a site). Omit to create its own space.' })),
594
608
  tslib_1.__metadata("design:type", Function),
595
- tslib_1.__metadata("design:paramtypes", [String, Object, Object, Boolean, String]),
609
+ tslib_1.__metadata("design:paramtypes", [String, Object, String, Object, Boolean, String]),
596
610
  tslib_1.__metadata("design:returntype", Promise)
597
611
  ], TwinReferenceResolver.prototype, "createFromTemplate", null);
598
612
  tslib_1.__decorate([
@@ -676,15 +690,18 @@ tslib_1.__decorate([
676
690
  ], TwinReferenceResolver.prototype, "discoverReferenceSites", null);
677
691
  tslib_1.__decorate([
678
692
  (0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
679
- (0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, { description: 'Create twins from a reference: discover sites → filter by siteIds (if given) → fetch master(s) → provision. spaceId co-locates the twin into an existing space (one site only). start=false to only provision; live=true to run as a mirror (adapter openLiveFeed → live projector) instead of sim.' }),
693
+ (0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, {
694
+ description: "Create twins from a reference: discover sites → filter by siteIds (if given) → fetch master(s) → provision. spaceId co-locates the twin into an existing space (one site only). start=false to only provision; live=true to run as a mirror (adapter openLiveFeed → live projector) instead of sim. restartPolicy declares how each new twin treats its own past on reboot ('resync' | 'resume' | 'reset'); omit it and live=true implies 'resync' — a mirror reads the source again — while a simulation must declare 'resume' or 'reset' for itself."
695
+ }),
680
696
  tslib_1.__param(0, (0, type_graphql_1.Arg)('source')),
681
697
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
682
698
  tslib_1.__param(2, (0, type_graphql_1.Arg)('siteIds', type => [String], { nullable: true })),
683
699
  tslib_1.__param(3, (0, type_graphql_1.Arg)('start', { nullable: true })),
684
700
  tslib_1.__param(4, (0, type_graphql_1.Arg)('live', { nullable: true })),
685
701
  tslib_1.__param(5, (0, type_graphql_1.Arg)('spaceId', { nullable: true, description: 'Existing space to co-locate this twin into (add a lens to a site). Only when importing a single site. Omit to give each site its own space.' })),
702
+ tslib_1.__param(6, (0, type_graphql_1.Arg)('restartPolicy', { nullable: true, description: "How each new twin treats its own past on reboot: 'resync' | 'resume' | 'reset'. Required unless live=true (a mirror is 'resync' by definition)." })),
686
703
  tslib_1.__metadata("design:type", Function),
687
- tslib_1.__metadata("design:paramtypes", [String, Object, Array, Boolean, Boolean, String]),
704
+ tslib_1.__metadata("design:paramtypes", [String, Object, Array, Boolean, Boolean, String, String]),
688
705
  tslib_1.__metadata("design:returntype", Promise)
689
706
  ], TwinReferenceResolver.prototype, "importTwinReference", null);
690
707
  exports.TwinReferenceResolver = TwinReferenceResolver = tslib_1.__decorate([