@operato/twin-kernel 0.1.0 → 0.2.0
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/contract.d.ts +209 -1
- package/dist/contract.js +4 -0
- package/dist/counterfactual.d.ts +2 -0
- package/dist/counterfactual.js +7 -3
- package/dist/domain-definition.d.ts +84 -1
- package/dist/domain-definition.js +11 -0
- package/dist/duration-estimator.d.ts +26 -2
- package/dist/epcis.d.ts +185 -6
- package/dist/epcis.js +175 -12
- package/dist/flow-engine.d.ts +171 -3
- package/dist/flow-engine.js +522 -21
- package/dist/forecast.d.ts +8 -0
- package/dist/forecast.js +9 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/iso-duration.d.ts +5 -0
- package/dist/iso-duration.js +43 -0
- package/dist/kernel.js +8 -2
- package/dist/mes-kernel.d.ts +2 -2
- package/dist/mes-kernel.js +18 -9
- package/dist/state-projector.d.ts +65 -2
- package/dist/state-projector.js +241 -17
- package/dist/twin-observer.d.ts +2 -0
- package/dist/twin-observer.js +9 -3
- package/dist-cjs/index.cjs +950 -158
- package/package.json +1 -1
package/dist/forecast.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export interface ForecastTwin {
|
|
|
5
5
|
tick(dtMs: number): void;
|
|
6
6
|
scenario: ScenarioControl;
|
|
7
7
|
fork(): ForecastTwin;
|
|
8
|
+
/**
|
|
9
|
+
* 시뮬 시각(ms) — **구동 루프가 시각만 읽을 때 쓰는 값.**
|
|
10
|
+
*
|
|
11
|
+
* 없으면 `getSnapshot().simClockMs` 로 떨어지지만, 그 경로는 전 노드·물품·자원·작업·오더를 새로
|
|
12
|
+
* 재료화하고 주목 신호까지 계산한 뒤 숫자 하나만 꺼내 버린다 — tick 마다 그러면 상태 크기 × 지평선
|
|
13
|
+
* 길이만큼 낭비가 쌓인다(물품 1만 건·30분 지평선에서 측정: 루프 조건에만 149ms vs 0ms).
|
|
14
|
+
*/
|
|
15
|
+
clockMs?: number;
|
|
8
16
|
}
|
|
9
17
|
export interface MonteCarloResult {
|
|
10
18
|
runs: number;
|
package/dist/forecast.js
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
* 관심 지표를 분포로 준다: "언제 끝나?"가 아니라 "P50/P90 완료시각·재고소진 확률".
|
|
6
6
|
* fork(현재예측) + 시나리오 재시드로 미래만 변주 — 현재 상태(재고·오더·태스크)는 보존.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* 구동 루프용 시각 읽기 — **숫자 하나 때문에 전체 상태를 만들지 않는다.**
|
|
10
|
+
* 커널은 `clockMs` 를 공개하므로 그것을 쓰고, 그것이 없는 외부 구현만 스냅샷으로 떨어진다.
|
|
11
|
+
*/
|
|
12
|
+
function clockOf(twin) {
|
|
13
|
+
return typeof twin.clockMs === 'number' ? twin.clockMs : twin.getSnapshot().simClockMs;
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* 현재 상태에서 N개 확률적 미래를 표본화 → 지표 분포.
|
|
10
17
|
* run 마다 fork(현재 보존) + scenario.load(seed+i)(미래 변주) + horizon 까지 구동. 원본 무간섭.
|
|
11
18
|
*/
|
|
12
19
|
export function monteCarloForecast(twin, opts) {
|
|
13
|
-
const now = twin
|
|
20
|
+
const now = clockOf(twin);
|
|
14
21
|
const step = opts.tickMs ?? 1000;
|
|
15
22
|
const baseSeed = opts.scenario.seed ?? 1;
|
|
16
23
|
const samples = [];
|
|
@@ -20,7 +27,7 @@ export function monteCarloForecast(twin, opts) {
|
|
|
20
27
|
fc.scenario.start();
|
|
21
28
|
const target = now + opts.horizonMs;
|
|
22
29
|
let guard = 0;
|
|
23
|
-
while (fc
|
|
30
|
+
while (clockOf(fc) < target && guard++ < 1_000_000)
|
|
24
31
|
fc.tick(step);
|
|
25
32
|
samples.push(opts.metric(fc.getSnapshot()));
|
|
26
33
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './capability.ts';
|
|
|
11
11
|
export * from './domain-catalog.ts';
|
|
12
12
|
export * from './allocation-policy.ts';
|
|
13
13
|
export * from './duration-estimator.ts';
|
|
14
|
+
export * from './iso-duration.ts';
|
|
14
15
|
export * from './state-projector.ts';
|
|
15
16
|
export * from './task-fold.ts';
|
|
16
17
|
export * from './face2-adapter.ts';
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./capability.js";
|
|
|
11
11
|
export * from "./domain-catalog.js";
|
|
12
12
|
export * from "./allocation-policy.js";
|
|
13
13
|
export * from "./duration-estimator.js";
|
|
14
|
+
export * from "./iso-duration.js";
|
|
14
15
|
export * from "./state-projector.js";
|
|
15
16
|
export * from "./task-fold.js";
|
|
16
17
|
export * from "./face2-adapter.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ISO 8601 기간 파서 — **표준 표기를 그대로 받기 위한 최소 도구.**
|
|
3
|
+
*
|
|
4
|
+
* ISA-95 의 `OperationsSegment.Duration` 은 `xsd:duration` 이다(B2MML `DurationType` =
|
|
5
|
+
* `<xsd:restriction base="xsd:duration"/>`). 그래서 명세가 말하는 소요시간은 `PT12M`·`PT1H15M`
|
|
6
|
+
* 같은 문자열로 온다 — 우리가 숫자 밀리초로 바꿔 부르는 순간 표준과 어긋나므로, **계약은 표준 표기로
|
|
7
|
+
* 받고 여기서 한 번만 해석한다**(소비처가 문자열을 자르지 않게).
|
|
8
|
+
*
|
|
9
|
+
* 지원 범위: `PnYnMnWnDTnHnMnS` 의 주·일·시·분·초(소수 초 포함). **연·월은 거부한다** —
|
|
10
|
+
* 길이가 달력에 따라 달라져 밀리초로 확정할 수 없다(28~31일). 모르면 꾸미지 않고 undefined 를 낸다.
|
|
11
|
+
*/
|
|
12
|
+
const RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
|
|
13
|
+
/**
|
|
14
|
+
* ISO 8601 기간 → 밀리초. 해석할 수 없으면 **undefined**(0 이 아니다 — "0초" 와 "모른다" 는 다르다).
|
|
15
|
+
* 연·월(`Y`·`P…M` 의 날짜부 M)은 달력 의존이라 거부한다.
|
|
16
|
+
*/
|
|
17
|
+
export function parseIsoDuration(text) {
|
|
18
|
+
if (typeof text !== 'string')
|
|
19
|
+
return undefined;
|
|
20
|
+
const s = text.trim();
|
|
21
|
+
if (!s || s === 'P' || s === 'PT')
|
|
22
|
+
return undefined;
|
|
23
|
+
if (/\d+Y/.test(s))
|
|
24
|
+
return undefined; // 연 — 달력 의존
|
|
25
|
+
/* 날짜부의 M(월)과 시간부의 M(분)을 구별한다: T 앞의 M 은 월이므로 거부. */
|
|
26
|
+
const tIdx = s.indexOf('T');
|
|
27
|
+
const datePart = tIdx === -1 ? s : s.slice(0, tIdx);
|
|
28
|
+
if (/\d+M/.test(datePart))
|
|
29
|
+
return undefined; // 월 — 달력 의존
|
|
30
|
+
const m = RE.exec(s);
|
|
31
|
+
if (!m)
|
|
32
|
+
return undefined;
|
|
33
|
+
const [, sign, w, d, h, min, sec] = m;
|
|
34
|
+
if (!w && !d && !h && !min && !sec)
|
|
35
|
+
return undefined;
|
|
36
|
+
const ms = (Number(w ?? 0) * 7 + Number(d ?? 0)) * 86_400_000 +
|
|
37
|
+
Number(h ?? 0) * 3_600_000 +
|
|
38
|
+
Number(min ?? 0) * 60_000 +
|
|
39
|
+
Number(sec ?? 0) * 1000;
|
|
40
|
+
if (!Number.isFinite(ms))
|
|
41
|
+
return undefined;
|
|
42
|
+
return sign ? -ms : ms;
|
|
43
|
+
}
|
package/dist/kernel.js
CHANGED
|
@@ -9,7 +9,7 @@ import { CMD } from "./contract.js";
|
|
|
9
9
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
10
10
|
import { FlowEngine } from "./flow-engine.js";
|
|
11
11
|
import { BIZSTEP, BTT } from "./wms-profile.js";
|
|
12
|
-
import { DISP, aggregationEvent, gdtiUri, objectEvent, ssccUri, transactionEvent } from "./epcis.js";
|
|
12
|
+
import { DISP, ILMD_ATTR, aggregationEvent, gdtiUri, objectEvent, ssccUri, transactionEvent } from "./epcis.js";
|
|
13
13
|
const TRAVEL_MS = 30_000;
|
|
14
14
|
const COMPANY_PREFIX = '0614141';
|
|
15
15
|
const SHELF_MS = 30 * 24 * 3_600_000; // 기본 유통기한(30일)
|
|
@@ -37,7 +37,13 @@ export class WmsKernel extends FlowEngine {
|
|
|
37
37
|
dock.occupancy++;
|
|
38
38
|
this.emit(transactionEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
|
|
39
39
|
this.emit(aggregationEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
|
|
40
|
-
|
|
40
|
+
/* 만료는 이 팔레트가 **생겨나는 순간** 정해지는 태생 속성이라 개체·로트 마스터데이터로 싣는다
|
|
41
|
+
* (§7.3.8: ObjectEvent action=ADD 에만 허용). 예전에는 커널 내부 상태에만 두어 미러가 알 수 없었다. */
|
|
42
|
+
this.emit(objectEvent({
|
|
43
|
+
eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, disposition: DISP.in_progress,
|
|
44
|
+
epcList: [epc], quantityList: qtyList, readPoint: dock.id, bizLocation: dock.id, bizTransactionList: poTxn,
|
|
45
|
+
ilmd: { [ILMD_ATTR.expiry]: expiry }
|
|
46
|
+
}));
|
|
41
47
|
const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews('storage') });
|
|
42
48
|
if (!binId)
|
|
43
49
|
return; // 수용 불가 → 도크 대기
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { GeneratorSpec, Command, CommandAck } from './contract.ts';
|
|
|
2
2
|
import type { AllocationPolicy } from './allocation-policy.ts';
|
|
3
3
|
import { FlowEngine } from './flow-engine.ts';
|
|
4
4
|
import type { FlowOrder, FlowTask } from './flow-engine.ts';
|
|
5
|
-
import type
|
|
5
|
+
import { type DomainDefinition } from './domain-definition.ts';
|
|
6
6
|
/** 편의 — 시나리오 skuMix 로 쓸 부품 클래스. */
|
|
7
7
|
export declare const MES_PART_GTINS: {
|
|
8
8
|
partA: string;
|
|
@@ -40,7 +40,7 @@ export declare class MesKernel extends FlowEngine {
|
|
|
40
40
|
/**
|
|
41
41
|
* MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
|
|
42
42
|
* 자동 체인지오버(task.changeoverKey 상이 시 셋업)의 수동 버전 — 운영자가 사전 전환(툴링 교체) 지시.
|
|
43
|
-
* 이미 그 제품이면 no-op, 아니면 셋업(
|
|
43
|
+
* 이미 그 제품이면 no-op, 아니면 셋업(기본값, OEE 가용성 손실) + lastChangeoverKey 각인
|
|
44
44
|
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
45
45
|
*/
|
|
46
46
|
protected handleCommand(cmd: Command): CommandAck;
|
package/dist/mes-kernel.js
CHANGED
|
@@ -12,14 +12,18 @@ import { firstFitPolicy } from "./allocation-policy.js";
|
|
|
12
12
|
import { FlowEngine } from "./flow-engine.js";
|
|
13
13
|
import { DISP, objectEvent, transactionEvent, gdtiUri, sgtinClass } from "./epcis.js";
|
|
14
14
|
import { MES_BIZSTEP, BTT_PRODORDER, sgtinUri } from "./mes-profile.js";
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { OP_PARAM } from "./domain-definition.js";
|
|
16
|
+
/* 아래 셋은 **기본값**이다 — 명세(OperationDef.duration·parameters)가 있으면 그 값이 이긴다.
|
|
17
|
+
* 이름에 DEFAULT 를 붙인 이유: 예전에는 이것이 유일한 값이어서 "현장 명세" 와 구별되지 않았고,
|
|
18
|
+
* 그 상수가 수율 판단(주목 신호)까지 만들어 냈다. 무엇을 기본값으로 굴렸는지는 specCoverage() 가 밝힌다. */
|
|
19
|
+
const DEFAULT_CYCLE_MS = 40_000;
|
|
20
|
+
const DEFAULT_SETUP_MS = 15_000; // 체인지오버(제품 전환) 셋업 — OEE 가용성 손실
|
|
17
21
|
// MES 도메인 커맨드(Tier 2, 무방언 — MES 어휘는 MES 커널 소유). handleCommand 로 처리.
|
|
18
22
|
const MES_CMD = { changeover: 'mes.changeover' };
|
|
19
23
|
const CP = '0614141';
|
|
20
24
|
const WIP_ITEMREF = '066666';
|
|
21
25
|
const WIP_GTIN = sgtinClass(CP, WIP_ITEMREF);
|
|
22
|
-
const
|
|
26
|
+
const DEFAULT_YIELD = 0.8; // 양품률 기본값 — 현장 값은 OperationDef.parameters(OP_PARAM.yield) 로 온다
|
|
23
27
|
// 부품(공용) — BOM 라인의 품목 클래스.
|
|
24
28
|
const PART_A = { itemRef: '055551', gtin: sgtinClass(CP, '055551') };
|
|
25
29
|
const PART_B = { itemRef: '055552', gtin: sgtinClass(CP, '055552') };
|
|
@@ -53,12 +57,15 @@ export class MesKernel extends FlowEngine {
|
|
|
53
57
|
constructor(tenantId, policy = firstFitPolicy, mesSpec) {
|
|
54
58
|
super(tenantId, policy);
|
|
55
59
|
this.mesSpec = mesSpec;
|
|
60
|
+
/* 정의가 있으면 그 안의 오퍼레이션 명세(소요·변동·모수)를 커널이 소비한다 — 없으면 기본값 경로. */
|
|
61
|
+
if (mesSpec?.definition?.operations)
|
|
62
|
+
this.loadOperations(mesSpec.definition.operations);
|
|
56
63
|
}
|
|
57
64
|
productOf(gtin) { return PRODUCTS.find(p => p.gtin === gtin); }
|
|
58
65
|
/**
|
|
59
66
|
* MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
|
|
60
67
|
* 자동 체인지오버(task.changeoverKey 상이 시 셋업)의 수동 버전 — 운영자가 사전 전환(툴링 교체) 지시.
|
|
61
|
-
* 이미 그 제품이면 no-op, 아니면 셋업(
|
|
68
|
+
* 이미 그 제품이면 no-op, 아니면 셋업(기본값, OEE 가용성 손실) + lastChangeoverKey 각인
|
|
62
69
|
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
63
70
|
*/
|
|
64
71
|
handleCommand(cmd) {
|
|
@@ -70,7 +77,9 @@ export class MesKernel extends FlowEngine {
|
|
|
70
77
|
if (!m)
|
|
71
78
|
return { commandId: cmd.commandId, accepted: false, errorCode: 'resource-not-found', errorParams: { resourceId: a.resourceId }, error: `resource-not-found: ${a.resourceId}` };
|
|
72
79
|
if (m.lastChangeoverKey !== a.gtin) {
|
|
73
|
-
|
|
80
|
+
/* 수동 전환은 특정 오퍼레이션이 아니라 설비 대상이라 작업별 명세를 고를 수 없다 → 기본값.
|
|
81
|
+
* 오퍼레이션별 셋업 명세는 작업 생성 경로(emitStation*)가 소비한다. */
|
|
82
|
+
m.setupMs += DEFAULT_SETUP_MS; // 셋업 = OEE 가용성 손실
|
|
74
83
|
m.lastChangeoverKey = a.gtin;
|
|
75
84
|
this.emitMover(m);
|
|
76
85
|
}
|
|
@@ -137,7 +146,7 @@ export class MesKernel extends FlowEngine {
|
|
|
137
146
|
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
138
147
|
emitStation(o, stage, itemEpc, changeoverKey) {
|
|
139
148
|
const node = this.nodeByType(stage.node);
|
|
140
|
-
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: 'created', itemEpc, fromNode: node.id, toNode: node.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: node.id, toNode: node.id, resourceKind: stage.resource },
|
|
149
|
+
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: 'created', itemEpc, fromNode: node.id, toNode: node.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: node.id, toNode: node.id, resourceKind: stage.resource }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: stage.resource, changeoverKey, setupMs: this.paramDuration(stage.kind, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: 'process' };
|
|
141
150
|
this.tasks.set(task.id, task);
|
|
142
151
|
this.emitTask(task);
|
|
143
152
|
}
|
|
@@ -165,7 +174,7 @@ export class MesKernel extends FlowEngine {
|
|
|
165
174
|
// 마지막 스테이션(조립) → 완성차 (transform 1→1, disposition 으로 수율 loss → OEE 품질)
|
|
166
175
|
const fgStore = this.nodeByType('fg-store');
|
|
167
176
|
const wip = order.allocated[0];
|
|
168
|
-
const good = this.rng() <
|
|
177
|
+
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
169
178
|
this.recordOutput(t.resource, good); // OEE 품질(마지막 자원별 양품/불량)
|
|
170
179
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
171
180
|
const outputEpc = sgtinUri(CP, product.ref, ++this.prodSeq);
|
|
@@ -245,7 +254,7 @@ export class MesKernel extends FlowEngine {
|
|
|
245
254
|
}
|
|
246
255
|
emitStationDef(o, op, itemEpc) {
|
|
247
256
|
const node = this.nodeByType(op.nodeType);
|
|
248
|
-
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: 'created', itemEpc, fromNode: node.id, toNode: node.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: op.key, fromNode: node.id, toNode: node.id, resourceKind: op.resourceType },
|
|
257
|
+
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: 'created', itemEpc, fromNode: node.id, toNode: node.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: op.key, fromNode: node.id, toNode: node.id, resourceKind: op.resourceType }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: op.resourceType, changeoverKey: o.gtin, setupMs: this.paramDuration(op.key, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: op.intent };
|
|
249
258
|
this.tasks.set(task.id, task);
|
|
250
259
|
this.emitTask(task);
|
|
251
260
|
}
|
|
@@ -272,7 +281,7 @@ export class MesKernel extends FlowEngine {
|
|
|
272
281
|
}
|
|
273
282
|
const fgStore = this.nodeByType('fg-store');
|
|
274
283
|
const wip = order.allocated[0];
|
|
275
|
-
const good = this.rng() <
|
|
284
|
+
const good = this.rng() < (this.paramNumber(t.kind, OP_PARAM.yield) ?? DEFAULT_YIELD);
|
|
276
285
|
this.recordOutput(t.resource, good);
|
|
277
286
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
278
287
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
|
@@ -1,35 +1,98 @@
|
|
|
1
|
-
import type { BoardDef, CanonicalEnvelope, NodeState, ItemState, MoverState, TaskState, OrderState } from './contract.ts';
|
|
2
|
-
/**
|
|
1
|
+
import type { AssetState, BoardDef, CanonicalEnvelope, NodeState, ItemState, MoverState, PersonState, TaskState, OrderState } from './contract.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 마스터 동기 — 선언적 로케이션 upsert/remove.
|
|
4
|
+
*
|
|
5
|
+
* 구조(로케이션·용량·구역 소속)는 이벤트가 말해 주는 것이 아니라 **원 시스템의 마스터**에서 온다.
|
|
6
|
+
* 최초 생성은 Face2 마스터 인제스트(host `ingestMaster`)가 하고, 그 뒤 현장이 바뀐 것(랙 증설·구역
|
|
7
|
+
* 재편·용량 변경)은 이 경로로 **기동 중에도** 반영된다 — 없으면 재기동해야 구조가 갱신된다.
|
|
8
|
+
*/
|
|
3
9
|
export interface MasterUpdate {
|
|
4
10
|
op: 'upsert' | 'remove';
|
|
5
11
|
node: {
|
|
6
12
|
id: string;
|
|
7
13
|
type?: string;
|
|
8
14
|
capacity?: number;
|
|
15
|
+
parentId?: string;
|
|
9
16
|
};
|
|
10
17
|
}
|
|
11
18
|
export interface ProjectedState {
|
|
12
19
|
revision: number;
|
|
20
|
+
/**
|
|
21
|
+
* 받은 정정 선언들 — 상태에 반영하지 않은 것을 **밝힌다**.
|
|
22
|
+
* 비어 있지 않으면 "원 시스템이 정정을 보냈고 우리는 아직 반영하지 못했다" 는 뜻이다(조용한 무시 금지).
|
|
23
|
+
*/
|
|
24
|
+
corrections?: {
|
|
25
|
+
declaredAt: string;
|
|
26
|
+
reason?: string;
|
|
27
|
+
correctiveEventIDs: string[];
|
|
28
|
+
eventID?: string;
|
|
29
|
+
}[];
|
|
13
30
|
nodes: NodeState[];
|
|
14
31
|
items: ItemState[];
|
|
32
|
+
/** 사람 — 등급·교대·투입. 인원을 선언하지 않은 트윈에서는 빈 배열. */
|
|
33
|
+
persons: PersonState[];
|
|
34
|
+
/** 물리 자산(반복사용) — 자산을 선언하지 않은 트윈에서는 빈 배열. */
|
|
35
|
+
assets: AssetState[];
|
|
15
36
|
tasks: TaskState[];
|
|
16
37
|
movers: MoverState[];
|
|
17
38
|
orders: OrderState[];
|
|
18
39
|
}
|
|
19
40
|
export declare class StateProjector {
|
|
41
|
+
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
20
42
|
private master;
|
|
21
43
|
private items;
|
|
22
44
|
private aggregation;
|
|
23
45
|
private tasks;
|
|
24
46
|
private movers;
|
|
47
|
+
private persons;
|
|
48
|
+
private assets;
|
|
25
49
|
private orders;
|
|
26
50
|
revision: number;
|
|
51
|
+
/** 받은 정정 선언 — 상태에 반영하지 않되 **버리지도 않는다**(소비처가 볼 수 있게). */
|
|
52
|
+
private corrections;
|
|
27
53
|
constructor(board: BoardDef);
|
|
28
54
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
29
55
|
applyMaster(u: MasterUpdate): void;
|
|
56
|
+
/**
|
|
57
|
+
* 관측된 로케이션을 구조로 승격 — **이벤트가 가르쳐 준 것을 구조에서 지우지 않는다.**
|
|
58
|
+
*
|
|
59
|
+
* 마스터에 없는 로케이션에서 물품이 관측되면, 예전에는 물품의 `location` 에만 남고 `nodes` 에는
|
|
60
|
+
* 나타나지 않았다. 그 결과 그 자리는 스키매틱에 없고, 점유가 집계되지 않고, 병목 주목이 뜰 수
|
|
61
|
+
* 없었다 — **사실은 들어왔는데 구조가 모르는 상태.** 이제 최소 형태로 승격한다:
|
|
62
|
+
* 종류는 모르므로 `unknown`, **용량은 비워 둔다**(발명하지 않는다), 출처는 `observed`.
|
|
63
|
+
*
|
|
64
|
+
* 출처를 표시하는 이유: 소비처가 "마스터가 말한 자리" 와 "관측으로 알게 된 자리" 를 구별해야 한다
|
|
65
|
+
* (보드에 좌표가 없고, 용량을 채워야 계획에 참여한다). 마스터 동기가 오면 `master` 로 승격된다.
|
|
66
|
+
*/
|
|
67
|
+
private touchLocation;
|
|
68
|
+
/**
|
|
69
|
+
* 늦게 도착한 옛 이벤트를 걸러낸다 — **도착 순서 ≠ 발생 순서**.
|
|
70
|
+
*
|
|
71
|
+
* 실 연동에서는 순서가 뒤집힌다(재시도·큐·배치). 시각을 비교하지 않으면 **늦게 온 옛 이벤트가 최신
|
|
72
|
+
* 상태를 덮어써** 위치가 과거로 튄다. 표준이 발생(`eventTime`)과 기록(`recordTime`)을 나눠 둔 이유가
|
|
73
|
+
* 이것이므로, 대상별로 마지막으로 반영한 시각을 기억해 그보다 오래된 것은 무시한다.
|
|
74
|
+
*
|
|
75
|
+
* 판정 시각은 **발생 시각**을 쓴다(현장에서 일어난 순서가 사실). `recordTime` 은 같은 발생 시각이
|
|
76
|
+
* 겹칠 때의 보조 기준이다. 시각이 없으면 판정하지 않는다(있는 것만 가지고 판단한다).
|
|
77
|
+
*/
|
|
78
|
+
private stale;
|
|
79
|
+
/** 대상별 마지막 반영 시각 — 순서 판정용(대상=EPC·작업·설비·오더 id). */
|
|
80
|
+
private lastAt;
|
|
30
81
|
/** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
|
|
31
82
|
apply(e: CanonicalEnvelope): void;
|
|
32
83
|
private applyEpcis;
|
|
84
|
+
/**
|
|
85
|
+
* 물품 한 건 병합 — **아는 것을 잃지 않는다.** 새로 온 값이 우선, 없으면 기존 값 유지.
|
|
86
|
+
* 클래스 식별자(LGTIN/idpat)에서 품번·로트를 파생한다 — 소비처가 문자열을 자르지 않게.
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* 개체·로트 마스터데이터에서 만료 시각을 뽑는다 — **우리가 아는 이름일 때만.**
|
|
90
|
+
*
|
|
91
|
+
* 표준이 속성 이름을 정의하지 않으므로 모르는 이름은 해석하지 않는다(추측하지 않는다). 원문은
|
|
92
|
+
* `ilmd` 로 그대로 남으니 도메인이 자기 어휘로 읽을 수 있다.
|
|
93
|
+
*/
|
|
94
|
+
private expiryOf;
|
|
95
|
+
private mergeItem;
|
|
33
96
|
/** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
|
|
34
97
|
private remove;
|
|
35
98
|
/** 현재 투영 State — 노드 점유는 아이템 위치 집계로 유도(pure projection). */
|