@things-factory/headless-twin 10.0.8 → 10.0.9
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/attention-digest.d.ts +52 -0
- package/dist-server/engine/attention-digest.js +76 -0
- package/dist-server/engine/attention-digest.js.map +1 -0
- package/dist-server/engine/board-vocabulary.d.ts +17 -0
- package/dist-server/engine/board-vocabulary.js +63 -0
- package/dist-server/engine/board-vocabulary.js.map +1 -0
- package/dist-server/engine/command-routing.d.ts +33 -0
- package/dist-server/engine/command-routing.js +53 -0
- package/dist-server/engine/command-routing.js.map +1 -0
- package/dist-server/engine/entity-delta.d.ts +13 -6
- package/dist-server/engine/entity-delta.js +38 -12
- package/dist-server/engine/entity-delta.js.map +1 -1
- package/dist-server/engine/index.d.ts +6 -0
- package/dist-server/engine/index.js +10 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +78 -0
- package/dist-server/engine/kpi-baseline.js +123 -0
- package/dist-server/engine/kpi-baseline.js.map +1 -0
- package/dist-server/engine/kpi-broadcast.d.ts +4 -0
- package/dist-server/engine/kpi-broadcast.js +16 -0
- package/dist-server/engine/kpi-broadcast.js.map +1 -0
- package/dist-server/engine/kpi-query.d.ts +31 -0
- package/dist-server/engine/kpi-query.js +50 -2
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-attentions.d.ts +1 -0
- package/dist-server/engine/live-attentions.js +7 -1
- package/dist-server/engine/live-attentions.js.map +1 -1
- package/dist-server/engine/runtime-key.d.ts +15 -0
- package/dist-server/engine/runtime-key.js +64 -0
- package/dist-server/engine/runtime-key.js.map +1 -0
- package/dist-server/engine/state-axes.d.ts +16 -0
- package/dist-server/engine/state-axes.js +54 -0
- package/dist-server/engine/state-axes.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +92 -16
- package/dist-server/engine/twin-engine.js +335 -77
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/twin-level.d.ts +23 -0
- package/dist-server/engine/twin-level.js +52 -0
- package/dist-server/engine/twin-level.js.map +1 -0
- package/dist-server/engine/warm-start.d.ts +58 -12
- package/dist-server/engine/warm-start.js +80 -9
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +34 -0
- package/dist-server/service/reference/discovery-result.js +84 -0
- package/dist-server/service/reference/discovery-result.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +30 -0
- package/dist-server/service/reference/ingest-space.js +63 -0
- package/dist-server/service/reference/ingest-space.js.map +1 -0
- package/dist-server/service/reference/knob-defaults.d.ts +20 -0
- package/dist-server/service/reference/knob-defaults.js +59 -0
- package/dist-server/service/reference/knob-defaults.js.map +1 -0
- package/dist-server/service/reference/reference-live.js +2 -2
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +37 -2
- package/dist-server/service/reference/reference-master.js +55 -5
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +2 -2
- package/dist-server/service/reference/reference-resolver.js +71 -15
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +29 -10
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +4 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +6 -2
- package/dist-server/service/twin-journal/twin-journal-query.js +25 -7
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +8 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +23 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +9 -0
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +2 -1
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/engine/attention-digest.ts +102 -0
- package/server/engine/board-vocabulary.ts +61 -0
- package/server/engine/command-routing.ts +67 -0
- package/server/engine/entity-delta.ts +33 -11
- package/server/engine/index.ts +10 -0
- package/server/engine/kpi-baseline.ts +202 -0
- package/server/engine/kpi-broadcast.ts +13 -0
- package/server/engine/kpi-query.ts +81 -3
- package/server/engine/live-attentions.ts +7 -2
- package/server/engine/runtime-key.ts +58 -0
- package/server/engine/state-axes.ts +55 -0
- package/server/engine/twin-engine.ts +350 -77
- package/server/engine/twin-level.ts +48 -0
- package/server/engine/warm-start.ts +130 -16
- package/server/service/reference/discovery-result.ts +95 -0
- package/server/service/reference/ingest-space.ts +70 -0
- package/server/service/reference/knob-defaults.ts +59 -0
- package/server/service/reference/reference-live.ts +2 -2
- package/server/service/reference/reference-master.ts +94 -8
- package/server/service/reference/reference-resolver.ts +80 -16
- package/server/service/twin-attention/twin-attention-query.ts +43 -6
- package/server/service/twin-control/twin-control-mutation.ts +31 -12
- package/server/service/twin-forecast/twin-forecast-query.ts +3 -2
- package/server/service/twin-instance/twin-instance.ts +6 -2
- package/server/service/twin-journal/twin-journal-query.ts +35 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +18 -2
- package/server/service/twin-metrics/twin-metrics-query.ts +1 -1
- package/server/service/twin-space/twin-space-resolver.ts +10 -1
- package/server/service/twin-state/twin-state-subscription.ts +1 -1
- package/server/service/twin-structure/twin-structure.ts +4 -1
- package/server/service/twin-target/twin-target-resolver.ts +22 -4
- package/test/attention-digest.test.ts +135 -0
- package/test/board-vocabulary.test.ts +114 -0
- package/test/capability-mapping.test.ts +4 -4
- package/test/command-routing.test.ts +61 -0
- package/test/discovery-result.test.ts +75 -0
- package/test/entity-delta.test.ts +25 -25
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-space.test.ts +50 -0
- package/test/knob-defaults.test.ts +72 -0
- package/test/kpi-baseline-db.test.ts +214 -0
- package/test/kpi-baseline.test.ts +196 -0
- package/test/kpi-query-bench.test.ts +128 -0
- package/test/live-mirror-parity.test.ts +35 -2
- package/test/master-to-twin.test.ts +7 -3
- package/test/mutation-gate.test.ts +108 -0
- package/test/oee-accumulator.test.ts +65 -1
- package/test/registry-key-guard.test.ts +80 -0
- package/test/runtime-key.test.ts +66 -0
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/state-axes.test.ts +74 -0
- package/test/streamline-e2e.test.ts +2 -2
- package/test/structure-revision-db.test.ts +5 -4
- package/test/tenant-registry-db.test.ts +149 -0
- package/test/warm-start-seam.test.ts +140 -0
- package/test/warm-start.test.ts +138 -4
|
@@ -17,7 +17,13 @@ function withLiveAttentions(state) {
|
|
|
17
17
|
* 입력을 **시뮬 쪽과 똑같이** 넣는다. 한쪽만 빠지면 같은 공장이 한쪽에서만 막힌 것처럼 보이고,
|
|
18
18
|
* 그 어긋남은 규칙이 아니라 배선 때문이라 아무 데서도 안 잡힌다(`tasks` 를 빠뜨려 실제로 그럴 뻔했다).
|
|
19
19
|
*/
|
|
20
|
-
attentions: (0, twin_kernel_1.deriveAttentions)({ equipment: state.equipment ?? [], locations: state.locations ?? [], orders: state.orders ?? [], tasks: state.tasks ?? [] },
|
|
20
|
+
attentions: (0, twin_kernel_1.deriveAttentions)({ equipment: state.equipment ?? [], locations: state.locations ?? [], orders: state.orders ?? [], tasks: state.tasks ?? [] },
|
|
21
|
+
/*
|
|
22
|
+
* **확인(ack)해 둔 것을 함께 넘긴다.** 예전에는 여기에 undefined 를 넘겨서, 라이브에서는
|
|
23
|
+
* 확인 처리가 화면에 전혀 반영되지 않았다 — 사람이 확인해도 신호가 계속 활성으로 나갔다.
|
|
24
|
+
* 확인 사실은 이제 저널에 남고(`attention.acked`) 프로젝터가 상태에 실어 오므로 여기서 받는다.
|
|
25
|
+
*/
|
|
26
|
+
state.acked?.length ? new Set(state.acked) : undefined, state.nowTime)
|
|
21
27
|
};
|
|
22
28
|
}
|
|
23
29
|
//# sourceMappingURL=live-attentions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-attentions.js","sourceRoot":"","sources":["../../server/engine/live-attentions.ts"],"names":[],"mappings":";;AAQA,
|
|
1
|
+
{"version":3,"file":"live-attentions.js","sourceRoot":"","sources":["../../server/engine/live-attentions.ts"],"names":[],"mappings":";;AAQA,gDAqBC;AA7BD;;;;;GAKG;AACH,sDAAuD;AAEvD,SAAgB,kBAAkB,CAChC,KAAQ;IAER,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxB,OAAO;QACL,GAAG,KAAK;QACR;;;WAGG;QACH,UAAU,EAAE,IAAA,8BAAgB,EAC1B,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE;QAC5H;;;;WAIG;QACH,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,EACtD,KAAK,CAAC,OAAO,CACd;KACF,CAAA;AACH,CAAC","sourcesContent":["/*\n * live State attention 보강 — projector 미러 State(관측)에 커널 deriveAttentions(계산)를 채운다.\n * live 스냅샷이 sim 동형(attentions 포함)이 되어 attention 질의/스트림이 live 에서도 신호를 준다.\n * 순수(입력 불변). face2-inbound-live §1.1 \"State-순수 파생\"(입력이 미러 State 에 있어 재적용 가능).\n * deriveAttentions 는 커널 공유 공식(sim/live 동일 임계값·규칙).\n */\nimport { deriveAttentions } from '@operato/twin-kernel'\n\nexport function withLiveAttentions<S extends { equipment?: any[]; locations?: any[]; orders?: any[]; tasks?: any[]; attentions?: any[]; acked?: string[]; nowTime?: string }>(\n state: S\n): S {\n if (!state) return state\n return {\n ...state,\n /*\n * 입력을 **시뮬 쪽과 똑같이** 넣는다. 한쪽만 빠지면 같은 공장이 한쪽에서만 막힌 것처럼 보이고,\n * 그 어긋남은 규칙이 아니라 배선 때문이라 아무 데서도 안 잡힌다(`tasks` 를 빠뜨려 실제로 그럴 뻔했다).\n */\n attentions: deriveAttentions(\n { equipment: state.equipment ?? [], locations: state.locations ?? [], orders: state.orders ?? [], tasks: state.tasks ?? [] },\n /*\n * **확인(ack)해 둔 것을 함께 넘긴다.** 예전에는 여기에 undefined 를 넘겨서, 라이브에서는\n * 확인 처리가 화면에 전혀 반영되지 않았다 — 사람이 확인해도 신호가 계속 활성으로 나갔다.\n * 확인 사실은 이제 저널에 남고(`attention.acked`) 프로젝터가 상태에 실어 오므로 여기서 받는다.\n */\n state.acked?.length ? new Set(state.acked) : undefined,\n state.nowTime\n )\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 도메인과 인스턴스 id 를 잇는 문자 — 저장소의 기존 캐시 키와 같은 모양. */
|
|
2
|
+
export declare const RUNTIME_KEY_SEP = ":";
|
|
3
|
+
/**
|
|
4
|
+
* 런타임 레지스트리 키.
|
|
5
|
+
*
|
|
6
|
+
* @throws 도메인 id 가 비었거나 구분자를 담고 있으면(키가 모호해진다). 인스턴스 id 가 비었으면.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runtimeKey(domainId: string, instanceId: string): string;
|
|
9
|
+
/** 키를 되돌린다 — 첫 구분자에서 자른다(인스턴스 id 안의 구분자는 보존). */
|
|
10
|
+
export declare function parseRuntimeKey(key: string): {
|
|
11
|
+
domainId: string;
|
|
12
|
+
instanceId: string;
|
|
13
|
+
};
|
|
14
|
+
/** 이 키가 이 테넌트의 것인가 — 레지스트리를 훑을 때 도메인으로 걸러낸다. */
|
|
15
|
+
export declare function isOfDomain(key: string, domainId: string): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 런타임 레지스트리 키 — **트윈의 정체성은 `(도메인, 인스턴스 id)` 다.** 순수.
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 있나 (2026-08-06 테넌트 격리 전수 확인) ───────────────────────────────
|
|
6
|
+
* `TwinEngine.instances` 와 `recovered` 가 **`instanceId` 하나로** 키를 잡고 있었다. 그런데 DB 유일성은
|
|
7
|
+
* `[domain, instanceId]` 이고(`TwinInstance` 인덱스) 트윈의 정체성도 그쪽이다. 즉 런타임 맵만 **전역
|
|
8
|
+
* 이름 공간**이었다.
|
|
9
|
+
*
|
|
10
|
+
* **충돌은 이론이 아니다.** 레퍼런스 경로가 `instanceId` 로 **레퍼런스 소스 이름**을 쓴다
|
|
11
|
+
* (`sap-ewm-1710`). 벤더 시스템 이름은 테넌트가 **공유하는 어휘**라, 두 테넌트가 같은 시스템에
|
|
12
|
+
* 연결하면 같은 id 가 된다 — 드문 사고가 아니라 예상되는 경우다.
|
|
13
|
+
*
|
|
14
|
+
* 그래서 무엇이 어긋났나:
|
|
15
|
+
* · `startLive` 의 조기 반환이 A 의 런타임을 B 에게 돌려주고 `ingestLive` 는 도메인을 확인하지 않아
|
|
16
|
+
* **B 의 라이브 이벤트가 A 의 커널에 적용**될 수 있었다.
|
|
17
|
+
* · `recovered` 가 겹치면 웜스타트 씨앗이 **남의 스냅샷**이 되어 재고가 섞인다.
|
|
18
|
+
* · `start`/`stop` 이 남의 런타임을 집으므로 B 는 "기동됨" 을 받고도 이후 전부 거부됐다.
|
|
19
|
+
*
|
|
20
|
+
* 이 부류는 **이미 한 번 물었다** — `twin-lifecycle-mutation.ts` 주석이 증언한다: "과거엔
|
|
21
|
+
* `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그." 그 자리만 고쳤다.
|
|
22
|
+
* 그래서 이번에는 가드를 더 붙이지 않고 **키를 고친다** — 저수준 메서드가 `domainId` 를 받게 되는 것이
|
|
23
|
+
* 요점이다. "누구의 트윈인가" 를 말하지 않고는 만질 수 없다.
|
|
24
|
+
*
|
|
25
|
+
* ── 구분자 규약 ─────────────────────────────────────────────────────────────
|
|
26
|
+
* `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).
|
|
27
|
+
* 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도
|
|
28
|
+
* 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로
|
|
29
|
+
* **그때는 던진다** — 조용히 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.RUNTIME_KEY_SEP = void 0;
|
|
33
|
+
exports.runtimeKey = runtimeKey;
|
|
34
|
+
exports.parseRuntimeKey = parseRuntimeKey;
|
|
35
|
+
exports.isOfDomain = isOfDomain;
|
|
36
|
+
/** 도메인과 인스턴스 id 를 잇는 문자 — 저장소의 기존 캐시 키와 같은 모양. */
|
|
37
|
+
exports.RUNTIME_KEY_SEP = ':';
|
|
38
|
+
/**
|
|
39
|
+
* 런타임 레지스트리 키.
|
|
40
|
+
*
|
|
41
|
+
* @throws 도메인 id 가 비었거나 구분자를 담고 있으면(키가 모호해진다). 인스턴스 id 가 비었으면.
|
|
42
|
+
*/
|
|
43
|
+
function runtimeKey(domainId, instanceId) {
|
|
44
|
+
if (!domainId)
|
|
45
|
+
throw new Error('runtimeKey: domainId is required — a twin runtime always belongs to a tenant');
|
|
46
|
+
if (!instanceId)
|
|
47
|
+
throw new Error('runtimeKey: instanceId is required');
|
|
48
|
+
if (domainId.includes(exports.RUNTIME_KEY_SEP)) {
|
|
49
|
+
throw new Error(`runtimeKey: domainId must not contain "${exports.RUNTIME_KEY_SEP}" (would make the key ambiguous)`);
|
|
50
|
+
}
|
|
51
|
+
return `${domainId}${exports.RUNTIME_KEY_SEP}${instanceId}`;
|
|
52
|
+
}
|
|
53
|
+
/** 키를 되돌린다 — 첫 구분자에서 자른다(인스턴스 id 안의 구분자는 보존). */
|
|
54
|
+
function parseRuntimeKey(key) {
|
|
55
|
+
const at = key.indexOf(exports.RUNTIME_KEY_SEP);
|
|
56
|
+
if (at <= 0 || at === key.length - 1)
|
|
57
|
+
throw new Error(`parseRuntimeKey: "${key}" is not a runtime key`);
|
|
58
|
+
return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) };
|
|
59
|
+
}
|
|
60
|
+
/** 이 키가 이 테넌트의 것인가 — 레지스트리를 훑을 때 도메인으로 걸러낸다. */
|
|
61
|
+
function isOfDomain(key, domainId) {
|
|
62
|
+
return !!domainId && key.startsWith(`${domainId}${exports.RUNTIME_KEY_SEP}`);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=runtime-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-key.js","sourceRoot":"","sources":["../../server/engine/runtime-key.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;AAUH,gCAOC;AAGD,0CAIC;AAGD,gCAEC;AA3BD,kDAAkD;AACrC,QAAA,eAAe,GAAG,GAAG,CAAA;AAElC;;;;GAIG;AACH,SAAgB,UAAU,CAAC,QAAgB,EAAE,UAAkB;IAC7D,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;IAC9G,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACtE,IAAI,QAAQ,CAAC,QAAQ,CAAC,uBAAe,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,0CAA0C,uBAAe,kCAAkC,CAAC,CAAA;IAC9G,CAAC;IACD,OAAO,GAAG,QAAQ,GAAG,uBAAe,GAAG,UAAU,EAAE,CAAA;AACrD,CAAC;AAED,iDAAiD;AACjD,SAAgB,eAAe,CAAC,GAAW;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,uBAAe,CAAC,CAAA;IACvC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,wBAAwB,CAAC,CAAA;IACvG,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAA;AACtE,CAAC;AAED,gDAAgD;AAChD,SAAgB,UAAU,CAAC,GAAW,EAAE,QAAgB;IACtD,OAAO,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,uBAAe,EAAE,CAAC,CAAA;AACtE,CAAC","sourcesContent":["/*\n * 런타임 레지스트리 키 — **트윈의 정체성은 `(도메인, 인스턴스 id)` 다.** 순수.\n *\n * ── 왜 있나 (2026-08-06 테넌트 격리 전수 확인) ───────────────────────────────\n * `TwinEngine.instances` 와 `recovered` 가 **`instanceId` 하나로** 키를 잡고 있었다. 그런데 DB 유일성은\n * `[domain, instanceId]` 이고(`TwinInstance` 인덱스) 트윈의 정체성도 그쪽이다. 즉 런타임 맵만 **전역\n * 이름 공간**이었다.\n *\n * **충돌은 이론이 아니다.** 레퍼런스 경로가 `instanceId` 로 **레퍼런스 소스 이름**을 쓴다\n * (`sap-ewm-1710`). 벤더 시스템 이름은 테넌트가 **공유하는 어휘**라, 두 테넌트가 같은 시스템에\n * 연결하면 같은 id 가 된다 — 드문 사고가 아니라 예상되는 경우다.\n *\n * 그래서 무엇이 어긋났나:\n * · `startLive` 의 조기 반환이 A 의 런타임을 B 에게 돌려주고 `ingestLive` 는 도메인을 확인하지 않아\n * **B 의 라이브 이벤트가 A 의 커널에 적용**될 수 있었다.\n * · `recovered` 가 겹치면 웜스타트 씨앗이 **남의 스냅샷**이 되어 재고가 섞인다.\n * · `start`/`stop` 이 남의 런타임을 집으므로 B 는 \"기동됨\" 을 받고도 이후 전부 거부됐다.\n *\n * 이 부류는 **이미 한 번 물었다** — `twin-lifecycle-mutation.ts` 주석이 증언한다: \"과거엔\n * `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그.\" 그 자리만 고쳤다.\n * 그래서 이번에는 가드를 더 붙이지 않고 **키를 고친다** — 저수준 메서드가 `domainId` 를 받게 되는 것이\n * 요점이다. \"누구의 트윈인가\" 를 말하지 않고는 만질 수 없다.\n *\n * ── 구분자 규약 ─────────────────────────────────────────────────────────────\n * `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`).\n * 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도\n * 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로\n * **그때는 던진다** — 조용히 잘못된 키를 만들면 그게 곧 이 결함의 재발이다.\n */\n\n/** 도메인과 인스턴스 id 를 잇는 문자 — 저장소의 기존 캐시 키와 같은 모양. */\nexport const RUNTIME_KEY_SEP = ':'\n\n/**\n * 런타임 레지스트리 키.\n *\n * @throws 도메인 id 가 비었거나 구분자를 담고 있으면(키가 모호해진다). 인스턴스 id 가 비었으면.\n */\nexport function runtimeKey(domainId: string, instanceId: string): string {\n if (!domainId) throw new Error('runtimeKey: domainId is required — a twin runtime always belongs to a tenant')\n if (!instanceId) throw new Error('runtimeKey: instanceId is required')\n if (domainId.includes(RUNTIME_KEY_SEP)) {\n throw new Error(`runtimeKey: domainId must not contain \"${RUNTIME_KEY_SEP}\" (would make the key ambiguous)`)\n }\n return `${domainId}${RUNTIME_KEY_SEP}${instanceId}`\n}\n\n/** 키를 되돌린다 — 첫 구분자에서 자른다(인스턴스 id 안의 구분자는 보존). */\nexport function parseRuntimeKey(key: string): { domainId: string; instanceId: string } {\n const at = key.indexOf(RUNTIME_KEY_SEP)\n if (at <= 0 || at === key.length - 1) throw new Error(`parseRuntimeKey: \"${key}\" is not a runtime key`)\n return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) }\n}\n\n/** 이 키가 이 테넌트의 것인가 — 레지스트리를 훑을 때 도메인으로 걸러낸다. */\nexport function isOfDomain(key: string, domainId: string): boolean {\n return !!domainId && key.startsWith(`${domainId}${RUNTIME_KEY_SEP}`)\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다. */
|
|
2
|
+
export declare const STATE_AXES: readonly ["locations", "items", "equipment", "persons", "assets", "tasks", "orders", "attentions", "acked"];
|
|
3
|
+
export type StateAxis = (typeof STATE_AXES)[number];
|
|
4
|
+
export interface PickedAxes {
|
|
5
|
+
/** 고른 축만 남은 상태. `axes` 가 비었으면 원본 그대로(자르지 않는다). */
|
|
6
|
+
state: any;
|
|
7
|
+
/** 상태 축이 아닌 이름 — 호출부가 오타를 알 수 있게 돌려준다. */
|
|
8
|
+
unknown: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 요청한 축만 남긴다.
|
|
12
|
+
*
|
|
13
|
+
* `axes` 가 비어 있으면 **자르지 않는다** — 기본값이 "전부" 여야 기존 호출부가 그대로 동작한다
|
|
14
|
+
* (자르는 쪽이 기본이면 축을 말하지 않은 화면이 조용히 비어 버린다).
|
|
15
|
+
*/
|
|
16
|
+
export declare function pickAxes(state: any, axes: readonly string[] | undefined | null): PickedAxes;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 상태 축 고르기 — **순수**. "요청한 축만 남긴다."
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────
|
|
6
|
+
* 지도는 구독으로 "바뀌었다" 신호를 받고 상태를 다시 읽는다. 그 신호는 의도적으로 가볍고(`{kind,
|
|
7
|
+
* revision}`) 리프레시도 250ms 로 합쳐진다 — 거기까지는 설계대로다. 문제는 **리프레시가 통째로**
|
|
8
|
+
* 라는 것이다: 실 저널로 재 보니 트윈 하나가 최대 710KB 이고, 같은 공간에 셋이면 한 번에 약 2MB 다.
|
|
9
|
+
*
|
|
10
|
+
* 그런데 지도가 실제로 쓰는 것은 `locations`·`attentions`·`nowTime` **셋뿐**이다(코드 확인). 물품
|
|
11
|
+
* 2,278건·작업·오더를 받아 놓고 쓰지 않는다. 축을 골라 받으면 트윈에 따라 1.7~15배 줄어든다
|
|
12
|
+
* (물품이 많은 트윈은 15배, 신호가 많은 트윈은 2배 — 신호가 큰 쪽은 그것이 다음 과제다).
|
|
13
|
+
*
|
|
14
|
+
* ── 화면 이름을 API 에 넣지 않는다 ──────────────────────────────────────────
|
|
15
|
+
* `twinMapState` 같은 화면 전용 질의를 만들지 않는다. 호출부가 **필요한 축을 말하고** 서버가 자른다 —
|
|
16
|
+
* 새 화면이 생길 때마다 질의가 늘지 않고, 어휘도 화면 이름이 아니라 상태의 축이다(무방언).
|
|
17
|
+
*
|
|
18
|
+
* 모르는 축 이름은 **조용히 버리지 않고 무시하되 그 사실을 돌려준다** — 오타 하나로 화면이 빈
|
|
19
|
+
* 채로 뜨는 것이 가장 나쁘다.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.STATE_AXES = void 0;
|
|
23
|
+
exports.pickAxes = pickAxes;
|
|
24
|
+
/** 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다. */
|
|
25
|
+
exports.STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked'];
|
|
26
|
+
/** 축과 무관하게 늘 남는 것 — 이것이 없으면 무엇을 받았는지 알 수 없다. */
|
|
27
|
+
const ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections'];
|
|
28
|
+
/**
|
|
29
|
+
* 요청한 축만 남긴다.
|
|
30
|
+
*
|
|
31
|
+
* `axes` 가 비어 있으면 **자르지 않는다** — 기본값이 "전부" 여야 기존 호출부가 그대로 동작한다
|
|
32
|
+
* (자르는 쪽이 기본이면 축을 말하지 않은 화면이 조용히 비어 버린다).
|
|
33
|
+
*/
|
|
34
|
+
function pickAxes(state, axes) {
|
|
35
|
+
if (!state || typeof state !== 'object')
|
|
36
|
+
return { state, unknown: [] };
|
|
37
|
+
if (!axes?.length)
|
|
38
|
+
return { state, unknown: [] };
|
|
39
|
+
const known = new Set(exports.STATE_AXES);
|
|
40
|
+
const wanted = axes.filter(a => known.has(a));
|
|
41
|
+
const unknown = axes.filter(a => !known.has(a));
|
|
42
|
+
/* 아는 축이 하나도 없으면 자르지 않는다 — 전부 오타인 경우에 빈 화면을 만들지 않는다. */
|
|
43
|
+
if (!wanted.length)
|
|
44
|
+
return { state, unknown };
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const k of ALWAYS)
|
|
47
|
+
if (k in state)
|
|
48
|
+
out[k] = state[k];
|
|
49
|
+
for (const a of wanted)
|
|
50
|
+
if (a in state)
|
|
51
|
+
out[a] = state[a];
|
|
52
|
+
return { state: out, unknown };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=state-axes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-axes.js","sourceRoot":"","sources":["../../server/engine/state-axes.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AAsBH,4BAcC;AAlCD,yCAAyC;AAC5B,QAAA,UAAU,GAAG,CAAC,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAU,CAAA;AAGrI,gDAAgD;AAChD,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,CAAU,CAAA;AAS5E;;;;;GAKG;AACH,SAAgB,QAAQ,CAAC,KAAU,EAAE,IAA0C;IAC7E,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IAEhD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,kBAA+B,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/C,uDAAuD;IACvD,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;IAE7C,MAAM,GAAG,GAAQ,EAAE,CAAA;IACnB,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,KAAK;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzD,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,IAAI,KAAK;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAA;AAChC,CAAC","sourcesContent":["/*\n * 상태 축 고르기 — **순수**. \"요청한 축만 남긴다.\"\n *\n * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────\n * 지도는 구독으로 \"바뀌었다\" 신호를 받고 상태를 다시 읽는다. 그 신호는 의도적으로 가볍고(`{kind,\n * revision}`) 리프레시도 250ms 로 합쳐진다 — 거기까지는 설계대로다. 문제는 **리프레시가 통째로**\n * 라는 것이다: 실 저널로 재 보니 트윈 하나가 최대 710KB 이고, 같은 공간에 셋이면 한 번에 약 2MB 다.\n *\n * 그런데 지도가 실제로 쓰는 것은 `locations`·`attentions`·`nowTime` **셋뿐**이다(코드 확인). 물품\n * 2,278건·작업·오더를 받아 놓고 쓰지 않는다. 축을 골라 받으면 트윈에 따라 1.7~15배 줄어든다\n * (물품이 많은 트윈은 15배, 신호가 많은 트윈은 2배 — 신호가 큰 쪽은 그것이 다음 과제다).\n *\n * ── 화면 이름을 API 에 넣지 않는다 ──────────────────────────────────────────\n * `twinMapState` 같은 화면 전용 질의를 만들지 않는다. 호출부가 **필요한 축을 말하고** 서버가 자른다 —\n * 새 화면이 생길 때마다 질의가 늘지 않고, 어휘도 화면 이름이 아니라 상태의 축이다(무방언).\n *\n * 모르는 축 이름은 **조용히 버리지 않고 무시하되 그 사실을 돌려준다** — 오타 하나로 화면이 빈\n * 채로 뜨는 것이 가장 나쁘다.\n */\n\n/** 상태가 들고 있는 축 — 이 목록 밖의 이름은 고를 수 없다. */\nexport const STATE_AXES = ['locations', 'items', 'equipment', 'persons', 'assets', 'tasks', 'orders', 'attentions', 'acked'] as const\nexport type StateAxis = (typeof STATE_AXES)[number]\n\n/** 축과 무관하게 늘 남는 것 — 이것이 없으면 무엇을 받았는지 알 수 없다. */\nconst ALWAYS = ['revision', 'nowTime', 'simClockMs', 'corrections'] as const\n\nexport interface PickedAxes {\n /** 고른 축만 남은 상태. `axes` 가 비었으면 원본 그대로(자르지 않는다). */\n state: any\n /** 상태 축이 아닌 이름 — 호출부가 오타를 알 수 있게 돌려준다. */\n unknown: string[]\n}\n\n/**\n * 요청한 축만 남긴다.\n *\n * `axes` 가 비어 있으면 **자르지 않는다** — 기본값이 \"전부\" 여야 기존 호출부가 그대로 동작한다\n * (자르는 쪽이 기본이면 축을 말하지 않은 화면이 조용히 비어 버린다).\n */\nexport function pickAxes(state: any, axes: readonly string[] | undefined | null): PickedAxes {\n if (!state || typeof state !== 'object') return { state, unknown: [] }\n if (!axes?.length) return { state, unknown: [] }\n\n const known = new Set<string>(STATE_AXES as readonly string[])\n const wanted = axes.filter(a => known.has(a))\n const unknown = axes.filter(a => !known.has(a))\n /* 아는 축이 하나도 없으면 자르지 않는다 — 전부 오타인 경우에 빈 화면을 만들지 않는다. */\n if (!wanted.length) return { state, unknown }\n\n const out: any = {}\n for (const k of ALWAYS) if (k in state) out[k] = state[k]\n for (const a of wanted) if (a in state) out[a] = state[a]\n return { state: out, unknown }\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReferenceMaster } from '../service/reference/reference-master.js';
|
|
2
|
+
import { type IngestWarning } from '../service/reference/reference-master.js';
|
|
2
3
|
import { type ModelBasis } from './model-basis.js';
|
|
3
4
|
import { type StructureDiff } from './structure-diff.js';
|
|
4
5
|
import { OeeAccumulator } from './oee-accumulator.js';
|
|
@@ -49,6 +50,7 @@ interface InstanceRuntime {
|
|
|
49
50
|
export declare class TwinEngine {
|
|
50
51
|
static instances: Record<string, InstanceRuntime>;
|
|
51
52
|
/** 라이브 런타임이 없을 때(복구 후 미기동) 저널에서 재구성한 상태 캐시. */
|
|
53
|
+
/** 웜스타트 씨앗 — `instances` 와 **같은 키**(겹치면 남의 스냅샷으로 재고가 섞인다). */
|
|
52
54
|
static recovered: Record<string, any>;
|
|
53
55
|
static TICK_MS: number;
|
|
54
56
|
static SNAPSHOT_CACHE_ID: string;
|
|
@@ -73,18 +75,26 @@ export declare class TwinEngine {
|
|
|
73
75
|
* 웜스타트 — 기동하는 커널에 **직전 관측 상태**를 심는다.
|
|
74
76
|
*
|
|
75
77
|
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
76
|
-
* `loadBoard` 는 **구조만** 싣는다(
|
|
78
|
+
* `loadBoard` 는 **구조만** 싣는다(자리·설비). 상태(무엇이 어디에 얼마나)는 없다. 그래서 재기동한
|
|
77
79
|
* 트윈은 저널에 입고 540건이 남아 있어도 재고가 0 이었고, 화면은 "보유 중인 것이 없습니다" 라고
|
|
78
80
|
* 말했다 — 있는 재고를 없다고 하는 셈이다(2026-07-31 hatiolab-wms 실측으로 확인).
|
|
79
81
|
* `bootstrap()` 이 이미 체크포인트 캐시(없으면 저널 replay)로 상태를 복구해 `recovered` 에 담아 두는데,
|
|
80
82
|
* 기동 순간 그걸 **버리고** 있었다. 반만 연결돼 있던 장치를 잇는다.
|
|
81
83
|
*
|
|
82
|
-
* ──
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* ── 반쪽이던 복구를 마무리한다 (2026-08-05) ─────────────────────────────────
|
|
85
|
+
* 재고는 살아났는데 **진행 중 주문이 통째로 사라진** 화면이 남아 있었다. 씨앗이 자리·물품·설비
|
|
86
|
+
* 셋만 넘겼기 때문이다. 그 근거는 "스냅샷 오더에 progress 밖에 없다" 였고, 그때는 맞았다 —
|
|
87
|
+
* 그러나 커널 `OrderState` 가 원값(`requested`·`fulfilled`·`lines`)을 되찾은 뒤에도 이 자리만
|
|
88
|
+
* 그대로 남았다. 오류를 내지 않는 종류라 오래 버텼다.
|
|
89
|
+
*
|
|
90
|
+
* 지금은 **관측 스냅샷 전체**를 넘긴다(오더·작업·사람·자산 포함). 시뮬은 커널 `snapshot()`,
|
|
91
|
+
* 라이브는 `StateProjector` 가 그 일곱 축을 모두 담는다. 무엇을 심을 수 있는지 판정하는 규칙
|
|
92
|
+
* (이행 완료 오더 제외·고아 작업 제외)은 커널에 있고, 호스트가 미리 골라내지 않는다.
|
|
93
|
+
*
|
|
94
|
+
* ── 남은 한계는 숨기지 않고 센다 ────────────────────────────────────────────
|
|
95
|
+
* · 원값이 없는 오더(progress 만 있는 것)는 남은 수량을 알 수 없어 심지 않는다. **지어내지 않는
|
|
96
|
+
* 대신 몇 건인지 말한다** — 세지 않으면 "주문이 없다" 와 "주문을 못 심었다" 가 화면에서 같아진다.
|
|
97
|
+
* · 진행 중 개별 작업의 내부 상태는 관측만으로 완전히 복원되지 않는다(커널이 명시한 한계, 재계획에 맡김).
|
|
88
98
|
*
|
|
89
99
|
* ── 벤치는 시드하지 않는다 ──────────────────────────────────────────────────
|
|
90
100
|
* 부하 벤치는 **새 시작에서 용량을 재는 것**이 목적이라 현재 상태를 심으면 측정이 오염된다.
|
|
@@ -172,7 +182,17 @@ export declare class TwinEngine {
|
|
|
172
182
|
*
|
|
173
183
|
* 보드에 없으면 `undefined` — 레거시 경로 그대로다(기존 트윈의 거동을 바꾸지 않는다).
|
|
174
184
|
*/
|
|
175
|
-
|
|
185
|
+
/**
|
|
186
|
+
* 생산 선언을 꺼낸다 — 커널 생성자에 넘긴다.
|
|
187
|
+
*
|
|
188
|
+
* **옛 이름(`mesSpec`)은 읽지 않는다.** 그것은 계약에 선언조차 없던 필드였고(MES 커널 생성자 인자
|
|
189
|
+
* 이름이 그대로 굳은 것), 일반 기제에 한 시스템 이름이 붙어 있었기 때문에 창고 트윈이 이 자리를
|
|
190
|
+
* 쓰지 못했다. 별명으로 남겨 두면 그 혼동이 계속되므로 하나로 통일했다.
|
|
191
|
+
*
|
|
192
|
+
* 옛 이름만 가진 보드가 있으면 **조용히 생산 선언을 잃는 대신 분명히 멈춘다** — 그 트윈은 공정이
|
|
193
|
+
* 없는 채로 돌게 되고(라인이 서 있는 창고), 원인을 찾기 어렵다.
|
|
194
|
+
*/
|
|
195
|
+
private static productionSpecOf;
|
|
176
196
|
/**
|
|
177
197
|
* 현장(공간)의 **시각 기준**을 보드에 얹는다 — 커널이 교대의 `HH:MM` 을 읽을 기준.
|
|
178
198
|
*
|
|
@@ -191,7 +211,19 @@ export declare class TwinEngine {
|
|
|
191
211
|
* reference 어댑터가 낸 records → 커널 face2-adapter.ingest → CanonicalEnvelope 를 여기로 밀어넣는다.
|
|
192
212
|
* (State 채널 델타/저널 결선은 후속 — 스켈레톤은 data(tag) 미러 중심.)
|
|
193
213
|
*/
|
|
194
|
-
static ingestLive(id: string, envelopes: CanonicalEnvelope[]): void;
|
|
214
|
+
static ingestLive(domainId: string, id: string, envelopes: CanonicalEnvelope[]): void;
|
|
215
|
+
/**
|
|
216
|
+
* 구간 성과 방송은 **없앴다**(2026-08-06). 카드가 `twinKpi` 를 직접 묻는다.
|
|
217
|
+
*
|
|
218
|
+
* 왜: 카드를 여러 단계(공간·트윈·구역·자리·설비)에 붙이려면 방송으로는 태그가 트윈당 1,200개가 되고,
|
|
219
|
+
* **보드에 카드를 하나도 안 놓아도** 30초마다 트윈마다 저널을 접었다. 질의로 바꾸니 보고 있는 카드
|
|
220
|
+
* 수만큼만 들고, 같은 (대상·창·축) 은 클라이언트가 하나로 합친다.
|
|
221
|
+
*
|
|
222
|
+
* 덤으로 질의만 할 수 있는 것이 둘 생겼다 — **과거 시각**(`toTime`)과 **공간 단위 합산**(여러 트윈을
|
|
223
|
+
* 한 번에 접기). 방송 루프는 트윈별이라 둘 다 못 했다.
|
|
224
|
+
*
|
|
225
|
+
* 축을 나눠도 폴드 비용이 같다는 실측이 근거다(`test/kpi-query-bench.test.ts`).
|
|
226
|
+
*/
|
|
195
227
|
/** 방송 병합 주기(ms) — 방송률 상한. 인제스트가 아무리 빨라도 이 주기로만 방송. */
|
|
196
228
|
static BROADCAST_COALESCE_MS: number;
|
|
197
229
|
private static broadcastTimer?;
|
|
@@ -318,11 +350,17 @@ export declare class TwinEngine {
|
|
|
318
350
|
* Face2 마스터 인제스트(ADR-0018) — 레퍼런스 시스템(실 또는 가상)의 마스터를 읽어 트윈을 생성.
|
|
319
351
|
* 엔티티를 손배선/발명하지 않고 마스터에서 반영: 공간(Space) upsert + 인스턴스 provision(미기동).
|
|
320
352
|
* 노드타입은 커널 카탈로그로 검증(warning). start 는 호출측(bootstrap/mutation)이 결정.
|
|
353
|
+
*
|
|
354
|
+
* `into.spaceId` — **이 현장에 더한다.** 한 현실을 여러 렌즈(WMS·MES·YMS)가 비추므로 새 트윈이
|
|
355
|
+
* 기존 공간으로 들어갈 수 있다. 아래 병합은 원래 그 경우를 위해 있었는데(N:1) 만드는 흐름에서
|
|
356
|
+
* 공간을 고를 방법이 없었다 — 마스터가 파생한 id 만 쓰였다. 판정은 `resolveIngestSpace`(순수).
|
|
321
357
|
*/
|
|
322
|
-
static ingestMaster(domainId: string, master: ReferenceMaster
|
|
358
|
+
static ingestMaster(domainId: string, master: ReferenceMaster, into?: {
|
|
359
|
+
spaceId?: string;
|
|
360
|
+
}): Promise<{
|
|
323
361
|
instanceId: string;
|
|
324
362
|
spaceId: string;
|
|
325
|
-
warnings:
|
|
363
|
+
warnings: IngestWarning[];
|
|
326
364
|
}>;
|
|
327
365
|
/** 단건 상세 — 프로비저닝 에디터가 편집할 board(구조+layout) 포함. */
|
|
328
366
|
static detail(domainId: string, instanceId: string): Promise<any | null>;
|
|
@@ -353,14 +391,28 @@ export declare class TwinEngine {
|
|
|
353
391
|
/** 정지 훅 — 인스턴스 정지 시 부수자원 정리(예: reference-live 피드 unsubscribe). 순환 임포트 없이 결선. */
|
|
354
392
|
static stopHooks: ((id: string) => void)[];
|
|
355
393
|
static onStop(fn: (id: string) => void): void;
|
|
356
|
-
static stop(id: string): Promise<void>;
|
|
357
|
-
static
|
|
394
|
+
static stop(domainId: string, id: string): Promise<void>;
|
|
395
|
+
private static tickGuarded;
|
|
396
|
+
static runtime(domainId: string, id: string): TwinRuntimeType | undefined;
|
|
397
|
+
static dispatchCommand(domainId: string, instanceId: string, command: any): Promise<{
|
|
398
|
+
accepted: boolean;
|
|
399
|
+
error?: string;
|
|
400
|
+
errorCode?: string;
|
|
401
|
+
errorParams?: Record<string, string | number>;
|
|
402
|
+
}>;
|
|
358
403
|
/**
|
|
359
404
|
* 도메인 소유 게이트 — 인스턴스가 이 도메인 소유일 때만 true(테넌트 격리).
|
|
360
405
|
* 없거나 다른 도메인이면 false. 노출 리졸버(per-id metrics/attentions/state/stop/forecast)가
|
|
361
406
|
* this.instances[id] 를 만지기 전에 반드시 이걸로 확인해야 한다. 인스턴스 id 는 추측 가능하므로
|
|
362
407
|
* 검증 없이 접근하면 크로스테넌트 읽기/정지가 가능해진다.
|
|
363
408
|
*/
|
|
409
|
+
/**
|
|
410
|
+
* 이 런타임의 **관측 모드** — `'live'`(외부 실물을 미러) 또는 `'sim'`(커널이 굴린다). 떠 있지 않으면 없다.
|
|
411
|
+
*
|
|
412
|
+
* 소비처가 `instances[id].mode` 를 직접 읽던 자리를 대신한다. 레지스트리를 밖에 열면 도메인 확인이
|
|
413
|
+
* 자리마다 제각각이 되고, 실제로 그렇게 됐다(테넌트 격리 전수 확인, 2026-08-06).
|
|
414
|
+
*/
|
|
415
|
+
static modeOf(domainId: string, id: string): 'live' | 'sim' | undefined;
|
|
364
416
|
static owns(domainId: string, id: string): boolean;
|
|
365
417
|
/**
|
|
366
418
|
* 이 트윈이 이 테넌트 것인가 — **떠 있든 아니든.**
|
|
@@ -373,10 +425,34 @@ export declare class TwinEngine {
|
|
|
373
425
|
* 소유는 **등록부**가 안다. 이력·집계처럼 런타임과 무관한 질문은 이쪽에 묻는다.
|
|
374
426
|
*/
|
|
375
427
|
static ownsRegistered(domainId: string, instanceId: string): Promise<boolean>;
|
|
428
|
+
/**
|
|
429
|
+
* **이 공간의 인스턴스들** — 한 현실을 여러 렌즈가 비추므로, 공간을 물으면 그 렌즈 전부를 답한다.
|
|
430
|
+
*
|
|
431
|
+
* 화면이 이 규칙을 손으로 짜지 않게 서버가 답한다. 예측 화면은 같은 확장을 **한 파일에서 두 번**
|
|
432
|
+
* 복제하고 있었다(전체 목록을 받아 클라이언트에서 걸렀다) — 규칙이 흩어지면 한쪽만 고쳐진다.
|
|
433
|
+
*
|
|
434
|
+
* 떠 있지 않은 것도 포함한다: 공간에 무엇이 있는지는 기동 여부와 다른 사실이다.
|
|
435
|
+
*/
|
|
436
|
+
static instanceIdsOfSpace(domainId: string, spaceId: string): Promise<string[]>;
|
|
437
|
+
/**
|
|
438
|
+
* **이 공간에서 봐야 할 것** — 공간의 모든 렌즈에서 신호를 모은다.
|
|
439
|
+
*
|
|
440
|
+
* 합집합이 그대로 뜻이 있는 유일한 렌즈다(예측은 합칠 수 없다 — P50 두 개를 더할 수 없다).
|
|
441
|
+
* 정렬·잘라내기는 화면과 **같은 규칙**을 쓴다(`digestAttentions`) — 서버가 고른 상위 N 이 화면이
|
|
442
|
+
* 고를 N 과 달라지면 그 어긋남은 아무 데서도 오류로 드러나지 않는다.
|
|
443
|
+
*
|
|
444
|
+
* 신호마다 어느 트윈에서 왔는지(`instanceId`)를 붙인다. 공간에서 보면 같은 자리 id 가 렌즈마다
|
|
445
|
+
* 다른 것을 가리킬 수 있고, 조치는 결국 그 트윈에 보내야 한다.
|
|
446
|
+
*/
|
|
447
|
+
static attentionsOfSpace(domainId: string, spaceId: string, limit?: number): Promise<{
|
|
448
|
+
attentions: any[];
|
|
449
|
+
attentionTotal: number;
|
|
450
|
+
severityByLocation: Record<string, string>;
|
|
451
|
+
}>;
|
|
376
452
|
/** 라이브 커널(ForecastTwin) — forecast/divergence 예측 연산용. */
|
|
377
|
-
static kernel(id: string): any;
|
|
453
|
+
static kernel(domainId: string, id: string): any;
|
|
378
454
|
/** 라이브 처리량 계측 스냅샷(모니터, ④-1) — 내부 누적(_acc*) 제외한 공개 지표. live 아니면 null. */
|
|
379
|
-
static metrics(id: string): any;
|
|
455
|
+
static metrics(domainId: string, id: string): any;
|
|
380
456
|
/** 전체 라이브 인스턴스 계측(모니터 대시보드용). */
|
|
381
457
|
static allMetrics(domainId?: string): Promise<any[]>;
|
|
382
458
|
/**
|
|
@@ -386,6 +462,6 @@ export declare class TwinEngine {
|
|
|
386
462
|
*/
|
|
387
463
|
static buildForecastKernelAt(domainId: string, instanceId: string, untilTime?: string): Promise<any | null>;
|
|
388
464
|
/** 현재 전체 스냅샷 — 라이브 우선, 없으면 저널 복구 캐시. */
|
|
389
|
-
static snapshot(id: string): any;
|
|
465
|
+
static snapshot(domainId: string, id: string): any;
|
|
390
466
|
}
|
|
391
467
|
export {};
|