@things-factory/headless-twin 10.0.11 → 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.
- package/dist-server/engine/canonical-ingest.d.ts +25 -2
- package/dist-server/engine/canonical-ingest.js +48 -10
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/declared-stimulus.d.ts +43 -0
- package/dist-server/engine/declared-stimulus.js +57 -0
- package/dist-server/engine/declared-stimulus.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +2 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +12 -0
- package/dist-server/engine/kpi-fold.js +21 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +3 -3
- package/dist-server/engine/kpi-query.js +4 -4
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +1 -1
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +3 -6
- package/dist-server/engine/local-declarations.js +97 -16
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/measured-yield.d.ts +42 -0
- package/dist-server/engine/measured-yield.js +75 -0
- package/dist-server/engine/measured-yield.js.map +1 -0
- package/dist-server/engine/operation-basis.d.ts +16 -0
- package/dist-server/engine/operation-basis.js +20 -2
- package/dist-server/engine/operation-basis.js.map +1 -1
- package/dist-server/engine/property-effects.js +17 -0
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/restart-policy.d.ts +13 -0
- package/dist-server/engine/restart-policy.js +52 -0
- package/dist-server/engine/restart-policy.js.map +1 -0
- package/dist-server/engine/spec-coverage.d.ts +8 -0
- package/dist-server/engine/spec-coverage.js +3 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +48 -15
- package/dist-server/engine/twin-engine.js +179 -39
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/reference/control-routing.d.ts +14 -0
- package/dist-server/service/reference/control-routing.js +64 -0
- package/dist-server/service/reference/control-routing.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +1 -0
- package/dist-server/service/reference/index.js +2 -0
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +67 -0
- package/dist-server/service/reference/reference-adapter.js +18 -0
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-live.js +3 -3
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +3 -3
- package/dist-server/service/reference/reference-resolver.js +35 -18
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
- package/dist-server/service/twin-audit/command-audit.js +15 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
- package/dist-server/service/twin-control/twin-control-mutation.js +85 -34
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +2 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +2 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +1 -1
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/package.json +3 -3
- package/server/engine/canonical-ingest.ts +78 -14
- package/server/engine/declared-stimulus.ts +66 -0
- package/server/engine/index.ts +2 -0
- package/server/engine/kpi-fold.ts +29 -1
- package/server/engine/kpi-query.ts +8 -8
- package/server/engine/live-feed-registry.ts +2 -2
- package/server/engine/local-declarations.ts +97 -19
- package/server/engine/measured-yield.ts +89 -0
- package/server/engine/operation-basis.ts +33 -2
- package/server/engine/property-effects.ts +17 -0
- package/server/engine/restart-policy.ts +55 -0
- package/server/engine/spec-coverage.ts +23 -3
- package/server/engine/twin-engine.ts +201 -42
- package/server/index.ts +1 -1
- package/server/service/reference/control-routing.ts +62 -0
- package/server/service/reference/index.ts +1 -0
- package/server/service/reference/reference-adapter.ts +78 -0
- package/server/service/reference/reference-live.ts +3 -3
- package/server/service/reference/reference-resolver.ts +46 -7
- package/server/service/twin-audit/command-audit.ts +34 -1
- package/server/service/twin-audit/twin-audit-event.ts +20 -0
- package/server/service/twin-control/twin-control-mutation.ts +75 -28
- package/server/service/twin-instance/twin-instance.ts +16 -10
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +2 -1
- package/server/service/twin-model/twin-model-query.ts +1 -1
- package/test/adopt-structure-live.test.ts +7 -7
- package/test/boot-resume.test.ts +3 -3
- package/test/control-capability.test.ts +103 -0
- package/test/declared-stimulus.test.ts +88 -0
- package/test/ingest-running-guard.test.ts +5 -5
- package/test/instance-cache-lifecycle.test.ts +1 -1
- package/test/kpi-baseline-db.test.ts +1 -1
- package/test/kpi-query-bench.test.ts +1 -1
- package/test/lineage-survives-restart.test.ts +3 -3
- package/test/live-feed-registry.test.ts +6 -6
- package/test/local-declarations.test.ts +108 -1
- package/test/measured-yield.test.ts +90 -0
- package/test/operation-basis.test.ts +28 -1
- package/test/operational-vocabulary.test.ts +108 -0
- package/test/operations-capability-db.test.ts +4 -4
- package/test/project-structure-db.test.ts +1 -1
- package/test/projection-reaches-screen.test.ts +1 -1
- package/test/property-effects.test.ts +28 -0
- package/test/restart-policy.test.ts +111 -0
- package/test/resync-origin-site.test.ts +7 -1
- package/test/source-outcome-audit.test.ts +104 -0
- package/test/structure-revision-db.test.ts +27 -27
- package/test/tenant-registry-db.test.ts +2 -2
- package/test/twin-model-item-db.test.ts +3 -3
- package/test/twin-model-tree-db.test.ts +7 -7
- package/test/twin-origin-resync.test.ts +4 -4
- package/test/yield-loop.test.ts +144 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -9,6 +9,8 @@ const shell_2 = require("@things-factory/shell");
|
|
|
9
9
|
const index_js_1 = require("../../engine/index.js");
|
|
10
10
|
const twin_audit_event_js_1 = require("../twin-audit/twin-audit-event.js");
|
|
11
11
|
const command_audit_js_1 = require("../twin-audit/command-audit.js");
|
|
12
|
+
/* 구동은 원본을 지난다(ADR-0029 §8) — 트윈 런타임을 직접 잡지 않는다. */
|
|
13
|
+
const control_routing_js_1 = require("../reference/control-routing.js");
|
|
12
14
|
/*
|
|
13
15
|
* 트윈 Command·Scenario 채널 — 상향 mutation.
|
|
14
16
|
* Command: runtime.dispatch(행위). Scenario: runtime.scenario.load/start/pause/setSpeed(예측 자극).
|
|
@@ -53,43 +55,79 @@ let TwinControlMutation = class TwinControlMutation {
|
|
|
53
55
|
/* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */
|
|
54
56
|
return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams };
|
|
55
57
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* 구동 채널 — **원본을 지난다**(ADR-0029 §8).
|
|
60
|
+
*
|
|
61
|
+
* ── 무엇이 바뀌었나 (2026-08-19) ──────────────────────────────────────────
|
|
62
|
+
* 예전에는 이 문이 `TwinEngine.runtime(...).scenario` 를 **직접** 잡았다. 시뮬만 할 수 있는 일(자극·배속·
|
|
63
|
+
* 정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. 이제 길은 하나다:
|
|
64
|
+
* 트윈 → 그 트윈의 원본 → 어댑터가 **선언한** `control` 능력. 선언하지 않은 원본에는 문이 열리지 않고,
|
|
65
|
+
* 그 사실을 사유 코드로 말한다(`not-declared`) — 실 WMS 가 정지되지 않는 것은 결손이 아니다.
|
|
66
|
+
*
|
|
67
|
+
* 반환을 Boolean 에서 객체로 바꿨다: 예전에는 거절 사유를 실을 자리가 없어 **로그로만** 남겼다.
|
|
68
|
+
* 화면은 왜 안 되는지 알 수 없었다.
|
|
69
|
+
*/
|
|
70
|
+
async controlTwinSource(instanceId, action, context, scenario, speed) {
|
|
58
71
|
const domainId = context.state.domain?.id;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
if (!domainId)
|
|
73
|
+
return { ok: false, code: 'no-domain' };
|
|
74
|
+
const allowed = ['stimulus', 'start', 'pause', 'speed', 'reset'];
|
|
75
|
+
if (!allowed.includes(action))
|
|
76
|
+
return { ok: false, code: 'unsupported-action', detail: `one of ${allowed.join(' | ')}` };
|
|
63
77
|
/*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
78
|
+
* **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour` 를
|
|
79
|
+
* 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다). 규칙은 커널이
|
|
80
|
+
* 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.
|
|
66
81
|
*/
|
|
67
|
-
if (
|
|
68
|
-
|
|
82
|
+
if (action === 'stimulus') {
|
|
83
|
+
const v = (0, twin_kernel_1.validateScenario)(scenario);
|
|
84
|
+
if (v.ok === false)
|
|
85
|
+
return { ok: false, code: 'invalid', detail: v.errorCode, params: v.errorParams };
|
|
86
|
+
}
|
|
87
|
+
if (action === 'speed' && !(Number(speed) > 0))
|
|
88
|
+
return { ok: false, code: 'invalid', detail: 'speed must be greater than 0' };
|
|
69
89
|
/*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
90
|
+
* **구동도 사람이 한 일이다 — 남긴다** (ADR-0029 §1 결정 C, 2026-08-19).
|
|
91
|
+
*
|
|
92
|
+
* 이 문은 감사를 아예 남기지 않았다. 그런데 자극을 바꾸고 배속을 올리고 원본을 멈추는 것은 그 트윈의
|
|
93
|
+
* 사실을 바꾸는 행위다 — 커맨드 채널은 남기는데 이쪽이 비어 있으면, 나중에 「무엇이 이 공장을
|
|
94
|
+
* 흔들었나」에 답할 수 없다.
|
|
95
|
+
*
|
|
96
|
+
* 결정 C 그대로: 게이트는 여기 남고(위 `@privilege`), **원본의 답을 같은 행에 닫는다.** 답을 못 받으면
|
|
97
|
+
* `unknown` 이다 — 성공으로 적지 않는다.
|
|
73
98
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await (0, control_routing_js_1.routeControl)(domainId, instanceId, { action: action, scenario, speed: speed != null ? Number(speed) : undefined });
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
result = { ok: false, code: 'failed', detail: e?.message ?? 'control failed' };
|
|
81
105
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
try {
|
|
107
|
+
const audit = (0, command_audit_js_1.buildCommandAudit)({
|
|
108
|
+
/* 커맨드 채널과 같은 모양으로 적는다 — 감사를 읽는 쪽이 두 어휘를 배우지 않게. */
|
|
109
|
+
command: { type: `source.${action}`, origin: 'user', args: { ...(scenario ? { scenario } : {}), ...(speed != null ? { speed } : {}) } },
|
|
110
|
+
/* 게이트는 통과했다(여기까지 왔다) — 그 사실과 원본의 답은 다른 칸이다. */
|
|
111
|
+
ack: { accepted: true },
|
|
112
|
+
source: { answered: typeof result?.ok === 'boolean', ok: result?.ok, code: result?.code },
|
|
113
|
+
actor: context.state.user,
|
|
114
|
+
at: new Date()
|
|
115
|
+
});
|
|
116
|
+
const repo = (0, shell_2.getRepository)(twin_audit_event_js_1.TwinAuditEvent);
|
|
117
|
+
await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
/* 기록이 실패해도 결과는 돌려준다 — 다만 삼키지 않는다(「기록 없음」과 「일 없음」을 구별해야 한다). */
|
|
121
|
+
console.error(`[twin-control] source control audit not recorded for "${instanceId}"`, err);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/** 이 트윈의 원본이 **무엇을 받을 수 있나** — 화면이 버튼을 열기 전에 묻는 자리. */
|
|
126
|
+
async twinSourceCapability(instanceId, context) {
|
|
127
|
+
const domainId = context.state.domain?.id;
|
|
128
|
+
if (!domainId)
|
|
129
|
+
return { capabilities: [] };
|
|
130
|
+
return (0, control_routing_js_1.controlCapabilityOf)(domainId, instanceId);
|
|
93
131
|
}
|
|
94
132
|
};
|
|
95
133
|
exports.TwinControlMutation = TwinControlMutation;
|
|
@@ -105,15 +143,28 @@ tslib_1.__decorate([
|
|
|
105
143
|
], TwinControlMutation.prototype, "dispatchTwinCommand", null);
|
|
106
144
|
tslib_1.__decorate([
|
|
107
145
|
(0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
108
|
-
(0, type_graphql_1.Mutation)(returns =>
|
|
146
|
+
(0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, {
|
|
147
|
+
description: "Drive the source behind a twin: stimulus | start | pause | speed | reset. The command goes through the source adapter's declared control capability — a source that does not declare it refuses with code 'not-declared' (a real system is not paused or seeded, and that is a fact rather than a gap). Returns { ok, code?, detail? }."
|
|
148
|
+
}),
|
|
109
149
|
tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId')),
|
|
110
150
|
tslib_1.__param(1, (0, type_graphql_1.Arg)('action')),
|
|
111
151
|
tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
|
|
112
152
|
tslib_1.__param(3, (0, type_graphql_1.Arg)('scenario', type => shell_1.ScalarObject, { nullable: true })),
|
|
153
|
+
tslib_1.__param(4, (0, type_graphql_1.Arg)('speed', { nullable: true })),
|
|
154
|
+
tslib_1.__metadata("design:type", Function),
|
|
155
|
+
tslib_1.__metadata("design:paramtypes", [String, String, Object, Object, Number]),
|
|
156
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
157
|
+
], TwinControlMutation.prototype, "controlTwinSource", null);
|
|
158
|
+
tslib_1.__decorate([
|
|
159
|
+
(0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
|
|
160
|
+
description: "What the source behind this twin can do: its declared capabilities ('live', 'control'). Empty when the twin has no source or its adapter declares nothing."
|
|
161
|
+
}),
|
|
162
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId')),
|
|
163
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
113
164
|
tslib_1.__metadata("design:type", Function),
|
|
114
|
-
tslib_1.__metadata("design:paramtypes", [String,
|
|
165
|
+
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
115
166
|
tslib_1.__metadata("design:returntype", Promise)
|
|
116
|
-
], TwinControlMutation.prototype, "
|
|
167
|
+
], TwinControlMutation.prototype, "twinSourceCapability", null);
|
|
117
168
|
exports.TwinControlMutation = TwinControlMutation = tslib_1.__decorate([
|
|
118
169
|
(0, type_graphql_1.Resolver)()
|
|
119
170
|
], TwinControlMutation);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-control-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-control/twin-control-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Resolver, Directive} from 'type-graphql'\n\nimport { validateScenario } from '@operato/twin-kernel'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { TwinAuditEvent } from '../twin-audit/twin-audit-event.js'\nimport { buildCommandAudit } from '../twin-audit/command-audit.js'\n\n/*\n * 트윈 Command·Scenario 채널 — 상향 mutation.\n * Command: runtime.dispatch(행위). Scenario: runtime.scenario.load/start/pause/setSpeed(예측 자극).\n * (State 하향은 twin-state subscription. tick 은 워커.)\n *\n * 인가: data 구독 리졸버와 대칭으로, 대상 인스턴스가 **호출자 도메인 소속**인지 검증한다(cross-tenant 커맨드 주입 차단).\n * command 채널은 sim 상태를 변이하므로 도메인 스코프가 필수.\n */\n@Resolver()\nexport class TwinControlMutation {\n /** Command 채널 — 오더 hold/resume/release 등. 커널 dispatch 로 위임, ack{accepted,error} 반환. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'dispatch a twin command (order.hold/resume/release …). Returns {accepted, error?}.' })\n async dispatchTwinCommand(\n @Arg('instanceId') instanceId: string,\n @Arg('command', type => ScalarObject) command: any,\n @Ctx() context: ResolverContext\n ): Promise<{ accepted: boolean; error?: string; errorCode?: string; errorParams?: Record<string, string | number> }> {\n const domainId = context.state.domain?.id\n if (!domainId) return { accepted: false, error: 'no-domain', errorCode: 'no-domain' }\n /*\n * 라우팅은 엔진이 한다 — 예전에는 여기서 `inst.runtime.dispatch` 를 곧바로 불렀고, `runtime` 이\n * 시뮬에만 있어서 미러 트윈에서는 이 한 줄이 TypeError 로 터졌다(깔끔한 거절이 아니라 실패).\n * 권위(모드) × 성격(주석/액추에이션) 판정은 한 곳에만 둔다.\n */\n /* 원본을 그대로 감사에 넘긴다 — 무엇이 들어왔는지가 기록의 대상이다(정규화는 감사 모듈이 한다). */\n const cmdWithOrigin = command\n const ack = await TwinEngine.dispatchCommand(domainId, instanceId, command)\n /* 라이브 구조 변이(resource.add)는 저장 model 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */\n if (ack?.accepted && command?.type === 'resource.add') {\n await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => console.error('twin syncBoardEquipment fail', err))\n }\n /*\n * 감사 기록 — 승인 게이트가 앞을 막고, 이 줄이 뒤를 설명한다. 둘 다 있어야 \"사람 손을 거쳤다\" 를\n * 증명할 수 있다. 커널이 거절한 것도 남긴다 — 무엇을 시도했는지가 감사의 절반이다.\n *\n * 기록이 실패해도 커맨드 결과는 그대로 돌려준다. 감사가 본업을 막으면 안 되지만, 실패를 삼키지도\n * 않는다(로그에 남긴다) — 조용히 안 남으면 나중에 \"기록이 없다\" 와 \"일이 없었다\" 를 구분 못 한다.\n */\n try {\n const audit = buildCommandAudit({ command: cmdWithOrigin, ack, actor: context.state.user, at: new Date() })\n const repo = getRepository(TwinAuditEvent)\n await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))\n } catch (err) {\n console.error(`[twin-control] command audit not recorded for \"${instanceId}\"`, err)\n }\n\n /* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */\n return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams }\n }\n\n /** Scenario 채널 — 생성기 시나리오 제어(load/start/pause/setSpeed). */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'control twin scenario (load|start|pause|setSpeed)' })\n async controlTwinScenario(\n @Arg('instanceId') instanceId: string,\n @Arg('action') action: string,\n @Ctx() context: ResolverContext,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any\n ): Promise<boolean> {\n const domainId = context.state.domain?.id\n /* 테넌트 격리 — 레지스트리를 직접 색인하지 않는다(키에 도메인이 들어 있다). */\n if (!TwinEngine.owns(domainId, instanceId)) return false\n const runtime = TwinEngine.runtime(domainId, instanceId)\n /*\n * 자극 주입 가드 — mirror 트윈(외부 실물이 진실)은 커널 시나리오 엔진이 없다(startLive=projector, runtime 없음).\n * what-if 를 여기에 주입하면 현실을 오염시키므로 거부한다(sim-experiment/sim-world 만 자극 가능). silent throw 방지.\n */\n if (!runtime?.scenario) return false\n /*\n * **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour`\n * 를 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다).\n * 규칙은 커널이 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.\n */\n if (action === 'load') {\n const v = validateScenario(scenario)\n if (v.ok === false) {\n /* 사유를 로그로 남긴다 — 이 뮤테이션의 반환은 Boolean 계약이라 사유를 실어 보낼 자리가 없다. */\n console.warn(`[twin] scenario rejected for \"${instanceId}\": ${v.errorCode} ${JSON.stringify(v.errorParams ?? {})}`)\n return false\n }\n }\n if (action === 'load' && scenario) runtime.scenario.load(scenario)\n else if (action === 'start') runtime.scenario.start()\n else if (action === 'pause') runtime.scenario.pause()\n else if (action === 'setSpeed' && scenario?.speed != null) runtime.scenario.setSpeed(Number(scenario.speed))\n else return false\n return true\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-control-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-control/twin-control-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAA6E;AAE7E,sDAAuD;AAEvD,iDAAoD;AAEpD,iDAAqD;AAErD,oDAAkD;AAClD,2EAAkE;AAClE,qEAAkE;AAClE,mDAAmD;AACnD,wEAAmF;AAEnF;;;;;;;GAOG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,uFAAuF;IAGjF,AAAN,KAAK,CAAC,mBAAmB,CACJ,UAAkB,EACC,OAAY,EAC3C,OAAwB;QAE/B,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAA;QACzC,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,CAAA;QACrF;;;;WAIG;QACH,4DAA4D;QAC5D,MAAM,aAAa,GAAG,OAAO,CAAA;QAC7B,MAAM,GAAG,GAAG,MAAM,qBAAU,CAAC,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;QAC3E,0EAA0E;QAC1E,IAAI,GAAG,EAAE,QAAQ,IAAI,OAAO,EAAE,IAAI,KAAK,cAAc,EAAE,CAAC;YACtD,MAAM,qBAAU,CAAC,kBAAkB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC,CAAA;QAC5H,CAAC;QACD;;;;;;WAMG;QACH,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,IAAA,oCAAiB,EAAC,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAA;YAC3G,MAAM,IAAI,GAAG,IAAA,qBAAa,EAAC,oCAAc,CAAC,CAAA;YAC1C,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAA;QACtF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,kDAAkD,UAAU,GAAG,EAAE,GAAG,CAAC,CAAA;QACrF,CAAC;QAED,0EAA0E;QAC1E,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,CAAA;IACnH,CAAC;IAED;;;;;;;;;;;OAWG;IAMG,AAAN,KAAK,CAAC,iBAAiB,CACF,UAAkB,EACtB,MAAc,EACtB,OAAwB,EAC4B,QAAc,EACvC,KAAc;QAEhD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAA;QACzC,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;QACtD,MAAM,OAAO,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;QAChE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,UAAU,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAA;QACxH;;;;WAIG;QACH,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,IAAA,8BAAgB,EAAC,QAAQ,CAAC,CAAA;YACpC,IAAI,CAAC,CAAC,EAAE,KAAK,KAAK;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;QACvG,CAAC;QACD,IAAI,MAAM,KAAK,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAA;QAE7H;;;;;;;;;WASG;QACH,IAAI,MAAuD,CAAA;QAC3D,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAA,iCAAY,EAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,MAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAA;QAC1I,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,MAAM,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,IAAI,gBAAgB,EAAE,CAAA;QAChF,CAAC;QACD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,IAAA,oCAAiB,EAAC;gBAC9B,mDAAmD;gBACnD,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;gBACvI,+CAA+C;gBAC/C,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;gBACvB,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,MAAM,EAAE,EAAE,KAAK,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;gBACzF,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI;gBACzB,EAAE,EAAE,IAAI,IAAI,EAAE;aACf,CAAC,CAAA;YACF,MAAM,IAAI,GAAG,IAAA,qBAAa,EAAC,oCAAc,CAAC,CAAA;YAC1C,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAA;QACtF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+DAA+D;YAC/D,OAAO,CAAC,KAAK,CAAC,yDAAyD,UAAU,GAAG,EAAE,GAAG,CAAC,CAAA;QAC5F,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC;IAED,uDAAuD;IAIjD,AAAN,KAAK,CAAC,oBAAoB,CAAoB,UAAkB,EAAS,OAAwB;QAC/F,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAA;QACzC,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,CAAA;QAC1C,OAAO,IAAA,wCAAmB,EAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;IAClD,CAAC;CACF,CAAA;AA7HY,kDAAmB;AAIxB;IAFL,IAAA,wBAAS,EAAC,uGAAuG,CAAC;IAClH,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,WAAW,EAAE,oFAAoF,EAAE,CAAC;IAEtI,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IACjB,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,oBAAY,CAAC,CAAA;IACpC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;8DAiCP;AAmBK;IALL,IAAA,wBAAS,EAAC,uGAAuG,CAAC;IAClH,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QACjC,WAAW,EACT,yUAAyU;KAC5U,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IACjB,mBAAA,IAAA,kBAAG,EAAC,QAAQ,CAAC,CAAA;IACb,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACzD,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;4DAkDlC;AAMK;IAHL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,WAAW,EAAE,4JAA4J;KAC1K,CAAC;IAC0B,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IAAsB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;+DAIvE;8BA5HU,mBAAmB;IAD/B,IAAA,uBAAQ,GAAE;GACE,mBAAmB,CA6H/B","sourcesContent":["import { Arg, Ctx, Mutation, Query, Resolver, Directive } from 'type-graphql'\n\nimport { validateScenario } from '@operato/twin-kernel'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { TwinAuditEvent } from '../twin-audit/twin-audit-event.js'\nimport { buildCommandAudit } from '../twin-audit/command-audit.js'\n/* 구동은 원본을 지난다(ADR-0029 §8) — 트윈 런타임을 직접 잡지 않는다. */\nimport { routeControl, controlCapabilityOf } from '../reference/control-routing.js'\n\n/*\n * 트윈 Command·Scenario 채널 — 상향 mutation.\n * Command: runtime.dispatch(행위). Scenario: runtime.scenario.load/start/pause/setSpeed(예측 자극).\n * (State 하향은 twin-state subscription. tick 은 워커.)\n *\n * 인가: data 구독 리졸버와 대칭으로, 대상 인스턴스가 **호출자 도메인 소속**인지 검증한다(cross-tenant 커맨드 주입 차단).\n * command 채널은 sim 상태를 변이하므로 도메인 스코프가 필수.\n */\n@Resolver()\nexport class TwinControlMutation {\n /** Command 채널 — 오더 hold/resume/release 등. 커널 dispatch 로 위임, ack{accepted,error} 반환. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'dispatch a twin command (order.hold/resume/release …). Returns {accepted, error?}.' })\n async dispatchTwinCommand(\n @Arg('instanceId') instanceId: string,\n @Arg('command', type => ScalarObject) command: any,\n @Ctx() context: ResolverContext\n ): Promise<{ accepted: boolean; error?: string; errorCode?: string; errorParams?: Record<string, string | number> }> {\n const domainId = context.state.domain?.id\n if (!domainId) return { accepted: false, error: 'no-domain', errorCode: 'no-domain' }\n /*\n * 라우팅은 엔진이 한다 — 예전에는 여기서 `inst.runtime.dispatch` 를 곧바로 불렀고, `runtime` 이\n * 시뮬에만 있어서 미러 트윈에서는 이 한 줄이 TypeError 로 터졌다(깔끔한 거절이 아니라 실패).\n * 권위(모드) × 성격(주석/액추에이션) 판정은 한 곳에만 둔다.\n */\n /* 원본을 그대로 감사에 넘긴다 — 무엇이 들어왔는지가 기록의 대상이다(정규화는 감사 모듈이 한다). */\n const cmdWithOrigin = command\n const ack = await TwinEngine.dispatchCommand(domainId, instanceId, command)\n /* 라이브 구조 변이(resource.add)는 저장 model 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */\n if (ack?.accepted && command?.type === 'resource.add') {\n await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => console.error('twin syncBoardEquipment fail', err))\n }\n /*\n * 감사 기록 — 승인 게이트가 앞을 막고, 이 줄이 뒤를 설명한다. 둘 다 있어야 \"사람 손을 거쳤다\" 를\n * 증명할 수 있다. 커널이 거절한 것도 남긴다 — 무엇을 시도했는지가 감사의 절반이다.\n *\n * 기록이 실패해도 커맨드 결과는 그대로 돌려준다. 감사가 본업을 막으면 안 되지만, 실패를 삼키지도\n * 않는다(로그에 남긴다) — 조용히 안 남으면 나중에 \"기록이 없다\" 와 \"일이 없었다\" 를 구분 못 한다.\n */\n try {\n const audit = buildCommandAudit({ command: cmdWithOrigin, ack, actor: context.state.user, at: new Date() })\n const repo = getRepository(TwinAuditEvent)\n await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))\n } catch (err) {\n console.error(`[twin-control] command audit not recorded for \"${instanceId}\"`, err)\n }\n\n /* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */\n return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams }\n }\n\n /**\n * 구동 채널 — **원본을 지난다**(ADR-0029 §8).\n *\n * ── 무엇이 바뀌었나 (2026-08-19) ──────────────────────────────────────────\n * 예전에는 이 문이 `TwinEngine.runtime(...).scenario` 를 **직접** 잡았다. 시뮬만 할 수 있는 일(자극·배속·\n * 정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. 이제 길은 하나다:\n * 트윈 → 그 트윈의 원본 → 어댑터가 **선언한** `control` 능력. 선언하지 않은 원본에는 문이 열리지 않고,\n * 그 사실을 사유 코드로 말한다(`not-declared`) — 실 WMS 가 정지되지 않는 것은 결손이 아니다.\n *\n * 반환을 Boolean 에서 객체로 바꿨다: 예전에는 거절 사유를 실을 자리가 없어 **로그로만** 남겼다.\n * 화면은 왜 안 되는지 알 수 없었다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n \"Drive the source behind a twin: stimulus | start | pause | speed | reset. The command goes through the source adapter's declared control capability — a source that does not declare it refuses with code 'not-declared' (a real system is not paused or seeded, and that is a fact rather than a gap). Returns { ok, code?, detail? }.\"\n })\n async controlTwinSource(\n @Arg('instanceId') instanceId: string,\n @Arg('action') action: string,\n @Ctx() context: ResolverContext,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any,\n @Arg('speed', { nullable: true }) speed?: number\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { ok: false, code: 'no-domain' }\n const allowed = ['stimulus', 'start', 'pause', 'speed', 'reset']\n if (!allowed.includes(action)) return { ok: false, code: 'unsupported-action', detail: `one of ${allowed.join(' | ')}` }\n /*\n * **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour` 를\n * 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다). 규칙은 커널이\n * 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.\n */\n if (action === 'stimulus') {\n const v = validateScenario(scenario)\n if (v.ok === false) return { ok: false, code: 'invalid', detail: v.errorCode, params: v.errorParams }\n }\n if (action === 'speed' && !(Number(speed) > 0)) return { ok: false, code: 'invalid', detail: 'speed must be greater than 0' }\n\n /*\n * **구동도 사람이 한 일이다 — 남긴다** (ADR-0029 §1 결정 C, 2026-08-19).\n *\n * 이 문은 감사를 아예 남기지 않았다. 그런데 자극을 바꾸고 배속을 올리고 원본을 멈추는 것은 그 트윈의\n * 사실을 바꾸는 행위다 — 커맨드 채널은 남기는데 이쪽이 비어 있으면, 나중에 「무엇이 이 공장을\n * 흔들었나」에 답할 수 없다.\n *\n * 결정 C 그대로: 게이트는 여기 남고(위 `@privilege`), **원본의 답을 같은 행에 닫는다.** 답을 못 받으면\n * `unknown` 이다 — 성공으로 적지 않는다.\n */\n let result: { ok: boolean; code?: string; detail?: string }\n try {\n result = await routeControl(domainId, instanceId, { action: action as any, scenario, speed: speed != null ? Number(speed) : undefined })\n } catch (e: any) {\n result = { ok: false, code: 'failed', detail: e?.message ?? 'control failed' }\n }\n try {\n const audit = buildCommandAudit({\n /* 커맨드 채널과 같은 모양으로 적는다 — 감사를 읽는 쪽이 두 어휘를 배우지 않게. */\n command: { type: `source.${action}`, origin: 'user', args: { ...(scenario ? { scenario } : {}), ...(speed != null ? { speed } : {}) } },\n /* 게이트는 통과했다(여기까지 왔다) — 그 사실과 원본의 답은 다른 칸이다. */\n ack: { accepted: true },\n source: { answered: typeof result?.ok === 'boolean', ok: result?.ok, code: result?.code },\n actor: context.state.user,\n at: new Date()\n })\n const repo = getRepository(TwinAuditEvent)\n await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))\n } catch (err) {\n /* 기록이 실패해도 결과는 돌려준다 — 다만 삼키지 않는다(「기록 없음」과 「일 없음」을 구별해야 한다). */\n console.error(`[twin-control] source control audit not recorded for \"${instanceId}\"`, err)\n }\n return result\n }\n\n /** 이 트윈의 원본이 **무엇을 받을 수 있나** — 화면이 버튼을 열기 전에 묻는 자리. */\n @Query(returns => ScalarObject, {\n description: \"What the source behind this twin can do: its declared capabilities ('live', 'control'). Empty when the twin has no source or its adapter declares nothing.\"\n })\n async twinSourceCapability(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { capabilities: [] }\n return controlCapabilityOf(domainId, instanceId)\n }\n}\n"]}
|
|
@@ -53,9 +53,9 @@ tslib_1.__decorate([
|
|
|
53
53
|
], TwinInstance.prototype, "kind", void 0);
|
|
54
54
|
tslib_1.__decorate([
|
|
55
55
|
(0, typeorm_1.Column)(),
|
|
56
|
-
(0, type_graphql_1.Field)({ description: "
|
|
56
|
+
(0, type_graphql_1.Field)({ description: "Restart policy: 'resync' (read again from the external source) | 'resume' (continue this twin's own journal) | 'reset' (wipe and replay from the seed). No default — an unspecified policy is refused." }),
|
|
57
57
|
tslib_1.__metadata("design:type", String)
|
|
58
|
-
], TwinInstance.prototype, "
|
|
58
|
+
], TwinInstance.prototype, "restartPolicy", void 0);
|
|
59
59
|
tslib_1.__decorate([
|
|
60
60
|
(0, typeorm_1.Column)({ type: 'simple-json', nullable: true, transformer: model_vocabulary_js_1.modelColumnTransformer }),
|
|
61
61
|
(0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-instance.js","sourceRoot":"","sources":["../../../server/service/twin-instance/twin-instance.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { modelColumnTransformer } from '../../engine/model-vocabulary.js'\n\n/*\n * TwinInstance — 트윈 인스턴스 레지스트리(재부팅 복구의 앵커).\n * 복구(DB→replay)는 model(마스터 청사진)가 있어야 이벤트를 상태로 다시 계산할 수 있다 → model·kind 를 영속.\n * 이벤트(TwinEvent)와 달리 가변(status 갱신) — 인스턴스의 \"현재 등록 상태\".\n * model 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).\n * (CLAUDE.md: 모든 @ObjectType/@Field 는 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_instance_0', (e: TwinInstance) => [e.domain, e.instanceId], { unique: true })\n@Index('ix_twin_instance_1', (e: TwinInstance) => [e.domain, e.spaceId]) // 공간→인스턴스 1급 질의(공간 중심 존재론)\n@ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (model + kind).' })\nexport class TwinInstance {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the registry record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinInstance) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance id (unique within a domain).' })\n instanceId: string\n\n /**\n * **사람이 부르는 이름** — 업무키(`instanceId`)와 나란히 두는 표시명(프레임워크 엔티티 규약).\n *\n * ── 왜 업무키와 이름을 나눠 두나 ─────────────────────────────────────────────\n * `instanceId` 는 URL·저널·구독이 전부 쓰는 **계약**이라 바꿀 수 없다. 반대로 이름은 바뀌고,\n * 중복되고(사이트 둘에 \"도장\" 이 있는 것이 정상), 언어마다 다르다. 둘을 한 컬럼에 담으면 개명이\n * 곧 다른 트윈이 되고 재동기가 끊긴다 — 그래서 나눈다(이 저장소에서 식별자가 밖에서 오는 것은\n * 전부 이 규약을 쓴다: `spaceId`·`locationId`·`equipmentId`·`operationKey`).\n *\n * 나눴으면 **이름을 반드시 함께 둬야 한다.** 이 자리가 없던 동안 사이트 이름은 model 안에만 있어서\n * 목록이 `crew-two` 만 보였다 — 나누기만 하고 절반을 빼먹으면 화면에는 id 만 남는다.\n *\n * 원본이 이름을 말하지 않으면 비운다(id 를 베끼지 않는다).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Human name of this twin (the site name from its master). Empty when the source does not name it — never copied from instanceId.' })\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Longer description when the source provides one.' })\n description?: string\n\n /* 어느 도메인 커널이 이 트윈을 운영하는가. 커널 없는 트윈은 없으므로 비어 있을 수 없다\n (`TwinEngine.start`·`register` 모두 필수 인자로 받는다). */\n @Column()\n @Field({ description: 'Domain kernel kind: wms | yms | mes.' })\n kind: string\n\n /*\n * realityMode — 이 트윈의 현실이 어디서 오는가(runtime-state-model §0·§1 \"지속 현실 하나 + 여러 렌즈\"의 ① 선언).\n * 'mirror' = 현실은 외부 실물 시스템. 트윈=순수 렌즈, 재기동 시 실물서 재동기(startLive).\n * 'sim-world' = 트윈이 낳은 타임라인이 곧 진실이며 이어진다 → 재기동 시 저널서 resume(현실 지속).\n * 'sim-experiment' = seed 재현형. 재기동 = 같은 우주 재실행(저널 reset, revision 0) — 백지 시작이 버그가 아니라 선언된 거동.\n *\n * **비어 있을 수 없다.** 이 값은 재기동 거동을 구분하는 선언이고, 없으면 읽는 쪽마다 각자\n * 기본값을 고르게 된다 — 같은 트윈이 부르는 자리에 따라 다르게 재기동하는 길이다.\n * 선언을 생략한 프로비저닝은 `register` 가 `DEFAULT_REALITY_MODE` 로 **각인해서 저장한다**.\n * 즉 \"미선언\" 이라는 상태 자체를 저장소에 두지 않는다.\n */\n @Column()\n @Field({ description: \"Reality source declaration: 'mirror' (external system is truth, resync on reboot) | 'sim-world' (generated timeline is truth and persists, resume on reboot) | 'sim-experiment' (seed-reproducible, resets on reboot).\" })\n realityMode: string\n\n /* 읽을 때 어휘 세대를 흡수한다(`nodes`/`movers`/`equipmentList` → `locations`/`equipment`). (vocabulary-guard: allow — 세대 이름 없이는 설명할 수 없다.)\n 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠지고, 빠진 것은 오류가 아니라 **빈 공장**이라\n 조용하다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/model-vocabulary.ts`. */\n @Column({ type: 'simple-json', nullable: true, transformer: modelColumnTransformer })\n @Field(type => ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)\n model?: any\n\n /*\n * 로컬 선언 겹 — **구조는 원천이, 수는 우리가.**\n *\n * 요금 단가·계약 정보·축전지 충전율처럼 원 시스템이 모르는 값을 넣는 자리다. `model` 에 직접 쓰지\n * 않는 이유는 그것이 **원천의 사본**이기 때문이다: 다시 읽기가 사본을 새로 그리면 직접 쓴 값은 조용히\n * 사라진다(요금이 사라지면 금액도 사라지고, 사용자는 이유를 알 수 없다).\n *\n * 그래서 여기 남기고, 모델을 그릴 때마다 그 위에 얹는다(`engine/local-declarations`). 얹힌 값에는\n * `source: 'local'` 이 붙는다 — 커널 계약의 `PropertySource` 축(`reference` ↔ `local`)이고, 「밖에서\n * 참조해 온 값이냐, 이 안에서 정한 값이냐」에 답한다.\n *\n * **이름에 장소 낱말을 쓰지 않는다**(옛 이름 `siteDeclarations`): 이 코드에서 `site` 는 **외부\n * 시스템이 부르는 사업장**이고(SAP `P3`·Oracle `W01` 이 같은 현장), 여기 담기는 것은 그 외부 시스템이\n * **모르는** 값이다 — 이름이 내용과 정반대였다. `space` 도 아니다: 그것은 「어디」에 답하고 이 축은\n * 「누가 말했나」에 답한다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: \"Locally-declared values layered on top of the reference-derived model (tariff, contract, state of charge) — values whose authority sits here, not in the source system. Kept apart from `model` so a resync cannot silently drop them.\" })\n localDeclarations?: any\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from model.spaceId — first-class \"space → its twins\" query (space-centered ontology).' })\n spaceId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from model.areaId.' })\n areaId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Lifecycle status: running | stopped.' })\n status?: string\n\n /*\n * purpose — 이 인스턴스가 운영 트윈인지, 격리된 벤치 사본인지 1급 선언(이름 접두사가 아니라 속성으로 구별).\n * 'bench' 는 규모/부하 실험용 격리 사본 — 목록·정리·안전 가드가 이 속성을 키로 동작(운영 트윈 무오염 보장).\n *\n * **비어 있을 수 없다.** 프로비저닝이 `register` 에서 'operational' 로 각인하고, 벤치 사본만\n * `setPurpose` 로 뒤집는다. 예전에는 비워 둔 채 읽는 쪽에서 메웠는데, 그러면 \"운영이라고 선언된 것\"\n * 과 \"아무도 말하지 않은 것\" 이 같은 값으로 보여서 벤치 격리가 추측에 기대게 된다.\n */\n @Column()\n @Field({ description: \"Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments).\" })\n purpose: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'For a bench copy: the source spaceId it was copied from (provenance).' })\n copyOf?: string\n\n /**\n * **이 트윈을 다시 읽는 방법** — 어떤 원본에서 나왔나(`MasterOrigin`).\n *\n * 트윈은 원본을 비출 뿐이므로(ADR-0028) 원본이 달라지면 다시 읽어야 한다. 그런데 이 자리가 없던\n * 동안 트윈은 **자기가 무엇에서 나왔는지 몰랐다** — 어댑터로 들어온 트윈은 `TwinReference` 행이\n * 있었지만 템플릿으로 세운 트윈에는 아무 표식도 없었고, 그래서 첫날의 모습에 영원히 머무는\n * 트윈이 만들어졌다. 원천이 무엇이든 여기 한 자리에 남긴다.\n *\n * **비어 있을 수 있다** — 이 자리가 생기기 전에 만들어진 트윈이 그렇다. 그때는 \"다시 읽을 수\n * 없다\" 가 사실이므로 화면이 그대로 말한다(짐작해 채우지 않는다).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, {\n nullable: true,\n description:\n \"How this twin's master can be produced again — {kind:'reference', source} or {kind:'template', templateId, params}. Absent means this twin cannot be re-read from its source.\"\n })\n origin?: any\n\n /*\n * `revision` 컬럼은 **없앴다**(2026-08-08).\n *\n * `ADR-0015` 가 \"정의만 있고 write 안 됨 — 프로비저닝에서 최신 저널 revision 을 반영하도록\n * 채운다\" 고 적어 두었는데, 채우지 않는 편이 맞다. 저널의 최신 번호는 **저널이 아는 사실**이고\n * (`TwinEngine.list` 가 `MAX(revision)` 으로 낸다), 여기 복제하면 **같은 사실의 집이 둘**이 된다.\n * 이 세션에만 그 부류를 셋 겪었다(`model.operations` 두 집 · `materials` 두 집 · tenantId 중복).\n *\n * 복제한 값은 반드시 어긋난다 — 저널은 틱마다 늘고 이 컬럼은 저장할 때만 갱신되므로, 화면이\n * 어느 쪽을 읽느냐로 다른 숫자가 나온다. 그 조용한 어긋남을 만들 이유가 없다.\n */\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance was first registered.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance registry row was last updated.' })\n updatedAt?: Date\n\n /*\n * 누가 만들고 누가 고쳤나 — **이 행은 사람이 만든다**(프로비저닝은 사용자 행위)이므로 규약의\n * 풀세트를 갖는다. 투영 행(자리·설비·공정)은 반대로 인제스트가 그린 것이라 저자가 사람이 아니고,\n * 그 저자는 `sourceRef`+`ingestedAt` 이 이미 말한다 — 그래서 그쪽에는 두지 않는다.\n *\n * 비어 있을 수 있다: 부팅 자동 프로비저닝처럼 **사람이 없는 경로**로 만들어진 행이 그렇다.\n * 그때 아무 사용자를 적어 넣으면 감사 기록이 거짓이 된다.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who first provisioned this twin. Empty when it was created by a system path (boot provisioning).' })\n creator?: User\n\n @RelationId((e: TwinInstance) => e.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who last changed this twin registry row.' })\n updater?: User\n\n @RelationId((e: TwinInstance) => e.updater)\n updaterId?: string\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-instance.js","sourceRoot":"","sources":["../../../server/service/twin-instance/twin-instance.ts"],"names":[],"mappings":";;;;AAAA,qCAAkI;AAClI,+CAAoD;AAEpD,iDAA4D;AAC5D,yDAAgD;AAChD,0EAAyE;AAEzE;;;;;;GAMG;AAKI,IAAM,YAAY,GAAlB,MAAM,YAAY;CAoLxB,CAAA;AApLY,oCAAY;AAGd;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;;wCAC7D;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC;sCACvE,cAAM;4CAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,CAAe,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;;8CACzB;AAIjB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,oDAAoD,EAAE,CAAC;;gDAC3D;AAkBlB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iIAAiI,EAAE,CAAC;;0CAC7J;AAIb;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kDAAkD,EAAE,CAAC;;iDACvE;AAMpB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,sCAAsC,EAAE,CAAC;;0CACnD;AAqBZ;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,wMAAwM,EAAE,CAAC;;mDAC5M;AAOrB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4CAAsB,EAAE,CAAC;IACpF,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,sMAAsM,EAAE,CAAC,CAAC,yEAAyE;;;2CACpU;AAoBX;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wOAAwO,EAAE,CAAC;;uDAChR;AAIvB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,sIAAsI,EAAE,CAAC;;6CAC/J;AAIhB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+EAA+E,EAAE,CAAC;;4CACzG;AAIf;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,sCAAsC,EAAE,CAAC;;4CAChE;AAYf;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,mGAAmG,EAAE,CAAC;;6CAC7G;AAIf;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uEAAuE,EAAE,CAAC;;4CACjG;AAmBf;IANC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC3B,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,+KAA+K;KAClL,CAAC;;4CACU;AAgBZ;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;sCAChF,IAAI;+CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4DAA4D,EAAE,CAAC;sCACzF,IAAI;IAEhB;;;;;;;OAOG;;+CATa;AAYhB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2GAA2G,EAAE,CAAC;sCACxJ,gBAAI;6CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,CAAe,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;;+CACzB;AAIlB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;sCAChG,gBAAI;6CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,CAAe,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;;+CACzB;uBAnLP,YAAY;IAJxB,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,oBAAoB,EAAE,CAAC,CAAe,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC5F,IAAA,eAAK,EAAC,oBAAoB,EAAE,CAAC,CAAe,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,2BAA2B;;IACnG,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,sFAAsF,EAAE,CAAC;GACvG,YAAY,CAoLxB","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { modelColumnTransformer } from '../../engine/model-vocabulary.js'\n\n/*\n * TwinInstance — 트윈 인스턴스 레지스트리(재부팅 복구의 앵커).\n * 복구(DB→replay)는 model(마스터 청사진)가 있어야 이벤트를 상태로 다시 계산할 수 있다 → model·kind 를 영속.\n * 이벤트(TwinEvent)와 달리 가변(status 갱신) — 인스턴스의 \"현재 등록 상태\".\n * model 는 simple-json(멀티DB 이식 — postgres/mysql/sqlite/mssql/oracle 공통).\n * (CLAUDE.md: 모든 @ObjectType/@Field 는 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_instance_0', (e: TwinInstance) => [e.domain, e.instanceId], { unique: true })\n@Index('ix_twin_instance_1', (e: TwinInstance) => [e.domain, e.spaceId]) // 공간→인스턴스 1급 질의(공간 중심 존재론)\n@ObjectType({ description: 'Registry of a hosted twin runtime instance — anchors reboot recovery (model + kind).' })\nexport class TwinInstance {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the registry record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinInstance) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance id (unique within a domain).' })\n instanceId: string\n\n /**\n * **사람이 부르는 이름** — 업무키(`instanceId`)와 나란히 두는 표시명(프레임워크 엔티티 규약).\n *\n * ── 왜 업무키와 이름을 나눠 두나 ─────────────────────────────────────────────\n * `instanceId` 는 URL·저널·구독이 전부 쓰는 **계약**이라 바꿀 수 없다. 반대로 이름은 바뀌고,\n * 중복되고(사이트 둘에 \"도장\" 이 있는 것이 정상), 언어마다 다르다. 둘을 한 컬럼에 담으면 개명이\n * 곧 다른 트윈이 되고 재동기가 끊긴다 — 그래서 나눈다(이 저장소에서 식별자가 밖에서 오는 것은\n * 전부 이 규약을 쓴다: `spaceId`·`locationId`·`equipmentId`·`operationKey`).\n *\n * 나눴으면 **이름을 반드시 함께 둬야 한다.** 이 자리가 없던 동안 사이트 이름은 model 안에만 있어서\n * 목록이 `crew-two` 만 보였다 — 나누기만 하고 절반을 빼먹으면 화면에는 id 만 남는다.\n *\n * 원본이 이름을 말하지 않으면 비운다(id 를 베끼지 않는다).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Human name of this twin (the site name from its master). Empty when the source does not name it — never copied from instanceId.' })\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Longer description when the source provides one.' })\n description?: string\n\n /* 어느 도메인 커널이 이 트윈을 운영하는가. 커널 없는 트윈은 없으므로 비어 있을 수 없다\n (`TwinEngine.start`·`register` 모두 필수 인자로 받는다). */\n @Column()\n @Field({ description: 'Domain kernel kind: wms | yms | mes.' })\n kind: string\n\n /*\n * **재기동 정책** — 이 트윈은 다시 세워질 때 자기 과거를 어떻게 대하나(ADR-0029 §2).\n *\n * 'resync' = 외부 실물에서 다시 읽는다(원본이 진실이므로 우리 과거를 이어 붙이지 않는다 → startLive).\n * 'resume' = 저널을 이어 세운다(이 트윈이 낳은 타임라인이 곧 사실이다).\n * 'reset' = 저널을 비우고 씨앗부터 다시 돌린다(재현 가능한 실험 — 백지 시작이 선언된 거동이다).\n *\n * ── 왜 트윈에 남았나 (옛 이름 `realityMode`) ────────────────────────────────\n * 「현실이 어디서 오나」는 ADR-0029 가 **원본의 종류**로 옮긴 물음이다(`TwinReference.adapterType`) —\n * 트윈은 한 종류이고 언제나 원본을 반영한다. 그런데 코드가 이 축으로 실제로 가른 것은 **재기동 거동\n * 하나**였고, 그것은 원본의 성질이 아니라 **트윈과 자기 과거의 관계**다: 같은 시뮬레이터 원본을 보는 두\n * 트윈이 하나는 이어가고 하나는 매번 처음부터 재현할 수 있다. 그래서 축은 남고 이름만 바뀌었다.\n *\n * **비어 있을 수 없고, 기본값도 없다.** 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로\n * 떨어졌다 — 값이 하나 어긋나는 순간 이력을 지우는 길이다. 이제 읽는 쪽이 **던진다**\n * (`engine/restart-policy.ts`). 선언은 프로비저닝하는 쪽의 몫이다.\n */\n @Column()\n @Field({ description: \"Restart policy: 'resync' (read again from the external source) | 'resume' (continue this twin's own journal) | 'reset' (wipe and replay from the seed). No default — an unspecified policy is refused.\" })\n restartPolicy: string\n\n /* 읽을 때 어휘 세대를 흡수한다(`nodes`/`movers`/`equipmentList` → `locations`/`equipment`). (vocabulary-guard: allow — 세대 이름 없이는 설명할 수 없다.)\n 자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠지고, 빠진 것은 오류가 아니라 **빈 공장**이라\n 조용하다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/model-vocabulary.ts`. */\n @Column({ type: 'simple-json', nullable: true, transformer: modelColumnTransformer })\n @Field(type => ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)\n model?: any\n\n /*\n * 로컬 선언 겹 — **구조는 원천이, 수는 우리가.**\n *\n * 요금 단가·계약 정보·축전지 충전율처럼 원 시스템이 모르는 값을 넣는 자리다. `model` 에 직접 쓰지\n * 않는 이유는 그것이 **원천의 사본**이기 때문이다: 다시 읽기가 사본을 새로 그리면 직접 쓴 값은 조용히\n * 사라진다(요금이 사라지면 금액도 사라지고, 사용자는 이유를 알 수 없다).\n *\n * 그래서 여기 남기고, 모델을 그릴 때마다 그 위에 얹는다(`engine/local-declarations`). 얹힌 값에는\n * `source: 'local'` 이 붙는다 — 커널 계약의 `PropertySource` 축(`reference` ↔ `local`)이고, 「밖에서\n * 참조해 온 값이냐, 이 안에서 정한 값이냐」에 답한다.\n *\n * **이름에 장소 낱말을 쓰지 않는다**(옛 이름 `siteDeclarations`): 이 코드에서 `site` 는 **외부\n * 시스템이 부르는 사업장**이고(SAP `P3`·Oracle `W01` 이 같은 현장), 여기 담기는 것은 그 외부 시스템이\n * **모르는** 값이다 — 이름이 내용과 정반대였다. `space` 도 아니다: 그것은 「어디」에 답하고 이 축은\n * 「누가 말했나」에 답한다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: \"Locally-declared values layered on top of the reference-derived model (tariff, contract, state of charge) — values whose authority sits here, not in the source system. Kept apart from `model` so a resync cannot silently drop them.\" })\n localDeclarations?: any\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning space id (co-location anchor). Promoted from model.spaceId — first-class \"space → its twins\" query (space-centered ontology).' })\n spaceId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Owning area id within the space (optional scope). Promoted from model.areaId.' })\n areaId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Lifecycle status: running | stopped.' })\n status?: string\n\n /*\n * purpose — 이 인스턴스가 운영 트윈인지, 격리된 벤치 사본인지 1급 선언(이름 접두사가 아니라 속성으로 구별).\n * 'bench' 는 규모/부하 실험용 격리 사본 — 목록·정리·안전 가드가 이 속성을 키로 동작(운영 트윈 무오염 보장).\n *\n * **비어 있을 수 없다.** 프로비저닝이 `register` 에서 'operational' 로 각인하고, 벤치 사본만\n * `setPurpose` 로 뒤집는다. 예전에는 비워 둔 채 읽는 쪽에서 메웠는데, 그러면 \"운영이라고 선언된 것\"\n * 과 \"아무도 말하지 않은 것\" 이 같은 값으로 보여서 벤치 격리가 추측에 기대게 된다.\n */\n @Column()\n @Field({ description: \"Instance purpose: 'operational' (real twin) | 'bench' (isolated copy for scale/load experiments).\" })\n purpose: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'For a bench copy: the source spaceId it was copied from (provenance).' })\n copyOf?: string\n\n /**\n * **이 트윈을 다시 읽는 방법** — 어떤 원본에서 나왔나(`MasterOrigin`).\n *\n * 트윈은 원본을 비출 뿐이므로(ADR-0028) 원본이 달라지면 다시 읽어야 한다. 그런데 이 자리가 없던\n * 동안 트윈은 **자기가 무엇에서 나왔는지 몰랐다** — 어댑터로 들어온 트윈은 `TwinReference` 행이\n * 있었지만 템플릿으로 세운 트윈에는 아무 표식도 없었고, 그래서 첫날의 모습에 영원히 머무는\n * 트윈이 만들어졌다. 원천이 무엇이든 여기 한 자리에 남긴다.\n *\n * **비어 있을 수 있다** — 이 자리가 생기기 전에 만들어진 트윈이 그렇다. 그때는 \"다시 읽을 수\n * 없다\" 가 사실이므로 화면이 그대로 말한다(짐작해 채우지 않는다).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, {\n nullable: true,\n description:\n \"How this twin's master can be produced again — {kind:'reference', source} or {kind:'template', templateId, params}. Absent means this twin cannot be re-read from its source.\"\n })\n origin?: any\n\n /*\n * `revision` 컬럼은 **없앴다**(2026-08-08).\n *\n * `ADR-0015` 가 \"정의만 있고 write 안 됨 — 프로비저닝에서 최신 저널 revision 을 반영하도록\n * 채운다\" 고 적어 두었는데, 채우지 않는 편이 맞다. 저널의 최신 번호는 **저널이 아는 사실**이고\n * (`TwinEngine.list` 가 `MAX(revision)` 으로 낸다), 여기 복제하면 **같은 사실의 집이 둘**이 된다.\n * 이 세션에만 그 부류를 셋 겪었다(`model.operations` 두 집 · `materials` 두 집 · tenantId 중복).\n *\n * 복제한 값은 반드시 어긋난다 — 저널은 틱마다 늘고 이 컬럼은 저장할 때만 갱신되므로, 화면이\n * 어느 쪽을 읽느냐로 다른 숫자가 나온다. 그 조용한 어긋남을 만들 이유가 없다.\n */\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance was first registered.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the instance registry row was last updated.' })\n updatedAt?: Date\n\n /*\n * 누가 만들고 누가 고쳤나 — **이 행은 사람이 만든다**(프로비저닝은 사용자 행위)이므로 규약의\n * 풀세트를 갖는다. 투영 행(자리·설비·공정)은 반대로 인제스트가 그린 것이라 저자가 사람이 아니고,\n * 그 저자는 `sourceRef`+`ingestedAt` 이 이미 말한다 — 그래서 그쪽에는 두지 않는다.\n *\n * 비어 있을 수 있다: 부팅 자동 프로비저닝처럼 **사람이 없는 경로**로 만들어진 행이 그렇다.\n * 그때 아무 사용자를 적어 넣으면 감사 기록이 거짓이 된다.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who first provisioned this twin. Empty when it was created by a system path (boot provisioning).' })\n creator?: User\n\n @RelationId((e: TwinInstance) => e.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who last changed this twin registry row.' })\n updater?: User\n\n @RelationId((e: TwinInstance) => e.updater)\n updaterId?: string\n}\n"]}
|
|
@@ -12,7 +12,8 @@ const domain_catalog_js_1 = require("./domain-catalog.js");
|
|
|
12
12
|
* 트윈 인스턴스 생명주기 채널 — 프로비저닝(상향, ADR-0015).
|
|
13
13
|
* State/Command/Scenario 는 이미 running 인 인스턴스를 다루지만, 이 채널은 인스턴스 자체를 만들고/설정하고/없앤다.
|
|
14
14
|
* 생명주기: provision(save, 미기동) → start → stop → delete. 모두 domain 스코프(context.state.domain.id).
|
|
15
|
-
* model(TwinModelDef+layout)=구조, scenario=자극 은 분리 — 여기선 구조만 세운다. 자극은
|
|
15
|
+
* model(TwinModelDef+layout)=구조, scenario=자극 은 분리 — 여기선 구조만 세운다. 자극은 **원본이 선언하고**
|
|
16
|
+
* (`TwinReference.connectionConfig.scenario`) 기동이 싣거나, 원본의 구동 능력으로 태운다(ADR-0029 §4·§8).
|
|
16
17
|
*/
|
|
17
18
|
let TwinLifecycleMutation = class TwinLifecycleMutation {
|
|
18
19
|
/*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-lifecycle-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-lifecycle/twin-lifecycle-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAA4E;AAE5E,iDAAoD;AAEpD,oDAAkD;AAIlD;0EAC0E;AAC1E,2DAAmD;AAEnD
|
|
1
|
+
{"version":3,"file":"twin-lifecycle-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-lifecycle/twin-lifecycle-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Query, Resolver, Directive} from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\n\nimport type { TwinModelDef } from '@operato/twin-kernel'\n\n/* 도메인 어휘 카탈로그(시스템별 노드 타입) — 커널이 SSOT. 앱/UI 는 이걸 소싱하고 재선언하지 않는다(방언 금지).\n * 조합은 domain-catalog.ts 가 소유 — 리졸버와 서버측 소비자(board-ai 도구 등)가 같은 형태를 본다. */\nimport { domainCatalog } from './domain-catalog.js'\n\n/*\n * 트윈 인스턴스 생명주기 채널 — 프로비저닝(상향, ADR-0015).\n * State/Command/Scenario 는 이미 running 인 인스턴스를 다루지만, 이 채널은 인스턴스 자체를 만들고/설정하고/없앤다.\n * 생명주기: provision(save, 미기동) → start → stop → delete. 모두 domain 스코프(context.state.domain.id).\n * model(TwinModelDef+layout)=구조, scenario=자극 은 분리 — 여기선 구조만 세운다. 자극은 **원본이 선언하고**\n * (`TwinReference.connectionConfig.scenario`) 기동이 싣거나, 원본의 구동 능력으로 태운다(ADR-0029 §4·§8).\n */\n@Resolver()\nexport class TwinLifecycleMutation {\n /*\n * `saveTwinInstance` 는 **없앴다**(2026-08-08, ADR-0028).\n *\n * 트윈의 구조를 임의의 model 로 덮어쓰는 문이었고, 유일한 소비자는 손저작 편집기였다.\n * 구조는 원본에서 온다 — 템플릿(`createFromTemplate`)이나 커넥터(`ingestMaster`)가 그 길이다.\n * 문을 열어 두면 화면만 지워도 다음 소비자가 다시 생긴다.\n *\n * 시뮬 트윈을 세우는 것은 여전히 가능하다. 다만 그 입구는 **원본을 만드는 쪽**이지\n * 트윈을 고치는 쪽이 아니다(ADR-0029 — 시뮬레이터는 원본의 한 종류).\n */\n\n /** 기동 — model 를 주면 provision+start, 없으면 레지스트리에 저장된 구조로 start. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, {\n description: 'Start a twin runtime instance and begin the worker tick. If model is given it provisions first; otherwise it starts from the saved registry model.'\n })\n async startTwinInstance(\n @Arg('instanceId') instanceId: string,\n @Ctx() context: ResolverContext,\n @Arg('kind', { nullable: true }) kind?: string,\n @Arg('model', type => ScalarObject, { nullable: true }) model?: any\n ): Promise<boolean> {\n const domainId = context.state.domain.id\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n if (model) {\n /* model 를 함께 주는 것은 프로비저닝이다 — 그러면 종류도 말해야 한다.\n 예전에는 `kind ?? 'wms'` 로 메웠는데, 종류를 빠뜨린 야드/생산 트윈이 **창고로 조용히 기동**했다. */\n if (!kind) throw new Error('kind is required when model is given — it decides which domain kernel runs this twin')\n TwinEngine.start(instanceId, domainId, kind, await TwinEngine.withSpaceTimeBase(model as TwinModelDef, domainId))\n }\n else await TwinEngine.startFromRegistry(domainId, instanceId)\n return true\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Stop a running twin runtime instance by its id (only if it belongs to the current domain).' })\n async stopTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n if (!TwinEngine.owns(context.state.domain.id, instanceId)) return false // 테넌트 격리 — 남의 도메인 트윈 정지 차단\n await TwinEngine.stop(context.state.domain.id, instanceId)\n return true\n }\n\n /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Delete a twin instance: stop it, remove the registry entry and purge its event journal.' })\n async deleteTwinInstance(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<boolean> {\n await TwinEngine.remove(context.state.domain.id, instanceId)\n return true\n }\n\n /** 관리 목록 — 등록된 인스턴스 전체(kind/status/running/revision/노드·무버 수). 데이터 없으면 빈 배열. */\n /**\n * **이 공간의 트윈 id 들** — 한 현실을 여러 렌즈가 비추므로 공간을 물으면 그 렌즈 전부를 답한다.\n *\n * 화면이 전체 목록을 받아 걸러 쓰던 규칙을 서버로 모은다. 예측 화면은 같은 확장을 한 파일에서\n * **두 번** 복제하고 있었고, 규칙이 흩어지면 한쪽만 고쳐진다. 떠 있지 않은 것도 포함한다 —\n * 공간에 무엇이 있는지는 기동 여부와 다른 사실이다.\n */\n @Query(returns => [String], { description: 'Ids of the twins that live in a space (all its lenses), running or not.' })\n async twinSpaceInstanceIds(@Arg('spaceId') spaceId: string, @Ctx() context: ResolverContext): Promise<string[]> {\n return TwinEngine.instanceIdsOfSpace(context.state.domain.id, spaceId)\n }\n\n @Query(returns => [ScalarObject], { description: 'List provisioned twin instances in the current domain with management metadata.' })\n async twinInstanceList(@Ctx() context: ResolverContext): Promise<any[]> {\n return TwinEngine.list(context.state.domain.id)\n }\n\n /** 단건 상세 — 프로비저닝 에디터용 model(구조+layout) 포함. */\n @Query(returns => ScalarObject, { nullable: true, description: 'Get one provisioned twin instance with its TwinModelDef (structure + layout) for editing.' })\n async twinInstanceDetail(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any | null> {\n return TwinEngine.detail(context.state.domain.id, instanceId)\n }\n\n /**\n * 라이브(메모리) 인스턴스 id 목록 — 콘솔 셀렉터·포트폴리오용. **현재 도메인만**(테넌트 격리).\n * 과거엔 Object.keys(TwinEngine.instances) 로 전역 반환 → 다른 도메인 트윈이 종합현황/셀렉터에 새던 버그.\n * 도메인 스코프 목록(twinInstanceList 와 동일 소스)에서 running 만 추린다.\n */\n @Query(returns => [String], { description: 'List ids of running (in-memory) twin runtime instances in the current domain (tenant-isolated).' })\n async twinInstances(@Ctx() context: ResolverContext): Promise<string[]> {\n const list = await TwinEngine.list(context.state.domain.id)\n return list.filter((x: any) => x.running).map((x: any) => x.instanceId)\n }\n\n /**\n * 도메인 어휘 카탈로그 — 커널(레퍼런스) SSOT 를 그대로 노출. { wms:{label,locationTypes[]}, yms:…, mes:… }.\n * 프로비저닝 UI 는 이걸 소싱해 노드 타입/시스템을 채운다(앱 내 하드코딩 금지).\n */\n @Query(returns => ScalarObject, { description: 'Domain vocabulary catalog (per-system location location types) sourced from the twin kernel — SSOT for provisioning UI.' })\n twinDomainCatalog(): any {\n return domainCatalog()\n }\n}\n"]}
|
|
@@ -361,7 +361,7 @@ let TwinModelQuery = class TwinModelQuery {
|
|
|
361
361
|
})(),
|
|
362
362
|
description: inst.description ?? null,
|
|
363
363
|
kind: inst.kind,
|
|
364
|
-
|
|
364
|
+
restartPolicy: inst.restartPolicy,
|
|
365
365
|
source: ref
|
|
366
366
|
? { referenceId: ref.source, adapterType: ref.adapterType, status: ref.status, lastSyncedAt: ref.lastSyncedAt }
|
|
367
367
|
: null,
|