@operato/twin-kernel 0.7.66 → 0.7.68
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/ems-kernel.d.ts +17 -0
- package/dist/ems-kernel.js +44 -0
- package/dist/epcis.d.ts +29 -0
- package/dist/epcis.js +31 -0
- package/dist/event-journal.d.ts +23 -4
- package/dist/event-journal.js +48 -8
- package/dist/flow-engine.d.ts +11 -0
- package/dist/flow-engine.js +43 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/master-data.d.ts +80 -0
- package/dist/master-data.js +168 -0
- package/dist/observed-reducer.d.ts +45 -0
- package/dist/observed-reducer.js +104 -4
- package/dist-cjs/index.cjs +5143 -4862
- package/package.json +1 -1
package/dist/ems-kernel.d.ts
CHANGED
|
@@ -93,6 +93,23 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
93
93
|
* 계측 자체는 원천 시각을 달고 있어서 화면의 순간값은 정상으로 보였다 — 그래서 조용한 결함이었다.
|
|
94
94
|
* 저널을 기간으로 계산하는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
|
|
95
95
|
*/
|
|
96
|
+
/**
|
|
97
|
+
* 이 커널이 따로 드는 상태를 재개점에 담는다 — 계량 지점 · 수요 구간 · 월 최고 (2026-08-27).
|
|
98
|
+
*
|
|
99
|
+
* ── 왜 담아야 하나 ──────────────────────────────────────────────────────────
|
|
100
|
+
* 이 값들은 계측을 모아 **트윈이 만든 것**이다. 발전소는 「우리 트윈이 정한 15분 구간의 최대 수요」를
|
|
101
|
+
* 갖고 있지 않으므로 되풀어 주지 못한다. 담지 않으면 중간 저장본에서 이어 처리할 때 그만큼이
|
|
102
|
+
* 조용히 사라지고, 과거의 요금 근거를 말할 수 없게 된다.
|
|
103
|
+
*
|
|
104
|
+
* ── 무엇을 담지 않나 ────────────────────────────────────────────────────────
|
|
105
|
+
* 같은 구간에 제안을 이미 냈는지(`suggestedFor`)와 다른 트윈의 설비 상태(`peerStatus`)는 담지 않는다.
|
|
106
|
+
* 앞의 것은 브로드캐스팅을 줄이려는 표시일 뿐이고, 뒤의 것은 호스트가 매번 다시 넣어 준다.
|
|
107
|
+
* 구간 길이(`windowMs`)도 담지 않는다 — 그것은 선언에서 오는 값이고, 담으면 선언을 고쳐도 옛 값이
|
|
108
|
+
* 살아남는다.
|
|
109
|
+
*/
|
|
110
|
+
serializeDomain(): unknown;
|
|
111
|
+
/** 재개점에서 되돌린다 — 담기지 않은 칸은 건드리지 않는다(빈 값으로 덮지 않는다). */
|
|
112
|
+
restoreDomain(raw: unknown): void;
|
|
96
113
|
apply(envelope: CanonicalEnvelope): void;
|
|
97
114
|
/**
|
|
98
115
|
* 설비가 낸 자기 에너지 상태를 그 설비에 적는다 — 발전·저장·감축 여지·개폐 위치.
|
package/dist/ems-kernel.js
CHANGED
|
@@ -212,6 +212,50 @@ export class EmsKernel extends FlowEngine {
|
|
|
212
212
|
* 계측 자체는 원천 시각을 달고 있어서 화면의 순간값은 정상으로 보였다 — 그래서 조용한 결함이었다.
|
|
213
213
|
* 저널을 기간으로 계산하는 쪽(성과·요금·피더 배분)은 그 트윈에서 영원히 아무것도 찾지 못했다.
|
|
214
214
|
*/
|
|
215
|
+
/**
|
|
216
|
+
* 이 커널이 따로 드는 상태를 재개점에 담는다 — 계량 지점 · 수요 구간 · 월 최고 (2026-08-27).
|
|
217
|
+
*
|
|
218
|
+
* ── 왜 담아야 하나 ──────────────────────────────────────────────────────────
|
|
219
|
+
* 이 값들은 계측을 모아 **트윈이 만든 것**이다. 발전소는 「우리 트윈이 정한 15분 구간의 최대 수요」를
|
|
220
|
+
* 갖고 있지 않으므로 되풀어 주지 못한다. 담지 않으면 중간 저장본에서 이어 처리할 때 그만큼이
|
|
221
|
+
* 조용히 사라지고, 과거의 요금 근거를 말할 수 없게 된다.
|
|
222
|
+
*
|
|
223
|
+
* ── 무엇을 담지 않나 ────────────────────────────────────────────────────────
|
|
224
|
+
* 같은 구간에 제안을 이미 냈는지(`suggestedFor`)와 다른 트윈의 설비 상태(`peerStatus`)는 담지 않는다.
|
|
225
|
+
* 앞의 것은 브로드캐스팅을 줄이려는 표시일 뿐이고, 뒤의 것은 호스트가 매번 다시 넣어 준다.
|
|
226
|
+
* 구간 길이(`windowMs`)도 담지 않는다 — 그것은 선언에서 오는 값이고, 담으면 선언을 고쳐도 옛 값이
|
|
227
|
+
* 살아남는다.
|
|
228
|
+
*/
|
|
229
|
+
serializeDomain() {
|
|
230
|
+
return {
|
|
231
|
+
points: [...this.points.values()],
|
|
232
|
+
open: this.open,
|
|
233
|
+
closed: this.closed,
|
|
234
|
+
closedTotal: this.closedTotal,
|
|
235
|
+
peak: this.peak,
|
|
236
|
+
generationHeardAtMs: [...this.generationHeardAtMs.entries()].map(([id, atMs]) => ({ id, atMs }))
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** 재개점에서 되돌린다 — 담기지 않은 칸은 건드리지 않는다(빈 값으로 덮지 않는다). */
|
|
240
|
+
restoreDomain(raw) {
|
|
241
|
+
const d = raw;
|
|
242
|
+
if (!d || typeof d !== 'object')
|
|
243
|
+
return;
|
|
244
|
+
if (Array.isArray(d.points)) {
|
|
245
|
+
this.points = new Map(d.points.filter((p) => p?.id).map((p) => [String(p.id), p]));
|
|
246
|
+
}
|
|
247
|
+
if (d.open !== undefined)
|
|
248
|
+
this.open = d.open;
|
|
249
|
+
if (Array.isArray(d.closed))
|
|
250
|
+
this.closed = d.closed;
|
|
251
|
+
if (typeof d.closedTotal === 'number')
|
|
252
|
+
this.closedTotal = d.closedTotal;
|
|
253
|
+
if (d.peak !== undefined)
|
|
254
|
+
this.peak = d.peak;
|
|
255
|
+
if (Array.isArray(d.generationHeardAtMs)) {
|
|
256
|
+
this.generationHeardAtMs = new Map(d.generationHeardAtMs.filter((x) => x?.id).map((x) => [String(x.id), Number(x.atMs)]));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
215
259
|
apply(envelope) {
|
|
216
260
|
if (envelope.eventType === ENERGY_EVENT.measured) {
|
|
217
261
|
this.ingestMeasured(envelope);
|
package/dist/epcis.d.ts
CHANGED
|
@@ -94,6 +94,35 @@ export declare const CBV_BIZSTEP: {
|
|
|
94
94
|
};
|
|
95
95
|
export type EpcisEventType = 'ObjectEvent' | 'AggregationEvent' | 'TransactionEvent' | 'TransformationEvent';
|
|
96
96
|
export type EpcisAction = 'ADD' | 'OBSERVE' | 'DELETE';
|
|
97
|
+
/**
|
|
98
|
+
* **봉투에 실리는 EPCIS 사건 종류의 이름** — 소비처가 손으로 적지 않게 (2026-08-28).
|
|
99
|
+
*
|
|
100
|
+
* 봉투의 `eventType` 은 `epcis.<클래스>` 다(§`face2-adapter`·§`flow-engine` 이 그 접두사를 붙인다).
|
|
101
|
+
* 그 문자열을 화면·조회가 손으로 적으면 곧 방언이 되고, 클래스가 늘 때 한쪽만 고쳐진다.
|
|
102
|
+
*
|
|
103
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
104
|
+
* 「물건이 움직인 사건만 보는 목록」이 지금 **제외 목록**으로 만들어져 있다 — 운영·에너지 종류 열아홉
|
|
105
|
+
* 개를 적어 빼는 방식이다. 그 방식은 두 가지로 약하다.
|
|
106
|
+
*
|
|
107
|
+
* ① **닫히지 않는다.** 커널이 세 번째 계열을 만들면 그 계열 전부가 오류 없이 「움직임」으로 들어온다.
|
|
108
|
+
* 실제로 그 일이 세 번 있었다(사람 상태·자산 상태·에너지).
|
|
109
|
+
* ② **색인을 못 쓴다.** 제외 + 시각순은 색인이 듣지 않아, 그 트윈의 종류 분포가 비용을 정한다
|
|
110
|
+
* (실측: 에너지가 0.008%인 트윈에서 같은 모양이 37초였다).
|
|
111
|
+
*
|
|
112
|
+
* 담을 것을 말하면 정의가 닫힌다 — **물건이 움직인 사건 = EPCIS 사건**이고 그 클래스는 표준이 정한다.
|
|
113
|
+
* 커널이 채널을 늘려도 이 집합은 늘지 않는다.
|
|
114
|
+
*
|
|
115
|
+
* 우리 커널이 다루는 클래스는 넷이다. 표준의 `AssociationEvent` 는 아직 만들지 않았고, 만들면 이 상수에
|
|
116
|
+
* 더한다 — 그 한 곳만 고치면 소비처가 함께 따라온다.
|
|
117
|
+
*/
|
|
118
|
+
export declare const EPCIS_EVENT: {
|
|
119
|
+
readonly object: "epcis.ObjectEvent";
|
|
120
|
+
readonly aggregation: "epcis.AggregationEvent";
|
|
121
|
+
readonly transaction: "epcis.TransactionEvent";
|
|
122
|
+
readonly transformation: "epcis.TransformationEvent";
|
|
123
|
+
};
|
|
124
|
+
/** 봉투의 `eventType` 이 EPCIS 사건인가 — 접두사 하나로 판정한다(클래스가 늘어도 그대로 산다). */
|
|
125
|
+
export declare function isEpcisEventType(eventType: unknown): boolean;
|
|
97
126
|
/**
|
|
98
127
|
* 수량 요소 — **클래스 식별자 + 얼마나**. 표준이 세 경우를 규정한다(EPCIS 2.0 §7.3.3.1).
|
|
99
128
|
*
|
package/dist/epcis.js
CHANGED
|
@@ -101,6 +101,37 @@ export const CBV_BIZSTEP = {
|
|
|
101
101
|
*/
|
|
102
102
|
other: 'urn:epcglobal:cbv:bizstep:other'
|
|
103
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* **봉투에 실리는 EPCIS 사건 종류의 이름** — 소비처가 손으로 적지 않게 (2026-08-28).
|
|
106
|
+
*
|
|
107
|
+
* 봉투의 `eventType` 은 `epcis.<클래스>` 다(§`face2-adapter`·§`flow-engine` 이 그 접두사를 붙인다).
|
|
108
|
+
* 그 문자열을 화면·조회가 손으로 적으면 곧 방언이 되고, 클래스가 늘 때 한쪽만 고쳐진다.
|
|
109
|
+
*
|
|
110
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────────
|
|
111
|
+
* 「물건이 움직인 사건만 보는 목록」이 지금 **제외 목록**으로 만들어져 있다 — 운영·에너지 종류 열아홉
|
|
112
|
+
* 개를 적어 빼는 방식이다. 그 방식은 두 가지로 약하다.
|
|
113
|
+
*
|
|
114
|
+
* ① **닫히지 않는다.** 커널이 세 번째 계열을 만들면 그 계열 전부가 오류 없이 「움직임」으로 들어온다.
|
|
115
|
+
* 실제로 그 일이 세 번 있었다(사람 상태·자산 상태·에너지).
|
|
116
|
+
* ② **색인을 못 쓴다.** 제외 + 시각순은 색인이 듣지 않아, 그 트윈의 종류 분포가 비용을 정한다
|
|
117
|
+
* (실측: 에너지가 0.008%인 트윈에서 같은 모양이 37초였다).
|
|
118
|
+
*
|
|
119
|
+
* 담을 것을 말하면 정의가 닫힌다 — **물건이 움직인 사건 = EPCIS 사건**이고 그 클래스는 표준이 정한다.
|
|
120
|
+
* 커널이 채널을 늘려도 이 집합은 늘지 않는다.
|
|
121
|
+
*
|
|
122
|
+
* 우리 커널이 다루는 클래스는 넷이다. 표준의 `AssociationEvent` 는 아직 만들지 않았고, 만들면 이 상수에
|
|
123
|
+
* 더한다 — 그 한 곳만 고치면 소비처가 함께 따라온다.
|
|
124
|
+
*/
|
|
125
|
+
export const EPCIS_EVENT = {
|
|
126
|
+
object: 'epcis.ObjectEvent',
|
|
127
|
+
aggregation: 'epcis.AggregationEvent',
|
|
128
|
+
transaction: 'epcis.TransactionEvent',
|
|
129
|
+
transformation: 'epcis.TransformationEvent'
|
|
130
|
+
};
|
|
131
|
+
/** 봉투의 `eventType` 이 EPCIS 사건인가 — 접두사 하나로 판정한다(클래스가 늘어도 그대로 산다). */
|
|
132
|
+
export function isEpcisEventType(eventType) {
|
|
133
|
+
return typeof eventType === 'string' && eventType.startsWith('epcis.');
|
|
134
|
+
}
|
|
104
135
|
// ── GS1 EPC URI 헬퍼 (표준) ────────────────────────────────────────────────
|
|
105
136
|
/** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
|
|
106
137
|
export function ssccUri(companyPrefix, serial) {
|
package/dist/event-journal.d.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { TwinModelDef, CanonicalEnvelope, StructureShift } from './contract.ts';
|
|
2
2
|
import { type ProjectedState, type ReducerCheckpoint } from './state-projector.ts';
|
|
3
|
+
/** 저장된 기록을 처리할 것 — 도메인 커널 또는 `StateProjector`. 계약은 넷뿐이다. */
|
|
4
|
+
export interface JournalProjector {
|
|
5
|
+
apply(e: CanonicalEnvelope): void;
|
|
6
|
+
snapshot(): ProjectedState;
|
|
7
|
+
serialize(): ReducerCheckpoint;
|
|
8
|
+
restore(cp: ReducerCheckpoint): void;
|
|
9
|
+
/** 공장이 바뀐 경계에서 구조를 갈아탄다 — 여러 마디를 이어 처리할 때만 쓴다(§`replaySegments`). */
|
|
10
|
+
adoptStructure(model: TwinModelDef): StructureShift;
|
|
11
|
+
}
|
|
12
|
+
/** 어떤 종류로 처리할지 부르는 쪽이 말한다 — 모델에는 종류가 없다(호스트가 든다). */
|
|
13
|
+
export interface ReplayOptions {
|
|
14
|
+
/** `wms` · `yms` · `mes` · `ems`. 없으면 도메인 규칙 없이 처리한다. */
|
|
15
|
+
kind?: string;
|
|
16
|
+
/** 생산 명세(`productionSpec`) — 도메인 커널이 받는다. */
|
|
17
|
+
productionSpec?: unknown;
|
|
18
|
+
/** 테넌트 — 커널 생성자가 받는다. 저장된 기록을 처리하는 데는 쓰이지 않지만 계약이 요구한다. */
|
|
19
|
+
tenantId?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function journalProjector(model: TwinModelDef, opts?: ReplayOptions): JournalProjector;
|
|
3
22
|
export declare class EventJournal {
|
|
4
23
|
private events;
|
|
5
24
|
/** 이벤트 1건 기록(추가 전용). runtime/kernel 의 onEvent 에 연결. */
|
|
@@ -14,7 +33,7 @@ export declare class EventJournal {
|
|
|
14
33
|
untilSimTime(iso: string): CanonicalEnvelope[];
|
|
15
34
|
}
|
|
16
35
|
/** 이벤트열 → 상태 재구성(시간여행). `model` = 그 시점의 트윈 모델(토폴로지·자원). */
|
|
17
|
-
export declare function replay(model: TwinModelDef, events: readonly CanonicalEnvelope[]): ProjectedState;
|
|
36
|
+
export declare function replay(model: TwinModelDef, events: readonly CanonicalEnvelope[], opts?: ReplayOptions): ProjectedState;
|
|
18
37
|
/**
|
|
19
38
|
* **재개점에서 이어서 계산한다** — 0부터 다시 계산하지 않는다.
|
|
20
39
|
*
|
|
@@ -29,12 +48,12 @@ export declare function replay(model: TwinModelDef, events: readonly CanonicalEn
|
|
|
29
48
|
* 구조가 바뀐 구간은 여기서 다루지 않는다(`replaySegments` 의 몫이다) — 재개점은 **한 구조 안에서**
|
|
30
49
|
* 이어 붙이는 것이다. 구조가 바뀌었으면 부르는 쪽이 그 경계에서 갈라야 한다.
|
|
31
50
|
*/
|
|
32
|
-
export declare function replayFrom(model: TwinModelDef, checkpoint: ReducerCheckpoint, events: readonly CanonicalEnvelope[]): {
|
|
51
|
+
export declare function replayFrom(model: TwinModelDef, checkpoint: ReducerCheckpoint, events: readonly CanonicalEnvelope[], opts?: ReplayOptions): {
|
|
33
52
|
state: ProjectedState;
|
|
34
53
|
checkpoint: ReducerCheckpoint;
|
|
35
54
|
};
|
|
36
55
|
/** 이벤트열을 계산하고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
|
|
37
|
-
export declare function replayWithCheckpoint(model: TwinModelDef, events: readonly CanonicalEnvelope[]): {
|
|
56
|
+
export declare function replayWithCheckpoint(model: TwinModelDef, events: readonly CanonicalEnvelope[], opts?: ReplayOptions): {
|
|
38
57
|
state: ProjectedState;
|
|
39
58
|
checkpoint: ReducerCheckpoint;
|
|
40
59
|
};
|
|
@@ -73,7 +92,7 @@ export interface SegmentShift extends StructureShift {
|
|
|
73
92
|
*
|
|
74
93
|
* 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
|
|
75
94
|
*/
|
|
76
|
-
export declare function replaySegments(segments: readonly StructureSegment[]): {
|
|
95
|
+
export declare function replaySegments(segments: readonly StructureSegment[], opts?: ReplayOptions): {
|
|
77
96
|
state: ProjectedState;
|
|
78
97
|
shifts: SegmentShift[];
|
|
79
98
|
};
|
package/dist/event-journal.js
CHANGED
|
@@ -7,6 +7,46 @@
|
|
|
7
7
|
* (이벤트-소싱: 상태는 항상 이벤트열의 함수. state-projector 가 폴딩 함수.)
|
|
8
8
|
*/
|
|
9
9
|
import { StateProjector } from "./state-projector.js";
|
|
10
|
+
import { WmsKernel } from "./kernel.js";
|
|
11
|
+
import { YmsKernel } from "./yms-kernel.js";
|
|
12
|
+
import { MesKernel } from "./mes-kernel.js";
|
|
13
|
+
import { EmsKernel } from "./ems-kernel.js";
|
|
14
|
+
/**
|
|
15
|
+
* 저장된 기록으로 상태를 만드는 것 — **도메인 커널이 있으면 그것을 쓴다** (2026-08-27).
|
|
16
|
+
*
|
|
17
|
+
* ── 무엇이 틀렸나 ───────────────────────────────────────────────────────────
|
|
18
|
+
* 이 파일의 함수들이 `StateProjector` 하나만 세웠다. 그런데 트윈이 도는 동안 사건을 처리하는 것은
|
|
19
|
+
* 도메인 커널이다(호스트가 `new EmsKernel(...)` 을 세워 `apply` 를 부른다). 두 코드가 아는 사건이
|
|
20
|
+
* 다르다 — `StateProjector` 는 `energy.*` 를 「담을 줄 모르는 사건」으로 세기만 한다.
|
|
21
|
+
*
|
|
22
|
+
* 그래서 같은 저장 기록에서 두 답이 나왔다. 과거 시점을 물으면(`recover`) 발전량과 계량이 늘 비어
|
|
23
|
+
* 있었다. 받아서 저장까지 해 둔 값인데 읽는 쪽이 건너뛴 것이다.
|
|
24
|
+
*
|
|
25
|
+
* 종류를 모르면 `StateProjector` 를 쓴다 — 부르는 쪽이 종류를 말해 주지 않는 경우가 있고, 그때
|
|
26
|
+
* 아무 커널이나 골라 세우면 없는 규칙이 상태에 섞인다.
|
|
27
|
+
*/
|
|
28
|
+
const DOMAIN_KERNELS = { wms: WmsKernel, yms: YmsKernel, mes: MesKernel, ems: EmsKernel };
|
|
29
|
+
export function journalProjector(model, opts) {
|
|
30
|
+
const Kernel = opts?.kind ? DOMAIN_KERNELS[opts.kind] : undefined;
|
|
31
|
+
if (!Kernel)
|
|
32
|
+
return new StateProjector(model);
|
|
33
|
+
/*
|
|
34
|
+
* 라이브가 세우는 순서와 **같은 순서**로 세운다(호스트 `startLive`). 순서가 다르면 같은 코드라도
|
|
35
|
+
* 다른 답이 나온다 — 모델을 싣기 전에 사건이 들어오면 없는 자리에 담긴다.
|
|
36
|
+
*/
|
|
37
|
+
const kernel = new Kernel(String(opts?.tenantId ?? 'replay'), undefined, opts?.productionSpec);
|
|
38
|
+
kernel.loadTwinModel(model);
|
|
39
|
+
/* 관측 구동임을 세울 때 말한다 — 말하지 않으면 커널이 시뮬레이션으로 여기고 원천의 틈에서 멈춘다. */
|
|
40
|
+
if (typeof kernel.observe === 'function')
|
|
41
|
+
kernel.observe();
|
|
42
|
+
return {
|
|
43
|
+
apply: (e) => kernel.apply(e),
|
|
44
|
+
snapshot: () => kernel.getSnapshot(),
|
|
45
|
+
serialize: () => kernel.observedCheckpoint(),
|
|
46
|
+
restore: (cp) => kernel.restoreObserved(cp),
|
|
47
|
+
adoptStructure: (m) => kernel.adoptStructure(m)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
10
50
|
export class EventJournal {
|
|
11
51
|
events = [];
|
|
12
52
|
/** 이벤트 1건 기록(추가 전용). runtime/kernel 의 onEvent 에 연결. */
|
|
@@ -33,8 +73,8 @@ export class EventJournal {
|
|
|
33
73
|
}
|
|
34
74
|
}
|
|
35
75
|
/** 이벤트열 → 상태 재구성(시간여행). `model` = 그 시점의 트윈 모델(토폴로지·자원). */
|
|
36
|
-
export function replay(model, events) {
|
|
37
|
-
const proj =
|
|
76
|
+
export function replay(model, events, opts) {
|
|
77
|
+
const proj = journalProjector(model, opts);
|
|
38
78
|
for (const e of events)
|
|
39
79
|
proj.apply(e);
|
|
40
80
|
return proj.snapshot();
|
|
@@ -53,16 +93,16 @@ export function replay(model, events) {
|
|
|
53
93
|
* 구조가 바뀐 구간은 여기서 다루지 않는다(`replaySegments` 의 몫이다) — 재개점은 **한 구조 안에서**
|
|
54
94
|
* 이어 붙이는 것이다. 구조가 바뀌었으면 부르는 쪽이 그 경계에서 갈라야 한다.
|
|
55
95
|
*/
|
|
56
|
-
export function replayFrom(model, checkpoint, events) {
|
|
57
|
-
const proj =
|
|
96
|
+
export function replayFrom(model, checkpoint, events, opts) {
|
|
97
|
+
const proj = journalProjector(model, opts);
|
|
58
98
|
proj.restore(checkpoint);
|
|
59
99
|
for (const e of events)
|
|
60
100
|
proj.apply(e);
|
|
61
101
|
return { state: proj.snapshot(), checkpoint: proj.serialize() };
|
|
62
102
|
}
|
|
63
103
|
/** 이벤트열을 계산하고 **재개점도 함께** 낸다 — 다음 번에 이어 붙일 수 있게. */
|
|
64
|
-
export function replayWithCheckpoint(model, events) {
|
|
65
|
-
const proj =
|
|
104
|
+
export function replayWithCheckpoint(model, events, opts) {
|
|
105
|
+
const proj = journalProjector(model, opts);
|
|
66
106
|
for (const e of events)
|
|
67
107
|
proj.apply(e);
|
|
68
108
|
return { state: proj.snapshot(), checkpoint: proj.serialize() };
|
|
@@ -79,10 +119,10 @@ export function replayWithCheckpoint(model, events) {
|
|
|
79
119
|
*
|
|
80
120
|
* 경계에서 사라진 자원은 결과에 실어 보낸다 — 수가 줄어든 것을 사용자가 눈치채지 못하면 안 된다.
|
|
81
121
|
*/
|
|
82
|
-
export function replaySegments(segments) {
|
|
122
|
+
export function replaySegments(segments, opts) {
|
|
83
123
|
if (!segments.length)
|
|
84
124
|
throw new Error('재생할 마디가 없다 — 구조를 하나도 주지 않았다');
|
|
85
|
-
const proj =
|
|
125
|
+
const proj = journalProjector(segments[0].model, opts);
|
|
86
126
|
const shifts = [];
|
|
87
127
|
for (let i = 0; i < segments.length; i++) {
|
|
88
128
|
/* 첫 마디는 생성자가 이미 그 구조로 섰다 — 두 번 세우면 관측 전 상태를 다시 덮는다. */
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, OeeMetrics, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation } from './contract.ts';
|
|
2
|
+
import type { VocabularyElement } from './master-data.ts';
|
|
2
3
|
import type { ReducerCheckpoint } from './observed-reducer.ts';
|
|
3
4
|
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
4
5
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
@@ -964,6 +965,16 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
964
965
|
*
|
|
965
966
|
* `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
|
|
966
967
|
*/
|
|
968
|
+
/**
|
|
969
|
+
* **마스터데이터를 받는다** — 변하지 않는 속성을 상태에 세운다(§`master-data`).
|
|
970
|
+
*
|
|
971
|
+
* 사건이 아니다. 그래서 구독자에게 흘리지 않고(저널에 적히지 않게) 상태만 세운다. 유통기한·로트번호가
|
|
972
|
+
* 이 문으로 온다 — 그 값이 갈 자리가 없어서 전량을 읽는 원본은 「들어오지 않은 것을 들어왔다」고
|
|
973
|
+
* 말해야 했다.
|
|
974
|
+
*
|
|
975
|
+
* 관측 모드가 아니면 아무 일도 하지 않는다(시뮬은 자기 상태를 자기가 만든다) — 그 사실을 수로 낸다.
|
|
976
|
+
*/
|
|
977
|
+
applyMasterData(elements: readonly VocabularyElement[]): number;
|
|
967
978
|
apply(envelope: CanonicalEnvelope): void;
|
|
968
979
|
/** 관측분을 커널 상태로 옮긴다 — 필요할 때 한 번만(같은 규칙, 같은 씨앗 경로). */
|
|
969
980
|
private settleObserved;
|
package/dist/flow-engine.js
CHANGED
|
@@ -693,7 +693,23 @@ export class FlowEngine {
|
|
|
693
693
|
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
694
694
|
*/
|
|
695
695
|
observedCheckpoint() {
|
|
696
|
-
|
|
696
|
+
/*
|
|
697
|
+
* 관측 구동인데 리듀서가 아직 없으면 만든다 (2026-08-27).
|
|
698
|
+
*
|
|
699
|
+
* 리듀서는 첫 봉투가 이 클래스의 `apply` 에 닿을 때 만들어진다. 그런데 도메인 커널이 그 봉투를
|
|
700
|
+
* 가로채고 돌아가면(에너지 사건이 그렇다) 리듀서가 만들어지지 않는다. 그러면 이 함수가
|
|
701
|
+
* `undefined` 를 내고, 호스트는 재개점이 없다고 보아 다음 조회마다 저장 기록을 처음부터 다시
|
|
702
|
+
* 처리한다 — 에너지만 들어오는 트윈에서 실제로 그랬다.
|
|
703
|
+
*/
|
|
704
|
+
if (!this.observer && this.observeMode) {
|
|
705
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
706
|
+
}
|
|
707
|
+
const cp = this.observer?.serialize();
|
|
708
|
+
if (!cp)
|
|
709
|
+
return undefined;
|
|
710
|
+
/* 도메인 커널이 따로 드는 상태를 함께 담는다 — 없으면 그 칸을 만들지 않는다. */
|
|
711
|
+
const domain = typeof this.serializeDomain === 'function' ? this.serializeDomain() : undefined;
|
|
712
|
+
return domain === undefined ? cp : { ...cp, domain };
|
|
697
713
|
}
|
|
698
714
|
/**
|
|
699
715
|
* **재개점에서 관측 리듀서를 되세운다** — 저널을 0부터 다시 집계하지 않게.
|
|
@@ -713,6 +729,11 @@ export class FlowEngine {
|
|
|
713
729
|
this.observeMode = true;
|
|
714
730
|
}
|
|
715
731
|
this.observer.restore(cp);
|
|
732
|
+
/* 도메인 커널이 따로 드는 상태를 되돌린다 — 담기지 않았으면 되돌릴 것도 없다. */
|
|
733
|
+
if (cp?.domain !== undefined && typeof this.restoreDomain === 'function') {
|
|
734
|
+
;
|
|
735
|
+
this.restoreDomain(cp.domain);
|
|
736
|
+
}
|
|
716
737
|
this.observedDirty = true;
|
|
717
738
|
this.settleObserved();
|
|
718
739
|
}
|
|
@@ -2073,6 +2094,27 @@ export class FlowEngine {
|
|
|
2073
2094
|
*
|
|
2074
2095
|
* `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
|
|
2075
2096
|
*/
|
|
2097
|
+
/**
|
|
2098
|
+
* **마스터데이터를 받는다** — 변하지 않는 속성을 상태에 세운다(§`master-data`).
|
|
2099
|
+
*
|
|
2100
|
+
* 사건이 아니다. 그래서 구독자에게 흘리지 않고(저널에 적히지 않게) 상태만 세운다. 유통기한·로트번호가
|
|
2101
|
+
* 이 문으로 온다 — 그 값이 갈 자리가 없어서 전량을 읽는 원본은 「들어오지 않은 것을 들어왔다」고
|
|
2102
|
+
* 말해야 했다.
|
|
2103
|
+
*
|
|
2104
|
+
* 관측 모드가 아니면 아무 일도 하지 않는다(시뮬은 자기 상태를 자기가 만든다) — 그 사실을 수로 낸다.
|
|
2105
|
+
*/
|
|
2106
|
+
applyMasterData(elements) {
|
|
2107
|
+
if (!elements.length)
|
|
2108
|
+
return 0;
|
|
2109
|
+
if (!this.observer) {
|
|
2110
|
+
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
2111
|
+
this.observeMode = true;
|
|
2112
|
+
}
|
|
2113
|
+
const n = this.observer.applyMasterData(elements);
|
|
2114
|
+
/* 상태가 바뀌었으므로 다음 스냅샷·fork 에서 옮겨져야 한다 — 관측 반영과 같은 규율이다. */
|
|
2115
|
+
this.observedDirty = true;
|
|
2116
|
+
return n;
|
|
2117
|
+
}
|
|
2076
2118
|
apply(envelope) {
|
|
2077
2119
|
if (!this.observer) {
|
|
2078
2120
|
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './counterfactual.ts';
|
|
|
5
5
|
export * from './forecast.ts';
|
|
6
6
|
export * from './twin-observer.ts';
|
|
7
7
|
export * from './event-journal.ts';
|
|
8
|
+
export * from './master-data.ts';
|
|
8
9
|
export * from './divergence.ts';
|
|
9
10
|
export * from './epcis.ts';
|
|
10
11
|
export * from './wms-profile.ts';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./counterfactual.js";
|
|
|
5
5
|
export * from "./forecast.js";
|
|
6
6
|
export * from "./twin-observer.js";
|
|
7
7
|
export * from "./event-journal.js";
|
|
8
|
+
export * from "./master-data.js";
|
|
8
9
|
export * from "./divergence.js";
|
|
9
10
|
export * from "./epcis.js";
|
|
10
11
|
export * from "./wms-profile.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** 표준 어휘 타입 — 이 세 가지가 우리 상태 축에 닿는다. 그 밖은 보관하고 알린다. */
|
|
2
|
+
export declare const VOCABULARY_TYPE: {
|
|
3
|
+
/** 품목·로트 클래스의 속성 — 유통기한·로트번호가 여기 온다. */
|
|
4
|
+
readonly epcClass: "urn:epcglobal:epcis:vtype:EPCClass";
|
|
5
|
+
/** 관측 지점의 속성. */
|
|
6
|
+
readonly readPoint: "urn:epcglobal:epcis:vtype:ReadPoint";
|
|
7
|
+
/** 업무 자리의 속성. */
|
|
8
|
+
readonly bizLocation: "urn:epcglobal:epcis:vtype:BizLocation";
|
|
9
|
+
};
|
|
10
|
+
export type VocabularyType = (typeof VOCABULARY_TYPE)[keyof typeof VOCABULARY_TYPE];
|
|
11
|
+
/** 어휘 요소 하나 — 표준의 모양. */
|
|
12
|
+
export interface VocabularyElement {
|
|
13
|
+
/** 그 어휘에서의 식별자(클래스 식별자·자리 식별자). */
|
|
14
|
+
id: string;
|
|
15
|
+
/** 속성 — 이름을 해석하지 않고 그대로 든다. */
|
|
16
|
+
attributes: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 원본이 보내는 마스터데이터 한 건 — 어휘 타입과 함께 온다.
|
|
20
|
+
*
|
|
21
|
+
* 타입을 **받는다.** 로트 전용 문을 만들지 않는 이유는 자리·품목이 같은 통로로 들어와야 하고, 표준이
|
|
22
|
+
* 이미 타입으로 갈라 두었기 때문이다.
|
|
23
|
+
*/
|
|
24
|
+
export interface MasterDataRecord extends VocabularyElement {
|
|
25
|
+
vocabularyType: string;
|
|
26
|
+
}
|
|
27
|
+
/** 이 레코드가 마스터데이터인가 — 갈래 판정은 **커널이 한 곳에서** 한다(어휘 넷을 가르는 방식과 같다). */
|
|
28
|
+
export declare function isMasterDataRecord(record: unknown): record is MasterDataRecord;
|
|
29
|
+
export interface MasterDataResult {
|
|
30
|
+
accepted: MasterDataRecord[];
|
|
31
|
+
rejected: {
|
|
32
|
+
record: unknown;
|
|
33
|
+
errors: string[];
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 마스터데이터를 받아들인다 — **거부한 것은 이유와 함께 남는다**(다른 어휘와 같은 규율).
|
|
38
|
+
*
|
|
39
|
+
* 빈 속성은 거부한다: 「이 로트에 대해 아무것도 모른다」를 보내는 것은 아무 뜻이 없고, 받아 두면
|
|
40
|
+
* 기존 값을 지우려는 것인지 아무 말도 아닌지 구별할 수 없다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function ingestMasterData(records: MasterDataRecord | MasterDataRecord[] | undefined | null): MasterDataResult;
|
|
43
|
+
/**
|
|
44
|
+
* 클래스 단위 마스터 표 — 관측 리듀서가 든다.
|
|
45
|
+
*
|
|
46
|
+
* 물품은 자리마다 따로 서지만(`클래스@자리`) 이 속성은 **클래스의 것**이다. 그래서 물품마다 복사해
|
|
47
|
+
* 두지 않고 클래스로 한 번 들고, 물품을 세울 때 본다. 마스터가 관측보다 늦게 와도 값이 서는 이유다.
|
|
48
|
+
*/
|
|
49
|
+
export declare class ClassMasterTable {
|
|
50
|
+
private byId;
|
|
51
|
+
/**
|
|
52
|
+
* 속성을 **합친다** — 덮지 않는다(한 번에 일부만 오는 원본이 있다). 바뀐 식별자를 돌려준다.
|
|
53
|
+
*
|
|
54
|
+
* ── 지우는 법 ───────────────────────────────────────────────────────────────
|
|
55
|
+
* 값에 `null` 을 실으면 **그 속성을 지운다**(「모름으로 되돌린다」). 합치기만 하면 원본에서 지운 값이
|
|
56
|
+
* 트윈에 영원히 남고, 그것을 표현할 방법이 없으면 「지웠다」와 「이번엔 말하지 않았다」가 같아진다.
|
|
57
|
+
*
|
|
58
|
+
* `undefined` 는 지우지 않는다 — 그것은 「이 진술에 그 속성이 없다」이고, 원본이 일부만 보내는
|
|
59
|
+
* 정상이다. 두 뜻을 갈라 둔다.
|
|
60
|
+
*
|
|
61
|
+
* 속성이 하나도 남지 않으면 그 요소를 표에서 버린다(빈 껍데기를 들고 있지 않는다).
|
|
62
|
+
*/
|
|
63
|
+
merge(elements: readonly VocabularyElement[]): string[];
|
|
64
|
+
/**
|
|
65
|
+
* 그 식별자의 항목을 버린다 — **아무도 가리키지 않게 된 뒤**에만 부른다.
|
|
66
|
+
*
|
|
67
|
+
* 로트는 계속 새로 생기므로 이 표는 스스로 줄지 않는다. 하루 수백 로트면 1년에 수십만 항목이고,
|
|
68
|
+
* 그것을 아무도 지우지 않으면 재기동으로만 줄어든다(그리고 중간 저장본이 그만큼 커진다).
|
|
69
|
+
*/
|
|
70
|
+
forget(id: string): boolean;
|
|
71
|
+
attributesOf(id: string | undefined): Record<string, unknown> | undefined;
|
|
72
|
+
get size(): number;
|
|
73
|
+
serialize(): Record<string, Record<string, unknown>>;
|
|
74
|
+
restore(saved: unknown): void;
|
|
75
|
+
clear(): void;
|
|
76
|
+
}
|
|
77
|
+
/** 마스터 속성에서 유통기한을 읽는다 — 사건의 `ilmd` 와 **같은 이름**을 본다. */
|
|
78
|
+
export declare function expiryFromAttributes(attrs: Record<string, unknown> | undefined): number | undefined;
|
|
79
|
+
/** 마스터 속성에서 로트 번호를 읽는다 — 사건의 `ilmd` 와 같은 이름. */
|
|
80
|
+
export declare function lotFromAttributes(attrs: Record<string, unknown> | undefined): string | undefined;
|