@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.
Files changed (43) hide show
  1. package/README.md +80 -0
  2. package/dist/allocation-policy.d.ts +48 -0
  3. package/dist/allocation-policy.js +50 -0
  4. package/dist/contract.d.ts +205 -0
  5. package/dist/contract.js +20 -0
  6. package/dist/counterfactual.d.ts +40 -0
  7. package/dist/counterfactual.js +59 -0
  8. package/dist/divergence.d.ts +15 -0
  9. package/dist/divergence.js +28 -0
  10. package/dist/duration-estimator.d.ts +12 -0
  11. package/dist/duration-estimator.js +10 -0
  12. package/dist/epcis.d.ts +125 -0
  13. package/dist/epcis.js +172 -0
  14. package/dist/event-journal.d.ts +17 -0
  15. package/dist/event-journal.js +41 -0
  16. package/dist/face2-adapter.d.ts +42 -0
  17. package/dist/face2-adapter.js +69 -0
  18. package/dist/flow-engine.d.ts +224 -0
  19. package/dist/flow-engine.js +422 -0
  20. package/dist/forecast.d.ts +29 -0
  21. package/dist/forecast.js +34 -0
  22. package/dist/index.d.ts +19 -0
  23. package/dist/index.js +19 -0
  24. package/dist/kernel.d.ts +28 -0
  25. package/dist/kernel.js +187 -0
  26. package/dist/mes-kernel.d.ts +28 -0
  27. package/dist/mes-kernel.js +126 -0
  28. package/dist/mes-profile.d.ts +9 -0
  29. package/dist/mes-profile.js +17 -0
  30. package/dist/runtime.d.ts +42 -0
  31. package/dist/runtime.js +59 -0
  32. package/dist/state-projector.d.ts +37 -0
  33. package/dist/state-projector.js +124 -0
  34. package/dist/twin-observer.d.ts +28 -0
  35. package/dist/twin-observer.js +41 -0
  36. package/dist/wms-profile.d.ts +13 -0
  37. package/dist/wms-profile.js +20 -0
  38. package/dist/yms-kernel.d.ts +29 -0
  39. package/dist/yms-kernel.js +181 -0
  40. package/dist/yms-profile.d.ts +11 -0
  41. package/dist/yms-profile.js +22 -0
  42. package/dist-cjs/index.cjs +1477 -0
  43. package/package.json +30 -0
@@ -0,0 +1,34 @@
1
+ /*
2
+ * Probabilistic Forecast — 몬테카를로 예측 (단일 결정값 → 분포).
3
+ *
4
+ * 예측은 본질적으로 불확실하다. 현재 상태를 fork 하고 seed 를 달리해(확률적 미래) N회 굴려,
5
+ * 관심 지표를 분포로 준다: "언제 끝나?"가 아니라 "P50/P90 완료시각·재고소진 확률".
6
+ * fork(현재예측) + 시나리오 재시드로 미래만 변주 — 현재 상태(재고·오더·태스크)는 보존.
7
+ */
8
+ /**
9
+ * 현재 상태에서 N개 확률적 미래를 표본화 → 지표 분포.
10
+ * run 마다 fork(현재 보존) + scenario.load(seed+i)(미래 변주) + horizon 까지 구동. 원본 무간섭.
11
+ */
12
+ export function monteCarloForecast(twin, opts) {
13
+ const now = twin.getSnapshot().simClockMs;
14
+ const step = opts.tickMs ?? 1000;
15
+ const baseSeed = opts.scenario.seed ?? 1;
16
+ const samples = [];
17
+ for (let i = 0; i < opts.runs; i++) {
18
+ const fc = twin.fork();
19
+ fc.scenario.load({ ...opts.scenario, seed: baseSeed + i }); // 미래만 변주(현재 상태는 fork 로 보존)
20
+ fc.scenario.start();
21
+ const target = now + opts.horizonMs;
22
+ let guard = 0;
23
+ while (fc.getSnapshot().simClockMs < target && guard++ < 1_000_000)
24
+ fc.tick(step);
25
+ samples.push(opts.metric(fc.getSnapshot()));
26
+ }
27
+ return summarize(opts.runs, samples);
28
+ }
29
+ function summarize(runs, samples) {
30
+ const sorted = [...samples].sort((a, b) => a - b);
31
+ const pct = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
32
+ const mean = samples.reduce((a, b) => a + b, 0) / (samples.length || 1);
33
+ return { runs, samples, min: sorted[0], max: sorted[sorted.length - 1], mean, p50: pct(0.5), p90: pct(0.9) };
34
+ }
@@ -0,0 +1,19 @@
1
+ export * from './contract.ts';
2
+ export * from './counterfactual.ts';
3
+ export * from './forecast.ts';
4
+ export * from './twin-observer.ts';
5
+ export * from './event-journal.ts';
6
+ export * from './divergence.ts';
7
+ export * from './epcis.ts';
8
+ export * from './wms-profile.ts';
9
+ export * from './allocation-policy.ts';
10
+ export * from './duration-estimator.ts';
11
+ export * from './state-projector.ts';
12
+ export * from './face2-adapter.ts';
13
+ export * from './runtime.ts';
14
+ export * from './yms-profile.ts';
15
+ export * from './mes-profile.ts';
16
+ export * from './flow-engine.ts';
17
+ export { WmsKernel } from './kernel.ts';
18
+ export { YmsKernel } from './yms-kernel.ts';
19
+ export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS } from './mes-kernel.ts';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export * from "./contract.js";
2
+ export * from "./counterfactual.js";
3
+ export * from "./forecast.js";
4
+ export * from "./twin-observer.js";
5
+ export * from "./event-journal.js";
6
+ export * from "./divergence.js";
7
+ export * from "./epcis.js";
8
+ export * from "./wms-profile.js";
9
+ export * from "./allocation-policy.js";
10
+ export * from "./duration-estimator.js";
11
+ export * from "./state-projector.js";
12
+ export * from "./face2-adapter.js";
13
+ export * from "./runtime.js";
14
+ export * from "./yms-profile.js";
15
+ export * from "./mes-profile.js";
16
+ export * from "./flow-engine.js";
17
+ export { WmsKernel } from "./kernel.js";
18
+ export { YmsKernel } from "./yms-kernel.js";
19
+ export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS } from "./mes-kernel.js";
@@ -0,0 +1,28 @@
1
+ import type { Command, CommandAck, GeneratorSpec } from './contract.ts';
2
+ import type { AllocationPolicy } from './allocation-policy.ts';
3
+ import { FlowEngine } from './flow-engine.ts';
4
+ import type { FlowOrder, FlowTask } from './flow-engine.ts';
5
+ export declare class WmsKernel extends FlowEngine {
6
+ private poSeq;
7
+ constructor(tenantId: string, policy?: AllocationPolicy);
8
+ /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
9
+ protected onArrival(spec: GeneratorSpec): void;
10
+ /**
11
+ * SO 오더 자극(생성기) → 오더 생성. `linesPerOrder` 지정 시 멀티SKU(라인별 distinct gtin),
12
+ * 미지정 시 단일 SKU(기존 경로 — rng 소비 순서 보존 = pickGtin → randInt).
13
+ */
14
+ protected onOrder(spec: GeneratorSpec): void;
15
+ /** Command 채널 — 오더 즉시 투입(트랜잭션 프론트엔드/prescriptive). */
16
+ protected handleCommand(cmd: Command): CommandAck;
17
+ private createSalesOrder;
18
+ /**
19
+ * created 오더를 정책으로 할당 → SO TransactionEvent(라인 통합) + pick task(라인별).
20
+ * 라인마다 selectStock(정책) → firstFit=라인 전량확보 대기 / partialFit=가용분 백오더.
21
+ * 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
22
+ */
23
+ protected allocate(o: FlowOrder): void;
24
+ /** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
25
+ protected onTaskComplete(t: FlowTask): void;
26
+ /** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
27
+ private finalizeOrder;
28
+ }
package/dist/kernel.js ADDED
@@ -0,0 +1,187 @@
1
+ /*
2
+ * WMS Kernel — 입출고 flow 동사(도메인). mechanics 는 FlowEngine base 재사용.
3
+ * 결정적 · 프레임워크 무관 · 퍼시스턴스 무관.
4
+ *
5
+ * 입고: 도착 → receiving(Txn PO + Agg 팔레트←케이스 + Object) → putaway → storing.
6
+ * 출고: SO 오더 → 할당(Txn SO) → pick(bin→staging) → picking → packing(Agg 화물←팔레트) → staging → shipping(DELETE).
7
+ */
8
+ import { CMD } from "./contract.js";
9
+ import { firstFitPolicy } from "./allocation-policy.js";
10
+ import { FlowEngine } from "./flow-engine.js";
11
+ import { BIZSTEP, BTT } from "./wms-profile.js";
12
+ import { DISP, aggregationEvent, gdtiUri, objectEvent, ssccUri, transactionEvent } from "./epcis.js";
13
+ const TRAVEL_MS = 30_000;
14
+ const COMPANY_PREFIX = '0614141';
15
+ const SHELF_MS = 30 * 24 * 3_600_000; // 기본 유통기한(30일)
16
+ const SHELF_JITTER_MS = 5 * 24 * 3_600_000; // 로트별 만료 편차(FEFO 가 FIFO 와 갈리게)
17
+ export class WmsKernel extends FlowEngine {
18
+ poSeq = 0;
19
+ constructor(tenantId, policy = firstFitPolicy) {
20
+ super(tenantId, policy);
21
+ }
22
+ /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
23
+ onArrival(spec) {
24
+ const dock = this.nodeByType('dock');
25
+ if (!dock)
26
+ return;
27
+ const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq); // 팔레트 SSCC
28
+ const gtin = this.pickGtin(spec.content.skuMix); // SGTIN idpat = epcClass
29
+ const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max); // 케이스 수(비직렬)
30
+ const po = gdtiUri(COMPANY_PREFIX, '401', ++this.poSeq);
31
+ const eventTime = this.now();
32
+ const qtyList = [{ epcClass: gtin, quantity: qty }];
33
+ const poTxn = [{ type: BTT.po, bizTransaction: po }];
34
+ // 로트 만료(FEFO 용, 결정적 — rng 무소비로 byte-identical 유지). 편차로 도착순≠만료순 → FEFO 가 유의미.
35
+ const expiry = this.clockMs + SHELF_MS - (this.epcSeq % 5) * SHELF_JITTER_MS;
36
+ this.items.set(epc, { epc, gtin, qty, location: dock.id, disposition: DISP.in_progress, expiry });
37
+ dock.occupancy++;
38
+ this.emit(transactionEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
39
+ this.emit(aggregationEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
40
+ this.emit(objectEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, disposition: DISP.in_progress, epcList: [epc], quantityList: qtyList, readPoint: dock.id, bizLocation: dock.id, bizTransactionList: poTxn }));
41
+ const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews('storage') });
42
+ if (!binId)
43
+ return; // 수용 불가 → 도크 대기
44
+ const id = `task-${++this.taskSeq}`;
45
+ const task = { id, kind: 'putaway', status: 'created', itemEpc: epc, fromNode: dock.id, toNode: binId, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'putaway', fromNode: dock.id, toNode: binId }, TRAVEL_MS) };
46
+ this.tasks.set(id, task);
47
+ this.emitTask(task);
48
+ }
49
+ /**
50
+ * SO 오더 자극(생성기) → 오더 생성. `linesPerOrder` 지정 시 멀티SKU(라인별 distinct gtin),
51
+ * 미지정 시 단일 SKU(기존 경로 — rng 소비 순서 보존 = pickGtin → randInt).
52
+ */
53
+ onOrder(spec) {
54
+ const lpl = spec.content.linesPerOrder;
55
+ if (!lpl) {
56
+ this.createSalesOrder([{ gtin: this.pickGtin(spec.content.skuMix), qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) }]);
57
+ return;
58
+ }
59
+ const n = Math.min(this.randInt(lpl.min, lpl.max), spec.content.skuMix.length);
60
+ const lines = [];
61
+ let pool = spec.content.skuMix;
62
+ for (let i = 0; i < n && pool.length > 0; i++) {
63
+ const gtin = this.pickGtin(pool);
64
+ lines.push({ gtin, qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) });
65
+ pool = pool.filter(m => m.gtin !== gtin); // 라인 SKU 중복 방지(distinct)
66
+ }
67
+ this.createSalesOrder(lines);
68
+ }
69
+ /** Command 채널 — 오더 즉시 투입(트랜잭션 프론트엔드/prescriptive). */
70
+ handleCommand(cmd) {
71
+ if (cmd.type === CMD.orderRelease) {
72
+ const a = cmd.args;
73
+ const lines = a?.lines ?? (a?.gtin ? [{ gtin: a.gtin, qty: a.qty ?? 1 }] : []);
74
+ if (lines.length === 0)
75
+ return { commandId: cmd.commandId, accepted: false, error: 'order.release: gtin 또는 lines 필요' };
76
+ this.createSalesOrder(lines);
77
+ return { commandId: cmd.commandId, accepted: true };
78
+ }
79
+ return super.handleCommand(cmd);
80
+ }
81
+ createSalesOrder(lines) {
82
+ const id = `order-${++this.orderSeq}`;
83
+ const so = gdtiUri(COMPANY_PREFIX, '402', ++this.soSeq);
84
+ const requested = lines.reduce((s, l) => s + l.qty, 0);
85
+ const order = {
86
+ id, kind: 'outbound', status: 'created', requested, fulfilled: 0, bizTransaction: so,
87
+ allocated: [], picked: [], shipmentEpc: null, lines: lines.map(l => ({ gtin: l.gtin, requested: l.qty }))
88
+ };
89
+ this.orders.set(id, order);
90
+ this.emitOrder(order);
91
+ }
92
+ /**
93
+ * created 오더를 정책으로 할당 → SO TransactionEvent(라인 통합) + pick task(라인별).
94
+ * 라인마다 selectStock(정책) → firstFit=라인 전량확보 대기 / partialFit=가용분 백오더.
95
+ * 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
96
+ */
97
+ allocate(o) {
98
+ const staging = this.nodeByType('staging');
99
+ if (!staging || !o.lines)
100
+ return;
101
+ const chosenAll = [];
102
+ for (const line of o.lines) {
103
+ const already = o.allocated.filter(e => this.items.get(e)?.gtin === line.gtin).length;
104
+ const need = line.requested - already;
105
+ if (need <= 0)
106
+ continue;
107
+ const available = [...this.items.values()]
108
+ .filter(i => i.gtin === line.gtin && i.disposition === DISP.sellable && this.nodes.get(i.location)?.type === 'storage')
109
+ .map(i => ({ epc: i.epc, location: i.location, qty: i.qty ?? 1, expiry: i.expiry }));
110
+ const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
111
+ for (const epc of chosen) {
112
+ this.items.get(epc).disposition = DISP.reserved;
113
+ o.allocated.push(epc);
114
+ chosenAll.push(epc);
115
+ }
116
+ }
117
+ if (chosenAll.length === 0)
118
+ return;
119
+ this.emit(transactionEvent({ eventTime: this.now(), action: 'ADD', bizStep: BIZSTEP.picking, bizTransactionList: [{ type: BTT.so, bizTransaction: o.bizTransaction }], epcList: chosenAll.slice() }));
120
+ for (const epc of chosenAll) {
121
+ const it = this.items.get(epc);
122
+ const id = `task-${++this.taskSeq}`;
123
+ const task = { id, kind: 'pick', status: 'created', itemEpc: epc, fromNode: it.location, toNode: staging.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'pick', fromNode: it.location, toNode: staging.id }, TRAVEL_MS), orderId: o.id };
124
+ this.tasks.set(id, task);
125
+ this.emitTask(task);
126
+ }
127
+ o.status = 'picking';
128
+ this.emitOrder(o);
129
+ }
130
+ /** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
131
+ onTaskComplete(t) {
132
+ const item = this.items.get(t.itemEpc);
133
+ const from = this.nodes.get(t.fromNode);
134
+ const to = this.nodes.get(t.toNode);
135
+ from.occupancy--;
136
+ to.occupancy++;
137
+ item.location = to.id;
138
+ if (t.kind === 'putaway') {
139
+ item.disposition = DISP.sellable;
140
+ this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: BIZSTEP.storing, disposition: DISP.sellable, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
141
+ return;
142
+ }
143
+ // pick
144
+ item.disposition = DISP.reserved;
145
+ this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: BIZSTEP.picking, disposition: DISP.reserved, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
146
+ const order = t.orderId ? this.orders.get(t.orderId) : undefined;
147
+ if (!order)
148
+ return;
149
+ order.picked.push(item.epc);
150
+ if (order.picked.length === order.allocated.length)
151
+ this.finalizeOrder(order, to);
152
+ }
153
+ /** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
154
+ finalizeOrder(order, staging) {
155
+ const shipDock = this.nodeByType('dock-ship') ?? staging;
156
+ const eventTime = this.now();
157
+ const shipment = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
158
+ order.shipmentEpc = shipment;
159
+ const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
160
+ // 패킹: 화물(shipment) ← 팔레트 조립(merge). 팔레트는 출하 DELETE 까지 독립 유지 → consume 없음.
161
+ this.aggregate(shipment, order.picked.slice(), { bizStep: BIZSTEP.packing, readPoint: staging.id, bizLocation: staging.id });
162
+ order.status = 'packed';
163
+ this.emit(objectEvent({ eventTime, action: 'OBSERVE', bizStep: BIZSTEP.staging_outbound, disposition: DISP.reserved, epcList: [shipment], readPoint: staging.id, bizLocation: staging.id, bizTransactionList: soTxn }));
164
+ this.emit(objectEvent({ eventTime, action: 'DELETE', bizStep: BIZSTEP.shipping, disposition: DISP.in_transit, epcList: [shipment], readPoint: shipDock.id, bizTransactionList: soTxn }));
165
+ order.fulfilled += order.allocated.length;
166
+ // 라인 잔량 차감(멀티SKU 백오더) — 출하 팔레트의 gtin 으로 라인 매핑(라인 SKU distinct).
167
+ if (order.lines)
168
+ for (const epc of order.picked) {
169
+ const g = this.items.get(epc)?.gtin;
170
+ const line = order.lines.find(l => l.gtin === g && l.requested > 0);
171
+ if (line)
172
+ line.requested--;
173
+ }
174
+ for (const epc of order.picked) {
175
+ this.items.delete(epc);
176
+ staging.occupancy--;
177
+ }
178
+ if (order.fulfilled >= order.requested)
179
+ order.status = 'shipped';
180
+ else {
181
+ order.allocated = [];
182
+ order.picked = [];
183
+ order.status = 'created';
184
+ } // 백오더 잔량 재할당
185
+ this.emitOrder(order);
186
+ }
187
+ }
@@ -0,0 +1,28 @@
1
+ import type { GeneratorSpec } from './contract.ts';
2
+ import type { AllocationPolicy } from './allocation-policy.ts';
3
+ import { FlowEngine } from './flow-engine.ts';
4
+ import type { FlowOrder, FlowTask } from './flow-engine.ts';
5
+ /** 편의 — 시나리오 skuMix 로 쓸 부품 클래스. */
6
+ export declare const MES_PART_GTINS: {
7
+ partA: string;
8
+ partB: string;
9
+ };
10
+ /** 편의 — 완제품 클래스(제품 2종). */
11
+ export declare const MES_PRODUCT_GTINS: {
12
+ p1: string;
13
+ p2: string;
14
+ };
15
+ export declare class MesKernel extends FlowEngine {
16
+ private wipSeq;
17
+ private prodSeq;
18
+ constructor(tenantId: string, policy?: AllocationPolicy);
19
+ private productOf;
20
+ /** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
21
+ protected onArrival(spec: GeneratorSpec): void;
22
+ /** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
23
+ protected onOrder(_spec: GeneratorSpec): void;
24
+ /** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + op1(cut, 체인지오버 셋업). */
25
+ protected allocate(o: FlowOrder): void;
26
+ /** op 완료 = 변환. cut: BOM 부품 소비 → WIP. weld: WIP → 완제품(수율: 양품/불량 → OEE 품질 보고). */
27
+ protected onTaskComplete(t: FlowTask): void;
28
+ }
@@ -0,0 +1,126 @@
1
+ /*
2
+ * MES Kernel — 제조: BOM 조립 + 다단계 라우팅(cut→weld) + 수율 + 셋업/체인지오버 + OEE. mechanics 는 FlowEngine base.
3
+ * 부품 수령(다품종) → 작업지시(제품 2종 교대) → BOM 예약 → op1 cut(BOM 소비→WIP) → op2 weld(→완제품, 수율) → 저장.
4
+ *
5
+ * 도메인 깊이:
6
+ * ① BOM = 제품별 다품목 레시피(P1=2A+1B / P2=1A+2B — 상이 → 체인지오버 유발).
7
+ * ② 수율 = 일부 불량(non_sellable) → OEE Quality.
8
+ * ③ 셋업/체인지오버 = work-center 가 제품 전환 시 셋업(changeoverKey=제품 gtin) → OEE Availability.
9
+ * ④ OEE = base 가 무버(설비)별 계측(가동/셋업/기아/품질). cut=cutter·weld=welder 이종 자원.
10
+ */
11
+ import { firstFitPolicy } from "./allocation-policy.js";
12
+ import { FlowEngine } from "./flow-engine.js";
13
+ import { DISP, objectEvent, transactionEvent, gdtiUri, sgtinClass } from "./epcis.js";
14
+ import { MES_BIZSTEP, BTT_PRODORDER, sgtinUri } from "./mes-profile.js";
15
+ const CYCLE_MS = 40_000;
16
+ const SETUP_MS = 15_000; // 체인지오버(제품 전환) 셋업 — OEE 가용성 손실
17
+ const CP = '0614141';
18
+ const WIP_ITEMREF = '066666';
19
+ const WIP_GTIN = sgtinClass(CP, WIP_ITEMREF);
20
+ const YIELD = 0.8; // 양품률 — 나머지는 불량(non_sellable) → OEE 품질 손실
21
+ // 부품(공용) — BOM 라인의 품목 클래스.
22
+ const PART_A = { itemRef: '055551', gtin: sgtinClass(CP, '055551') };
23
+ const PART_B = { itemRef: '055552', gtin: sgtinClass(CP, '055552') };
24
+ // 제품 2종 — BOM 상이(제품 전환 시 cut/weld 체인지오버 셋업 발생).
25
+ const PRODUCTS = [
26
+ { key: 'P1', ref: '077777', gtin: sgtinClass(CP, '077777'), bom: [{ part: PART_A, qty: 2 }, { part: PART_B, qty: 1 }] },
27
+ { key: 'P2', ref: '077778', gtin: sgtinClass(CP, '077778'), bom: [{ part: PART_A, qty: 1 }, { part: PART_B, qty: 2 }] }
28
+ ];
29
+ /** 편의 — 시나리오 skuMix 로 쓸 부품 클래스. */
30
+ export const MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
31
+ /** 편의 — 완제품 클래스(제품 2종). */
32
+ export const MES_PRODUCT_GTINS = { p1: PRODUCTS[0].gtin, p2: PRODUCTS[1].gtin };
33
+ export class MesKernel extends FlowEngine {
34
+ wipSeq = 0;
35
+ prodSeq = 0;
36
+ constructor(tenantId, policy = firstFitPolicy) {
37
+ super(tenantId, policy);
38
+ }
39
+ productOf(gtin) { return PRODUCTS.find(p => p.gtin === gtin); }
40
+ /** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
41
+ onArrival(spec) {
42
+ const rawStore = this.nodeByType('raw-store');
43
+ if (!rawStore)
44
+ return;
45
+ const gtin = this.pickGtin(spec.content.skuMix);
46
+ const part = [PART_A, PART_B].find(p => p.gtin === gtin);
47
+ if (!part)
48
+ return; // BOM 부품 아님
49
+ const epc = sgtinUri(CP, part.itemRef, ++this.epcSeq);
50
+ this.items.set(epc, { epc, gtin: part.gtin, qty: 1, location: rawStore.id, disposition: DISP.sellable });
51
+ rawStore.occupancy++;
52
+ this.emit(objectEvent({ eventTime: this.now(), action: 'ADD', bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: part.gtin, quantity: 1 }], readPoint: rawStore.id, bizLocation: rawStore.id }));
53
+ }
54
+ /** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
55
+ onOrder(_spec) {
56
+ const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
57
+ const id = `order-${++this.orderSeq}`;
58
+ const wo = gdtiUri(CP, '403', ++this.soSeq);
59
+ const order = { id, kind: 'workorder', status: 'created', gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [] };
60
+ this.orders.set(id, order);
61
+ this.emitOrder(order);
62
+ }
63
+ /** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + op1(cut, 체인지오버 셋업). */
64
+ allocate(o) {
65
+ const cut = this.nodeByType('cut-station');
66
+ const product = this.productOf(o.gtin);
67
+ if (!cut || !product)
68
+ return;
69
+ // BOM 전 라인 확보 확인(부족하면 아무것도 예약 안 하고 대기)
70
+ const picks = [];
71
+ for (const line of product.bom) {
72
+ const available = [...this.items.values()]
73
+ .filter(i => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.nodes.get(i.location)?.type === 'raw-store')
74
+ .map(i => ({ epc: i.epc, location: i.location, qty: 1 }));
75
+ const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
76
+ if (chosen.length < line.qty)
77
+ return; // 부품 부족 → 대기
78
+ picks.push(...chosen);
79
+ }
80
+ for (const epc of picks) {
81
+ this.items.get(epc).disposition = DISP.reserved;
82
+ o.allocated.push(epc);
83
+ }
84
+ this.emit(transactionEvent({ eventTime: this.now(), action: 'ADD', bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
85
+ const task = { id: `task-${++this.taskSeq}`, kind: 'cut', status: 'created', itemEpc: o.allocated[0], fromNode: cut.id, toNode: cut.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'cut', fromNode: cut.id, toNode: cut.id, resourceKind: 'cutter' }, CYCLE_MS), orderId: o.id, resourceType: 'cutter', changeoverKey: product.gtin, setupMs: SETUP_MS, intent: 'process' };
86
+ this.tasks.set(task.id, task);
87
+ this.emitTask(task);
88
+ o.status = 'op-cut';
89
+ this.emitOrder(o);
90
+ }
91
+ /** op 완료 = 변환. cut: BOM 부품 소비 → WIP. weld: WIP → 완제품(수율: 양품/불량 → OEE 품질 보고). */
92
+ onTaskComplete(t) {
93
+ const order = this.orders.get(t.orderId);
94
+ const product = this.productOf(order.gtin);
95
+ const eventTime = this.now();
96
+ if (t.kind === 'cut') {
97
+ const cut = this.nodes.get(t.toNode);
98
+ const weld = this.nodeByType('weld-station');
99
+ const inputs = order.allocated.slice(); // BOM 부품 전부
100
+ const wip = sgtinUri(CP, WIP_ITEMREF, ++this.wipSeq);
101
+ // 변환(merge N→1 + product-change): BOM 부품 → WIP. base 원시가 소비·생산·계보 방출.
102
+ this.transform(inputs, [{ epc: wip, gtin: WIP_GTIN, qty: 1, location: cut.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: cut.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
103
+ order.allocated = [wip];
104
+ const wtask = { id: `task-${++this.taskSeq}`, kind: 'weld', status: 'created', itemEpc: wip, fromNode: weld.id, toNode: weld.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'weld', fromNode: weld.id, toNode: weld.id, resourceKind: 'welder' }, CYCLE_MS), orderId: order.id, resourceType: 'welder', changeoverKey: product.gtin, setupMs: SETUP_MS, intent: 'process' };
105
+ this.tasks.set(wtask.id, wtask);
106
+ this.emitTask(wtask);
107
+ order.status = 'op-weld';
108
+ this.emitOrder(order);
109
+ return;
110
+ }
111
+ // weld → 완제품 (transform 1→1, disposition 으로 수율 loss 반영 → OEE 품질)
112
+ const weld = this.nodes.get(t.toNode);
113
+ const fgStore = this.nodeByType('fg-store');
114
+ const wip = order.allocated[0];
115
+ const good = this.rng() < YIELD;
116
+ this.recordOutput(t.resource, good); // OEE 품질(welder 자원별 양품/불량)
117
+ const disp = good ? DISP.sellable : DISP.non_sellable;
118
+ const outputEpc = sgtinUri(CP, product.ref, ++this.prodSeq);
119
+ this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint: weld.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
120
+ this.emit(objectEvent({ eventTime, action: 'OBSERVE', bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outputEpc], quantityList: [{ epcClass: product.gtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
121
+ order.allocated = [];
122
+ order.fulfilled = 1;
123
+ order.status = good ? 'produced' : 'scrapped';
124
+ this.emitOrder(order);
125
+ }
126
+ }
@@ -0,0 +1,9 @@
1
+ export declare const MES_BIZSTEP: {
2
+ readonly receiving: "urn:epcglobal:cbv:bizstep:receiving";
3
+ readonly producing: "urn:epcglobal:cbv:bizstep:commissioning";
4
+ readonly storing: "urn:epcglobal:cbv:bizstep:storing";
5
+ };
6
+ /** 작업지시(Work Order) = 생산 오더 거래 유형. */
7
+ export declare const BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
8
+ /** 직렬 SGTIN URI (원자재 단위·완제품). */
9
+ export declare function sgtinUri(companyPrefix: string, itemRef: string, serial: number): string;
@@ -0,0 +1,17 @@
1
+ /*
2
+ * MES Profile — 제조(manufacturing) 버티컬. 세 번째 프로파일 = base 를 "변환"으로 가장 강하게 stress.
3
+ * 재료 소비 → 제품 생산(이동 아님). EPCIS 2.0 TransformationEvent + epcis 빌더 재사용.
4
+ * (MES=ISA-95 앵커지만 이벤트 모델은 EPCIS TransformationEvent 가 제조 추적의 정합 표준.)
5
+ */
6
+ // MES bizStep — CBV 재사용. producing = commissioning(신규 제품 커미셔닝).
7
+ export const MES_BIZSTEP = {
8
+ receiving: 'urn:epcglobal:cbv:bizstep:receiving', // 원자재 수령
9
+ producing: 'urn:epcglobal:cbv:bizstep:commissioning', // 생산(제품 최초 생성)
10
+ storing: 'urn:epcglobal:cbv:bizstep:storing' // 완제품 저장
11
+ };
12
+ /** 작업지시(Work Order) = 생산 오더 거래 유형. */
13
+ export const BTT_PRODORDER = 'urn:epcglobal:cbv:btt:prodorder';
14
+ /** 직렬 SGTIN URI (원자재 단위·완제품). */
15
+ export function sgtinUri(companyPrefix, itemRef, serial) {
16
+ return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
17
+ }
@@ -0,0 +1,42 @@
1
+ import type { CanonicalEnvelope, Command, CommandAck, ScenarioControl, StateSnapshot, TwinKernel } from './contract.ts';
2
+ export type SubscriptionMessage = {
3
+ kind: 'snapshot';
4
+ revision: number;
5
+ state: StateSnapshot;
6
+ } | {
7
+ kind: 'delta';
8
+ revision: number;
9
+ event: CanonicalEnvelope;
10
+ } | {
11
+ kind: 'clock';
12
+ revision: number;
13
+ simClockMs: number;
14
+ };
15
+ export type SubscriptionHandler = (msg: SubscriptionMessage) => void;
16
+ export interface RuntimeSubscription {
17
+ unsubscribe(): void;
18
+ }
19
+ export declare class TwinRuntime {
20
+ private kernel;
21
+ private subscribers;
22
+ private currentRevision;
23
+ constructor(kernel: TwinKernel);
24
+ /** State 채널 구독 — 즉시 현재 snapshot, 이후 델타(revision 연속). 멀티 구독자 독립. */
25
+ subscribe(handler: SubscriptionHandler): RuntimeSubscription;
26
+ /** gap(revision 불연속) 감지 시 재동기 — 현재 스냅샷. */
27
+ resync(): {
28
+ revision: number;
29
+ state: StateSnapshot;
30
+ };
31
+ /** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 방송(⊥ tick). 클라 보간의 nowSim. */
32
+ nowClock(): Extract<SubscriptionMessage, {
33
+ kind: 'clock';
34
+ }>;
35
+ /** Command 채널. */
36
+ dispatch(cmd: Command): CommandAck;
37
+ /** Clock — host 가 서버 클럭/RAF 로 구동. 실행당 엔진 1개(ADR-0010). */
38
+ tick(dtMs: number): void;
39
+ /** Scenario 채널(sim 모드). */
40
+ get scenario(): ScenarioControl;
41
+ get revision(): number;
42
+ }
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Twin Runtime — host-facing 3채널 facade + 구독 프로토콜 (framework-agnostic).
3
+ * 설계 SoT: integration/face1-contract.md "구독 프로토콜", execution-model.md §3·§4
4
+ *
5
+ * 커널(엔진)을 감싸 host 가 붙일 표면을 제공한다:
6
+ * - State : subscribe(snapshot → delta…, revision 연속) + resync(gap 복구)
7
+ * - Command: dispatch
8
+ * - Scenario: scenario (sim 모드)
9
+ * - Clock : tick (host 가 서버 클럭/RAF 로 구동 — 실행당 엔진 1개)
10
+ *
11
+ * 델타는 커널 이벤트(전이·move-start)로 **sparse** — 매 tick 스트림 아님(execution-model §4).
12
+ * GraphQL sub 등 실제 전송은 이 표면을 얇게 래핑(host 계층). 여기엔 프레임워크 결합 없음.
13
+ */
14
+ export class TwinRuntime {
15
+ kernel;
16
+ subscribers = new Set();
17
+ currentRevision;
18
+ constructor(kernel) {
19
+ this.kernel = kernel;
20
+ this.currentRevision = kernel.getSnapshot().revision;
21
+ // 단일 내부 구독으로 revision 을 추적하고 구독자에게 fan-out.
22
+ // (커널은 emit 당 revision++ 1회 → currentRevision 이 커널 revision 을 미러.)
23
+ kernel.onEvent(event => {
24
+ this.currentRevision++;
25
+ const msg = { kind: 'delta', revision: this.currentRevision, event };
26
+ for (const s of this.subscribers)
27
+ s(msg);
28
+ });
29
+ }
30
+ /** State 채널 구독 — 즉시 현재 snapshot, 이후 델타(revision 연속). 멀티 구독자 독립. */
31
+ subscribe(handler) {
32
+ handler({ kind: 'snapshot', revision: this.currentRevision, state: this.kernel.getSnapshot() });
33
+ this.subscribers.add(handler);
34
+ return { unsubscribe: () => { this.subscribers.delete(handler); } };
35
+ }
36
+ /** gap(revision 불연속) 감지 시 재동기 — 현재 스냅샷. */
37
+ resync() {
38
+ return { revision: this.currentRevision, state: this.kernel.getSnapshot() };
39
+ }
40
+ /** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 방송(⊥ tick). 클라 보간의 nowSim. */
41
+ nowClock() {
42
+ return { kind: 'clock', revision: this.currentRevision, simClockMs: this.kernel.getSnapshot().simClockMs };
43
+ }
44
+ /** Command 채널. */
45
+ dispatch(cmd) {
46
+ return this.kernel.dispatch(cmd);
47
+ }
48
+ /** Clock — host 가 서버 클럭/RAF 로 구동. 실행당 엔진 1개(ADR-0010). */
49
+ tick(dtMs) {
50
+ this.kernel.tick(dtMs);
51
+ }
52
+ /** Scenario 채널(sim 모드). */
53
+ get scenario() {
54
+ return this.kernel.scenario;
55
+ }
56
+ get revision() {
57
+ return this.currentRevision;
58
+ }
59
+ }
@@ -0,0 +1,37 @@
1
+ import type { BoardDef, CanonicalEnvelope, NodeState, ItemState, MoverState, TaskState, OrderState } from './contract.ts';
2
+ /** 마스터 동기 — 선언적 로케이션 upsert/remove (실 소스 연동은 host 계층 Face2 커넥터). */
3
+ export interface MasterUpdate {
4
+ op: 'upsert' | 'remove';
5
+ node: {
6
+ id: string;
7
+ type?: string;
8
+ capacity?: number;
9
+ };
10
+ }
11
+ export interface ProjectedState {
12
+ revision: number;
13
+ nodes: NodeState[];
14
+ items: ItemState[];
15
+ tasks: TaskState[];
16
+ movers: MoverState[];
17
+ orders: OrderState[];
18
+ }
19
+ export declare class StateProjector {
20
+ private master;
21
+ private items;
22
+ private aggregation;
23
+ private tasks;
24
+ private movers;
25
+ private orders;
26
+ revision: number;
27
+ constructor(board: BoardDef);
28
+ /** 마스터 동기 — 로케이션 추가/변경/제거. */
29
+ applyMaster(u: MasterUpdate): void;
30
+ /** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
31
+ apply(e: CanonicalEnvelope): void;
32
+ private applyEpcis;
33
+ /** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
34
+ private remove;
35
+ /** 현재 투영 State — 노드 점유는 아이템 위치 집계로 유도(pure projection). */
36
+ snapshot(): ProjectedState;
37
+ }