@operato/twin-kernel 0.0.1
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/README.md +80 -0
- package/dist/allocation-policy.d.ts +48 -0
- package/dist/allocation-policy.js +50 -0
- package/dist/contract.d.ts +205 -0
- package/dist/contract.js +20 -0
- package/dist/counterfactual.d.ts +40 -0
- package/dist/counterfactual.js +59 -0
- package/dist/divergence.d.ts +15 -0
- package/dist/divergence.js +28 -0
- package/dist/duration-estimator.d.ts +12 -0
- package/dist/duration-estimator.js +10 -0
- package/dist/epcis.d.ts +125 -0
- package/dist/epcis.js +172 -0
- package/dist/event-journal.d.ts +17 -0
- package/dist/event-journal.js +41 -0
- package/dist/face2-adapter.d.ts +42 -0
- package/dist/face2-adapter.js +69 -0
- package/dist/flow-engine.d.ts +224 -0
- package/dist/flow-engine.js +422 -0
- package/dist/forecast.d.ts +29 -0
- package/dist/forecast.js +34 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/kernel.d.ts +28 -0
- package/dist/kernel.js +187 -0
- package/dist/mes-kernel.d.ts +28 -0
- package/dist/mes-kernel.js +126 -0
- package/dist/mes-profile.d.ts +9 -0
- package/dist/mes-profile.js +17 -0
- package/dist/runtime.d.ts +42 -0
- package/dist/runtime.js +59 -0
- package/dist/state-projector.d.ts +37 -0
- package/dist/state-projector.js +124 -0
- package/dist/twin-observer.d.ts +28 -0
- package/dist/twin-observer.js +41 -0
- package/dist/wms-profile.d.ts +13 -0
- package/dist/wms-profile.js +20 -0
- package/dist/yms-kernel.d.ts +29 -0
- package/dist/yms-kernel.js +181 -0
- package/dist/yms-profile.d.ts +11 -0
- package/dist/yms-profile.js +22 -0
- package/dist-cjs/index.cjs +1477 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @operato-twin/kernel — 비즈니스레이어 디지털트윈 커널
|
|
2
|
+
|
|
3
|
+
물류창고·야드·스마트팩토리의 **비즈니스 실행**을 시뮬레이션·모니터링하는 **헤드리스·프레임워크 무관·zero-dep** TS 커널. UI/3D/DOM 없이 순수 Node 에서 도는 것이 목적(sim=능동 생산, live=수동 미러, 계약 동일).
|
|
4
|
+
|
|
5
|
+
- 실행: `node --test test/*.test.ts` (Node 25 네이티브 TS, **무의존**)
|
|
6
|
+
- 상태: **38 tests green**, 3 버티컬(WMS/YMS/MES)
|
|
7
|
+
|
|
8
|
+
## 계층 (아래로만 의존, 무방언)
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
contract.ts 3채널 계약(State/Command/Scenario) + 운영 델타 + TwinKernel ← 도메인/표준 무관
|
|
12
|
+
epcis.ts GS1 EPCIS 2.0 이벤트 machinery(타입·빌더·검증기·URI·CBV disp) ← 표준(도메인 무관)
|
|
13
|
+
flow-engine.ts FlowEngine base — mechanics(RNG·clock·tick·자원배정·태스크진행·emit·snapshot)
|
|
14
|
+
allocation-policy.ts AllocationPolicy 확장 시임(selectPlacement/selectStock)
|
|
15
|
+
state-projector.ts 이벤트 스트림 → State 투영(모니터링 미러) ← 도메인 무관
|
|
16
|
+
runtime.ts TwinRuntime — host-facing facade + 구독 프로토콜 ← 도메인/전송 무관
|
|
17
|
+
face2-adapter.ts 레거시 레코드 → 정규 EPCIS(선언적 매핑 + 검증) ← ACL
|
|
18
|
+
|
|
19
|
+
{wms,yms,mes}-profile.ts 도메인 어휘(bizStep/btt)만
|
|
20
|
+
{wms,yms,mes}-kernel.ts 도메인 flow 동사(4 hook) — FlowEngine 확장
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> **무방언 원칙**: 코어(contract/epcis/flow-engine/policy/projector/runtime)에 특정 도메인 용어를 넣지 않는다. EPCIS 는 표준이라 `epcis.ts`(≠wms). order.kind·task.resourceType 등은 도메인이 소유. 정책 인터페이스는 `selectPlacement`/`selectStock`(≠putaway/pallets).
|
|
24
|
+
|
|
25
|
+
## FlowEngine — 단일 base, 도메인은 4 hook 만
|
|
26
|
+
|
|
27
|
+
`abstract FlowEngine implements TwinKernel` 이 모든 mechanics 를 소유. 도메인 kernel 은 **flow 동사**만 구현:
|
|
28
|
+
|
|
29
|
+
| hook | 역할 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `onArrival(spec)` | 입고/도착 자극 → 아이템 생성 + EPCIS + 반입 태스크 |
|
|
32
|
+
| `onOrder(spec)` | 오더/작업지시 자극 → 오더 생성 |
|
|
33
|
+
| `allocate(order)` | created 오더 할당 → 재고 선택(정책) + 태스크. **시간창 게이트도 여기**(스케줄링) |
|
|
34
|
+
| `onTaskComplete(task)` | 완료의 의미 — 이동(WMS/YMS) 또는 변환(MES). occupancy·EPCIS·오더 이행 전부 도메인 |
|
|
35
|
+
|
|
36
|
+
base 는 task **생명주기**(자원 배정·진행·완료·해제·델타)만 소유 → 이동-중립. 새 버티컬 = 프로파일(어휘) + kernel(4 hook), **~100줄**.
|
|
37
|
+
|
|
38
|
+
## 3 버티컬 — base 가 4 flow 성격을 담음을 실증
|
|
39
|
+
|
|
40
|
+
| 버티컬 | flow 성격 | base 대응 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **WMS** | 이동 (putaway/pick/pack/ship) + 백오더 | 기본 |
|
|
43
|
+
| **YMS** | 이동 (spot/pull) + **시간창 스케줄링**(어포인트먼트↔도크도어 예약) | 스케줄링=도메인 게이팅으로 **base 흡수** |
|
|
44
|
+
| **MES** | **변환**(TransformationEvent) + 다단계 라우팅 + **이종 자원**(cutter/welder) | 자원 매칭=`FlowTask.resourceType` **base 확장**(load-bearing 검증) |
|
|
45
|
+
|
|
46
|
+
두 진화 축: 시간창 스케줄링=흡수, 자원-타입 매칭=최소 확장. 둘 다 backward-compatible(기존 도메인 무영향, 예제 바이트 동일로 검증).
|
|
47
|
+
|
|
48
|
+
## 3채널 계약 (Face 1) + 구독
|
|
49
|
+
|
|
50
|
+
- **State**: `getSnapshot()`(스냅샷) + 델타 이벤트 스트림. 델타 = EPCIS(재고/위치/조립/변환) + 운영 델타(`task/equipment/order.status`, EPCIS 로 재구성 불가한 절반).
|
|
51
|
+
- **Command** (행위/act): `dispatch(cmd)` 가 실제로 sim 을 변이 → State 델타 유발(폐루프). 코어 공통 `order.hold`/`resume`(할당 보류/재개) + 도메인 `order.release`(즉시 투입) 등은 `handleCommand` 시임으로 확장.
|
|
52
|
+
- **Scenario**: `scenario.load/start/pause/setSpeed`(시뮬 자극).
|
|
53
|
+
- **TwinRuntime**: `subscribe`(snapshot→delta, revision 연속) · `resync` · `tick`. sparse 스트리밍(전이·move-start 만, 매 tick 아님).
|
|
54
|
+
|
|
55
|
+
## State 이원 모델
|
|
56
|
+
- **EPCIS 저널**(이산): 재고·위치·조립·변환. `epcis.ts` 로 정규 방출, `validateEpcisEvent` 검증.
|
|
57
|
+
- **운영·키네마틱**(연속): 무버 motion(from/to/progress)·태스크·오더 진척. 운영 델타로 미러.
|
|
58
|
+
- **StateProjector** 가 둘을 접어 재구성 → sim 이벤트든 live 이벤트든 같은 State(데이터원 스왑).
|
|
59
|
+
|
|
60
|
+
## 트윈 본연 — 현재로부터 예측 + 정합 (mirror+sim 과 구별짓는 기능)
|
|
61
|
+
관측·예측·행위가 따로 있는 건 mirror+simulator. 트윈의 본질은 그 **커플링**:
|
|
62
|
+
- **`kernel.fork()`** — 현재 상태(in-flight 포함)를 정확히 복제한 격리 엔진. 원본(live/sim)은 계속 가고, fork 는 **현재로부터 앞으로 굴려** forecast(완료 시각·처리량)·대안 what-if 탐색.
|
|
63
|
+
- **`compareStates(predicted, actual)`** — fork 예측 vs 실제 관측을 같은 시점에 대조 → **드리프트(모델↔현실 이탈) 탐지·국소화**(item/node/order 필드별). 발산 = 이상/개입 신호.
|
|
64
|
+
- 루프: 관측(현재) → fork 예측(미래) → 관측(실제) → 정합(발산) → 행위(개입).
|
|
65
|
+
- **`EventJournal` + `replay(board, events)`** — 트윈의 **기억**: 이벤트열이 곧 상태(이벤트-소싱). `journal.until(revision)`/`untilSimTime(iso)` 을 projector 로 재생 → **임의 과거 시점 상태 재구성(시간여행)**. 감사·리플레이·발산 비교의 기반. things-factory 에선 append 를 DB 이벤트 테이블로 교체.
|
|
66
|
+
|
|
67
|
+
### 본질 심화
|
|
68
|
+
- **fork = 완전한 결정적 분기** — 상태 + 시나리오(gens) + **RNG state** 까지 복제 → 미래 생성까지 원본과 동일하게 이어가는 진짜 continuation(what-if 는 fork 후 scenario 교체).
|
|
69
|
+
- **`TwinObserver`** — 자동 정합 루프: 주기적으로 fork-예측 후 실제가 그 시점 도달 시 대조 → **드리프트를 알림 이벤트로**. 트윈이 자기 모델↔현실 이탈을 능동 감지(방해 없으면 발산 0).
|
|
70
|
+
- **`monteCarloForecast`** — 확률적 예측: seed 변주 N개 fork → 지표를 **분포**로(min/mean/p50/p90/max). "언제 끝나?"가 아니라 "P90 완료시각·소진 확률". 원본 무간섭.
|
|
71
|
+
- **`TwinHistory` + `counterfactualAt`** — 반사실(기억+분기): 주기 checkpoint(전체 커널 fork 저장) → 과거 시점 T로 되돌아가(사이 시점은 결정적 재구동) **대안 결정을 fork** → 앞으로 굴려 baseline 과 대조 → **"그때 X 했다면?"의 효과**. 사후분석·의사결정 평가.
|
|
72
|
+
|
|
73
|
+
## 확장 지점
|
|
74
|
+
- **버티컬 추가**: `{x}-profile.ts`(어휘) + `{x}-kernel.ts`(4 hook).
|
|
75
|
+
- **할당 정책**: `AllocationPolicy` 교체(firstFit/partialFit 내장, FEFO/nearest/zone 추가 가능).
|
|
76
|
+
- **Face2 어댑터**: 실 시스템 페이로드 → 선언적 매핑 → 정규 EPCIS.
|
|
77
|
+
- **host 결합**: `TwinRuntime` 를 GraphQL sub·서비스로 래핑(전송은 얇은 host 계층).
|
|
78
|
+
|
|
79
|
+
## 미구현(범위 밖)
|
|
80
|
+
host 결합(things-factory 전송/퍼시스턴스/커넥터) · 보드 바인딩(컴포넌트↔SGLN) · 도메인 깊이(WMS 멀티라인, YMS 상하차 AggregationEvent, MES BOM/수율). 설계 SoT: `operato-twin/design/`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** 배치(placement) 대상 후보 슬롯의 관측 뷰 (내부 상태 누출 없이 결정에 필요한 만큼만). */
|
|
2
|
+
export interface SlotView {
|
|
3
|
+
id: string;
|
|
4
|
+
capacity: number;
|
|
5
|
+
occupancy: number;
|
|
6
|
+
reserved: number;
|
|
7
|
+
}
|
|
8
|
+
/** 할당 후보 재고(가용 스톡)의 관측 뷰. */
|
|
9
|
+
export interface StockView {
|
|
10
|
+
epc: string;
|
|
11
|
+
location: string;
|
|
12
|
+
qty: number;
|
|
13
|
+
expiry?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface PlacementContext {
|
|
16
|
+
item: {
|
|
17
|
+
epc: string;
|
|
18
|
+
gtin: string;
|
|
19
|
+
qty: number;
|
|
20
|
+
};
|
|
21
|
+
slots: readonly SlotView[];
|
|
22
|
+
}
|
|
23
|
+
export interface StockRequest {
|
|
24
|
+
gtin: string;
|
|
25
|
+
qty: number;
|
|
26
|
+
available: readonly StockView[];
|
|
27
|
+
}
|
|
28
|
+
export interface AllocationPolicy {
|
|
29
|
+
/** 배치 목적지 슬롯 id. 수용 불가면 null(도크 대기 → 다음 tick 재시도). */
|
|
30
|
+
selectPlacement(ctx: PlacementContext): string | null;
|
|
31
|
+
/** 이번 라운드에 할당할 재고 epc 목록(부분 가능). 빈 배열 = 이번엔 할당 보류. */
|
|
32
|
+
selectStock(ctx: StockRequest): readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 기본 정책 — first-fit slot + all-or-nothing 오더 할당.
|
|
36
|
+
* (재고가 요청 전량을 못 채우면 이번 라운드 보류 → 전량 확보 시 단일 출하.)
|
|
37
|
+
*/
|
|
38
|
+
export declare const firstFitPolicy: AllocationPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* 부분할당 정책 — first-fit slot + 가용분만큼 즉시 할당(백오더).
|
|
41
|
+
* 재고 일부만 있어도 그만큼 먼저 출하하고, 잔량은 재고 도착 시 후속 라운드로 채운다.
|
|
42
|
+
*/
|
|
43
|
+
export declare const partialFitPolicy: AllocationPolicy;
|
|
44
|
+
/**
|
|
45
|
+
* FEFO 정책 — First-Expired-First-Out. 만료 임박 로트를 먼저 출고(신선/제약 도메인).
|
|
46
|
+
* all-or-nothing(전량 확보 전 대기)이되 선택 순서는 만료 오름차순(동률은 epc). 코어 수정 없이 정책 교체만으로 확장(원칙 2).
|
|
47
|
+
*/
|
|
48
|
+
export declare const fefoPolicy: AllocationPolicy;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 할당 정책 — 확장 시임 (헌장 원칙 2: 불변 코어 + 확장 시임, open/closed).
|
|
3
|
+
*
|
|
4
|
+
* 커널 코어는 "무엇을(putaway 할 팔레트, 출고할 오더)"만 알고, "어디에·어느 것을"의
|
|
5
|
+
* 결정은 정책에 위임한다. 고객은 FIFO/FEFO/nearest/zone·부분할당 등을 정책 교체로만
|
|
6
|
+
* 확장 — 코어 수정 없이. (설계 gap #5 "할당 정책 플러그")
|
|
7
|
+
*/
|
|
8
|
+
function freeBinsFirstFit(slots) {
|
|
9
|
+
const sorted = [...slots].sort((a, b) => a.id.localeCompare(b.id));
|
|
10
|
+
for (const b of sorted)
|
|
11
|
+
if (b.occupancy + b.reserved < b.capacity)
|
|
12
|
+
return b.id;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function sortByEpc(available) {
|
|
16
|
+
return [...available].sort((a, b) => a.epc.localeCompare(b.epc));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 기본 정책 — first-fit slot + all-or-nothing 오더 할당.
|
|
20
|
+
* (재고가 요청 전량을 못 채우면 이번 라운드 보류 → 전량 확보 시 단일 출하.)
|
|
21
|
+
*/
|
|
22
|
+
export const firstFitPolicy = {
|
|
23
|
+
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
24
|
+
selectStock: ({ qty, available }) => {
|
|
25
|
+
if (available.length < qty)
|
|
26
|
+
return []; // 전량 확보 전엔 대기
|
|
27
|
+
return sortByEpc(available).slice(0, qty).map(s => s.epc);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 부분할당 정책 — first-fit slot + 가용분만큼 즉시 할당(백오더).
|
|
32
|
+
* 재고 일부만 있어도 그만큼 먼저 출하하고, 잔량은 재고 도착 시 후속 라운드로 채운다.
|
|
33
|
+
*/
|
|
34
|
+
export const partialFitPolicy = {
|
|
35
|
+
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
36
|
+
selectStock: ({ qty, available }) => sortByEpc(available).slice(0, qty).map(s => s.epc)
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* FEFO 정책 — First-Expired-First-Out. 만료 임박 로트를 먼저 출고(신선/제약 도메인).
|
|
40
|
+
* all-or-nothing(전량 확보 전 대기)이되 선택 순서는 만료 오름차순(동률은 epc). 코어 수정 없이 정책 교체만으로 확장(원칙 2).
|
|
41
|
+
*/
|
|
42
|
+
export const fefoPolicy = {
|
|
43
|
+
selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
|
|
44
|
+
selectStock: ({ qty, available }) => {
|
|
45
|
+
if (available.length < qty)
|
|
46
|
+
return []; // 전량 확보 전엔 대기
|
|
47
|
+
const byExpiry = [...available].sort((a, b) => (a.expiry ?? Infinity) - (b.expiry ?? Infinity) || a.epc.localeCompare(b.epc));
|
|
48
|
+
return byExpiry.slice(0, qty).map(s => s.epc);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export type ISOTime = string;
|
|
2
|
+
export interface CanonicalEnvelope<T = unknown> {
|
|
3
|
+
eventId: string;
|
|
4
|
+
eventType: string;
|
|
5
|
+
eventTime: ISOTime;
|
|
6
|
+
tenantId: string;
|
|
7
|
+
correlationId?: string;
|
|
8
|
+
data: T;
|
|
9
|
+
}
|
|
10
|
+
export type TaskStatus = 'created' | 'assigned' | 'in-progress' | 'completed';
|
|
11
|
+
export interface NodeState {
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
capacity?: number;
|
|
15
|
+
occupancy: number;
|
|
16
|
+
status?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ItemState {
|
|
19
|
+
epc: string;
|
|
20
|
+
gtin?: string;
|
|
21
|
+
location: string;
|
|
22
|
+
disposition?: string;
|
|
23
|
+
parent?: string;
|
|
24
|
+
qty?: number;
|
|
25
|
+
expiry?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 운영·키네마틱 모션 — State 이원 모델의 "연속" 절반.
|
|
29
|
+
* 백엔드는 이동 시작 시 이 값(from/to/duration)만 방출하고, UI 는 progress 를
|
|
30
|
+
* 로컬 프레임레이트로 보간한다(매 tick 통신 아님). 좌표는 커널 토폴로지 수준이 아니라
|
|
31
|
+
* 보드 바인딩에서 노드→좌표로 해석. 상세: design/simulation/execution-model.md §4·§5
|
|
32
|
+
*/
|
|
33
|
+
export interface MoverMotion {
|
|
34
|
+
fromNode: string;
|
|
35
|
+
toNode: string;
|
|
36
|
+
startedAtSimMs: number;
|
|
37
|
+
durationMs: number;
|
|
38
|
+
progress: number;
|
|
39
|
+
elapsedMs: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 설비 종합효율(OEE = Availability × Performance × Quality) — 도메인-일반 설비 메트릭.
|
|
43
|
+
* 규약(단일 컨벤션): PlannedTime=설비 존재 sim 시간. Availability 손실=셋업(+고장), Performance 손실=기아(유휴),
|
|
44
|
+
* Quality 손실=불량. Availability=(planned−setup)/planned · Performance=run/(planned−setup) · Quality=good/(good+scrap).
|
|
45
|
+
* → OEE=(run/planned)×quality. 세 인자가 손실 위치(셋업·기아·불량)를 분해해 드러낸다.
|
|
46
|
+
*/
|
|
47
|
+
export interface OeeMetrics {
|
|
48
|
+
availability: number;
|
|
49
|
+
performance: number;
|
|
50
|
+
quality: number;
|
|
51
|
+
overall: number;
|
|
52
|
+
runMs: number;
|
|
53
|
+
setupMs: number;
|
|
54
|
+
downMs: number;
|
|
55
|
+
idleMs: number;
|
|
56
|
+
goodCount: number;
|
|
57
|
+
scrapCount: number;
|
|
58
|
+
}
|
|
59
|
+
export interface MoverState {
|
|
60
|
+
id: string;
|
|
61
|
+
kind: string;
|
|
62
|
+
location?: string;
|
|
63
|
+
status: string;
|
|
64
|
+
taskId?: string;
|
|
65
|
+
motion?: MoverMotion;
|
|
66
|
+
oee?: OeeMetrics;
|
|
67
|
+
}
|
|
68
|
+
export interface TaskState {
|
|
69
|
+
id: string;
|
|
70
|
+
kind: string;
|
|
71
|
+
status: TaskStatus;
|
|
72
|
+
itemRefs?: string[];
|
|
73
|
+
fromNode?: string;
|
|
74
|
+
toNode?: string;
|
|
75
|
+
resourceRef?: string;
|
|
76
|
+
progress?: number;
|
|
77
|
+
}
|
|
78
|
+
export interface OrderState {
|
|
79
|
+
id: string;
|
|
80
|
+
kind: string;
|
|
81
|
+
status: string;
|
|
82
|
+
progress?: number;
|
|
83
|
+
held?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface StateSnapshot {
|
|
86
|
+
revision: number;
|
|
87
|
+
simClockMs: number;
|
|
88
|
+
nodes: NodeState[];
|
|
89
|
+
items: ItemState[];
|
|
90
|
+
movers: MoverState[];
|
|
91
|
+
tasks: TaskState[];
|
|
92
|
+
orders: OrderState[];
|
|
93
|
+
}
|
|
94
|
+
export interface Command<T = unknown> {
|
|
95
|
+
commandId: string;
|
|
96
|
+
type: string;
|
|
97
|
+
tenantId: string;
|
|
98
|
+
correlationId?: string;
|
|
99
|
+
args: T;
|
|
100
|
+
}
|
|
101
|
+
export interface CommandAck {
|
|
102
|
+
commandId: string;
|
|
103
|
+
accepted: boolean;
|
|
104
|
+
error?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface RateSpec {
|
|
107
|
+
distribution: 'poisson' | 'uniform' | 'constant' | 'profile';
|
|
108
|
+
meanPerHour: number;
|
|
109
|
+
profile?: number[];
|
|
110
|
+
}
|
|
111
|
+
export interface ContentSpec {
|
|
112
|
+
skuMix: {
|
|
113
|
+
gtin: string;
|
|
114
|
+
weight: number;
|
|
115
|
+
}[];
|
|
116
|
+
qtyPerLine: {
|
|
117
|
+
min: number;
|
|
118
|
+
max: number;
|
|
119
|
+
};
|
|
120
|
+
linesPerOrder?: {
|
|
121
|
+
min: number;
|
|
122
|
+
max: number;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export interface GeneratorSpec {
|
|
126
|
+
kind: 'inbound-arrival' | 'outbound-order';
|
|
127
|
+
rate: RateSpec;
|
|
128
|
+
content: ContentSpec;
|
|
129
|
+
window?: {
|
|
130
|
+
startHour: number;
|
|
131
|
+
endHour: number;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export interface ScenarioDef {
|
|
135
|
+
seed?: number;
|
|
136
|
+
speed?: number;
|
|
137
|
+
horizon?: number;
|
|
138
|
+
generators: GeneratorSpec[];
|
|
139
|
+
}
|
|
140
|
+
export interface ScenarioControl {
|
|
141
|
+
load(def: ScenarioDef): void;
|
|
142
|
+
start(): void;
|
|
143
|
+
pause(): void;
|
|
144
|
+
reset(): void;
|
|
145
|
+
setSpeed(factor: number): void;
|
|
146
|
+
}
|
|
147
|
+
export declare const OP_EVENT: {
|
|
148
|
+
readonly task: "task.status";
|
|
149
|
+
readonly equipment: "equipment.status";
|
|
150
|
+
readonly order: "order.status";
|
|
151
|
+
};
|
|
152
|
+
export interface TaskStatusDelta {
|
|
153
|
+
taskId: string;
|
|
154
|
+
kind: string;
|
|
155
|
+
status: TaskStatus;
|
|
156
|
+
fromNode?: string;
|
|
157
|
+
toNode?: string;
|
|
158
|
+
itemRefs?: string[];
|
|
159
|
+
resourceRef?: string;
|
|
160
|
+
}
|
|
161
|
+
export interface EquipmentStatusDelta {
|
|
162
|
+
moverId: string;
|
|
163
|
+
kind: string;
|
|
164
|
+
status: string;
|
|
165
|
+
location?: string;
|
|
166
|
+
motion?: MoverMotion;
|
|
167
|
+
}
|
|
168
|
+
export interface OrderStatusDelta {
|
|
169
|
+
orderId: string;
|
|
170
|
+
kind: string;
|
|
171
|
+
status: string;
|
|
172
|
+
requested: number;
|
|
173
|
+
fulfilled: number;
|
|
174
|
+
held?: boolean;
|
|
175
|
+
}
|
|
176
|
+
export declare const CMD: {
|
|
177
|
+
readonly orderHold: "order.hold";
|
|
178
|
+
readonly orderResume: "order.resume";
|
|
179
|
+
readonly orderRelease: "order.release";
|
|
180
|
+
};
|
|
181
|
+
export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | OrderStatusDelta;
|
|
182
|
+
export type EventHandler = (e: CanonicalEnvelope) => void;
|
|
183
|
+
export type Unsubscribe = () => void;
|
|
184
|
+
export interface BoardDef {
|
|
185
|
+
nodes: {
|
|
186
|
+
id: string;
|
|
187
|
+
type: string;
|
|
188
|
+
capacity: number;
|
|
189
|
+
}[];
|
|
190
|
+
movers: {
|
|
191
|
+
id: string;
|
|
192
|
+
kind: string;
|
|
193
|
+
homeNode: string;
|
|
194
|
+
mtbfMs?: number;
|
|
195
|
+
mttrMs?: number;
|
|
196
|
+
}[];
|
|
197
|
+
}
|
|
198
|
+
export interface TwinKernel {
|
|
199
|
+
loadBoard(def: BoardDef): void;
|
|
200
|
+
getSnapshot(): StateSnapshot;
|
|
201
|
+
onEvent(handler: EventHandler): Unsubscribe;
|
|
202
|
+
dispatch(cmd: Command): CommandAck;
|
|
203
|
+
readonly scenario: ScenarioControl;
|
|
204
|
+
tick(dtMs: number): void;
|
|
205
|
+
}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Face 1 — 3채널 계약 (walking skeleton 범위).
|
|
3
|
+
* 설계 SoT: operato-twin/design/integration/face1-contract.md
|
|
4
|
+
*/
|
|
5
|
+
// ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
|
|
6
|
+
// EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·movers(equipment)·orders 의 운영 상태는
|
|
7
|
+
// 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
|
|
8
|
+
// (execution-model.md §5, roadmap 발견 gap: 운영 델타 이벤트화)
|
|
9
|
+
export const OP_EVENT = {
|
|
10
|
+
task: 'task.status',
|
|
11
|
+
equipment: 'equipment.status',
|
|
12
|
+
order: 'order.status'
|
|
13
|
+
};
|
|
14
|
+
// ── Command 채널 어휘 — 트윈의 "행위(act)" 면 (prescriptive/트랜잭션 프론트엔드) ──
|
|
15
|
+
// 코어 공통: order.hold/resume(할당 보류). 도메인: order.release(즉시 투입) 등은 handleCommand 로.
|
|
16
|
+
export const CMD = {
|
|
17
|
+
orderHold: 'order.hold',
|
|
18
|
+
orderResume: 'order.resume',
|
|
19
|
+
orderRelease: 'order.release'
|
|
20
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command, CommandAck, ScenarioControl, StateSnapshot } from './contract.ts';
|
|
2
|
+
import { type StateDivergence } from './divergence.ts';
|
|
3
|
+
/** fork·구동·행위 가능한 트윈(FlowEngine 이 만족). */
|
|
4
|
+
export interface CounterfactualTwin {
|
|
5
|
+
getSnapshot(): StateSnapshot;
|
|
6
|
+
tick(dtMs: number): void;
|
|
7
|
+
fork(): CounterfactualTwin;
|
|
8
|
+
dispatch(cmd: Command): CommandAck;
|
|
9
|
+
scenario: ScenarioControl;
|
|
10
|
+
}
|
|
11
|
+
/** 체크포인트 이력 — 주기적으로 live 를 fork 해 저장(전체 상태 보존). 시간여행의 앵커. */
|
|
12
|
+
export declare class TwinHistory {
|
|
13
|
+
private live;
|
|
14
|
+
private tickMs;
|
|
15
|
+
private checkpoints;
|
|
16
|
+
constructor(live: CounterfactualTwin, opts?: {
|
|
17
|
+
tickMs?: number;
|
|
18
|
+
});
|
|
19
|
+
/** 현재를 체크포인트로 저장(호스트가 주기적으로 호출). */
|
|
20
|
+
checkpoint(): void;
|
|
21
|
+
get count(): number;
|
|
22
|
+
/** 시각 T 의 전체 커널 — T 이하 최신 체크포인트에서 fork 후 정확히 T 로 재구동(결정적). */
|
|
23
|
+
at(simMs: number): CounterfactualTwin | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface CounterfactualResult {
|
|
26
|
+
atSimMs: number;
|
|
27
|
+
withAlt: StateSnapshot;
|
|
28
|
+
baseline: StateSnapshot;
|
|
29
|
+
effect: StateDivergence;
|
|
30
|
+
}
|
|
31
|
+
export interface CounterfactualOptions {
|
|
32
|
+
alternative: (twin: CounterfactualTwin) => void;
|
|
33
|
+
horizonMs: number;
|
|
34
|
+
tickMs?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 반사실: history 의 시각 T 상태에서 두 분기를 굴려 대안의 효과를 잰다.
|
|
38
|
+
* withAlt = T 상태 + 대안 → T+H / baseline = T 상태 그대로 → T+H / effect = 둘의 발산.
|
|
39
|
+
*/
|
|
40
|
+
export declare function counterfactualAt(history: TwinHistory, atSimMs: number, opts: CounterfactualOptions): CounterfactualResult | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Counterfactual + Checkpointing — "과거 T에 다른 결정을 했다면?" (기억 + 분기 결합).
|
|
3
|
+
*
|
|
4
|
+
* replay(기억)는 과거를 재구성만 한다. 여기에 fork(분기)를 결합하면 반사실 분석:
|
|
5
|
+
* 과거 시점으로 되돌아가 대안 결정을 넣고 앞으로 굴려, 실제(대안 없음)와 대조 → 대안의 효과.
|
|
6
|
+
* 전체 커널 상태가 필요하므로(replay 의 projected 상태로는 부족) 주기적 checkpoint(fork 저장)를 쓴다.
|
|
7
|
+
* 체크포인트 fork 를 정확히 T 로 재구동(RNG 연속 → 실제와 일치)한 뒤 분기 = 결정적 반사실.
|
|
8
|
+
*/
|
|
9
|
+
import { compareStates } from "./divergence.js";
|
|
10
|
+
/** 체크포인트 이력 — 주기적으로 live 를 fork 해 저장(전체 상태 보존). 시간여행의 앵커. */
|
|
11
|
+
export class TwinHistory {
|
|
12
|
+
live;
|
|
13
|
+
tickMs;
|
|
14
|
+
checkpoints = [];
|
|
15
|
+
constructor(live, opts = {}) {
|
|
16
|
+
this.live = live;
|
|
17
|
+
this.tickMs = opts.tickMs ?? 1000;
|
|
18
|
+
}
|
|
19
|
+
/** 현재를 체크포인트로 저장(호스트가 주기적으로 호출). */
|
|
20
|
+
checkpoint() {
|
|
21
|
+
this.checkpoints.push({ simMs: this.live.getSnapshot().simClockMs, twin: this.live.fork() });
|
|
22
|
+
}
|
|
23
|
+
get count() {
|
|
24
|
+
return this.checkpoints.length;
|
|
25
|
+
}
|
|
26
|
+
/** 시각 T 의 전체 커널 — T 이하 최신 체크포인트에서 fork 후 정확히 T 로 재구동(결정적). */
|
|
27
|
+
at(simMs) {
|
|
28
|
+
let best;
|
|
29
|
+
for (const c of this.checkpoints)
|
|
30
|
+
if (c.simMs <= simMs)
|
|
31
|
+
best = c;
|
|
32
|
+
if (!best)
|
|
33
|
+
return undefined;
|
|
34
|
+
const t = best.twin.fork();
|
|
35
|
+
let guard = 0;
|
|
36
|
+
while (t.getSnapshot().simClockMs < simMs && guard++ < 1_000_000)
|
|
37
|
+
t.tick(this.tickMs);
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 반사실: history 의 시각 T 상태에서 두 분기를 굴려 대안의 효과를 잰다.
|
|
43
|
+
* withAlt = T 상태 + 대안 → T+H / baseline = T 상태 그대로 → T+H / effect = 둘의 발산.
|
|
44
|
+
*/
|
|
45
|
+
export function counterfactualAt(history, atSimMs, opts) {
|
|
46
|
+
const base = history.at(atSimMs);
|
|
47
|
+
if (!base)
|
|
48
|
+
return undefined;
|
|
49
|
+
const step = opts.tickMs ?? 1000;
|
|
50
|
+
const target = atSimMs + opts.horizonMs;
|
|
51
|
+
const withAlt = base.fork();
|
|
52
|
+
opts.alternative(withAlt);
|
|
53
|
+
const baseline = base.fork();
|
|
54
|
+
const run = (t) => { let g = 0; while (t.getSnapshot().simClockMs < target && g++ < 1_000_000)
|
|
55
|
+
t.tick(step); };
|
|
56
|
+
run(withAlt);
|
|
57
|
+
run(baseline);
|
|
58
|
+
return { atSimMs, withAlt: withAlt.getSnapshot(), baseline: baseline.getSnapshot(), effect: compareStates(withAlt.getSnapshot(), baseline.getSnapshot()) };
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StateSnapshot } from './contract.ts';
|
|
2
|
+
export interface FieldDiff<T> {
|
|
3
|
+
id: string;
|
|
4
|
+
predicted: T | undefined;
|
|
5
|
+
actual: T | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface StateDivergence {
|
|
8
|
+
hasDrift: boolean;
|
|
9
|
+
itemLocation: FieldDiff<string>[];
|
|
10
|
+
itemDisposition: FieldDiff<string>[];
|
|
11
|
+
nodeOccupancy: FieldDiff<number>[];
|
|
12
|
+
orderStatus: FieldDiff<string>[];
|
|
13
|
+
}
|
|
14
|
+
/** predicted(fork forecast)와 actual(관측) 스냅샷을 대조. 같은 sim 시점에 호출하는 것이 의미 있음. */
|
|
15
|
+
export declare function compareStates(predicted: StateSnapshot, actual: StateSnapshot): StateDivergence;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Divergence — 예측(predicted) vs 실제(actual) 상태 대조 (디지털트윈 본연: 정합/발산).
|
|
3
|
+
*
|
|
4
|
+
* 트윈을 mirror+simulator 와 구별짓는 기능: 현재에서 fork 한 forecast 와, 이후 실제로 관측된
|
|
5
|
+
* 상태를 같은 시점에 비교해 **드리프트(모델과 현실의 이탈)** 를 탐지한다. 발산 = 이상/개입 신호.
|
|
6
|
+
* (fork 로 예측 → 관측으로 실제 → 여기서 대조. execution-model.md 시뮬↔모니터링 커플링.)
|
|
7
|
+
*/
|
|
8
|
+
function diffBy(predicted, actual, idOf, valOf) {
|
|
9
|
+
const p = new Map(predicted.map(e => [idOf(e), valOf(e)]));
|
|
10
|
+
const a = new Map(actual.map(e => [idOf(e), valOf(e)]));
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const id of new Set([...p.keys(), ...a.keys()])) {
|
|
13
|
+
const pv = p.get(id);
|
|
14
|
+
const av = a.get(id);
|
|
15
|
+
if (pv !== av)
|
|
16
|
+
out.push({ id, predicted: pv, actual: av });
|
|
17
|
+
}
|
|
18
|
+
return out.sort((x, y) => x.id.localeCompare(y.id));
|
|
19
|
+
}
|
|
20
|
+
/** predicted(fork forecast)와 actual(관측) 스냅샷을 대조. 같은 sim 시점에 호출하는 것이 의미 있음. */
|
|
21
|
+
export function compareStates(predicted, actual) {
|
|
22
|
+
const itemLocation = diffBy(predicted.items, actual.items, i => i.epc, i => i.location);
|
|
23
|
+
const itemDisposition = diffBy(predicted.items, actual.items, i => i.epc, i => i.disposition);
|
|
24
|
+
const nodeOccupancy = diffBy(predicted.nodes, actual.nodes, n => n.id, n => n.occupancy);
|
|
25
|
+
const orderStatus = diffBy(predicted.orders, actual.orders, o => o.id, o => o.status);
|
|
26
|
+
const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 || nodeOccupancy.length > 0 || orderStatus.length > 0;
|
|
27
|
+
return { hasDrift, itemLocation, itemDisposition, nodeOccupancy, orderStatus };
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface DurationContext {
|
|
2
|
+
kind: string;
|
|
3
|
+
fromNode: string;
|
|
4
|
+
toNode: string;
|
|
5
|
+
resourceKind?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DurationEstimator {
|
|
8
|
+
/** 소요(ms). undefined 반환 시 도메인 기본값(fallback) 사용 — 일부 kind 만 다루고 나머지는 defer 가능. */
|
|
9
|
+
estimate(ctx: DurationContext): number | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** 균일 상수 estimator (테스트·단순 보드용). 실 duration 은 보통 씬-유도 estimator 가 제공. */
|
|
12
|
+
export declare const constantDuration: (ms: number) => DurationEstimator;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Duration 시임 — task 소요시간(durationMs) 산출 확장점 (AllocationPolicy 와 동형, open/closed).
|
|
3
|
+
*
|
|
4
|
+
* 커널은 "이동/처리에 얼마 걸리나"(durationMs)만 소비하고, "왜·어떤 경로로"(거리·경로탐색·교통)는
|
|
5
|
+
* 안 묻는다. 그 물리 연산은 씬(things-scene FlowGraph·nav-graph·traffic) 소관 — 커널은 좌표-free.
|
|
6
|
+
* 보드/씬 바인딩이 거리·속도 기반 estimator 를 주입하면 가변 이동시간이 되고, 미주입 시 도메인 상수.
|
|
7
|
+
* (설계 SoT: design/integration/things-factory-host.md §6 — Kernel↔Host 경계 리뷰 결론)
|
|
8
|
+
*/
|
|
9
|
+
/** 균일 상수 estimator (테스트·단순 보드용). 실 duration 은 보통 씬-유도 estimator 가 제공. */
|
|
10
|
+
export const constantDuration = (ms) => ({ estimate: () => ms });
|