@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,124 @@
1
+ /*
2
+ * State Projector — 이벤트 스트림 → State 투영 (모니터링의 "수동 미러").
3
+ *
4
+ * sim 모드: 커널이 State 를 능동 생산(내부 상태 + 이벤트 방출).
5
+ * live 모드: 외부(실 WMS)에서 이벤트가 도착 → 이 projector 가 State 를 재구성.
6
+ * → "계약 동일, 데이터원만 스왑"(execution-model.md §5, ADR-0010). 같은 이벤트면 같은 State.
7
+ *
8
+ * 두 갈래 이벤트를 함께 접는다:
9
+ * - EPCIS(epcis.*) → 재고/위치/조립 (What/Where)
10
+ * - 운영 델타(task/equipment/order.status) → tasks·movers·orders (EPCIS 로 재구성 불가한 절반)
11
+ * 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
12
+ */
13
+ import { OP_EVENT } from "./contract.js";
14
+ export class StateProjector {
15
+ master = new Map();
16
+ items = new Map();
17
+ aggregation = new Map(); // parent SSCC → child EPCs
18
+ tasks = new Map();
19
+ movers = new Map();
20
+ orders = new Map();
21
+ revision = 0;
22
+ constructor(board) {
23
+ for (const n of board.nodes)
24
+ this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity });
25
+ // 무버 기준선(마스터) — equipment.status 델타로 갱신됨.
26
+ for (const m of board.movers)
27
+ this.movers.set(m.id, { id: m.id, kind: m.kind, status: 'idle', location: m.homeNode });
28
+ }
29
+ /** 마스터 동기 — 로케이션 추가/변경/제거. */
30
+ applyMaster(u) {
31
+ if (u.op === 'remove') {
32
+ this.master.delete(u.node.id);
33
+ return;
34
+ }
35
+ const cur = this.master.get(u.node.id);
36
+ this.master.set(u.node.id, {
37
+ id: u.node.id,
38
+ type: u.node.type ?? cur?.type ?? 'unknown',
39
+ capacity: u.node.capacity ?? cur?.capacity ?? 0
40
+ });
41
+ }
42
+ /** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
43
+ apply(e) {
44
+ this.revision++;
45
+ if (e.eventType.startsWith('epcis.')) {
46
+ this.applyEpcis(e.data);
47
+ return;
48
+ }
49
+ switch (e.eventType) {
50
+ case OP_EVENT.task: {
51
+ const d = e.data;
52
+ this.tasks.set(d.taskId, { id: d.taskId, kind: d.kind, status: d.status, fromNode: d.fromNode, toNode: d.toNode, itemRefs: d.itemRefs, resourceRef: d.resourceRef });
53
+ break;
54
+ }
55
+ case OP_EVENT.equipment: {
56
+ const d = e.data;
57
+ this.movers.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, motion: d.motion });
58
+ break;
59
+ }
60
+ case OP_EVENT.order: {
61
+ const d = e.data;
62
+ this.orders.set(d.orderId, { id: d.orderId, kind: d.kind, status: d.status, progress: d.requested ? d.fulfilled / d.requested : 0, held: d.held });
63
+ break;
64
+ }
65
+ // 알 수 없는 eventType 은 무시(전방 호환).
66
+ }
67
+ }
68
+ applyEpcis(ev) {
69
+ if (ev.type === 'AggregationEvent') {
70
+ if (ev.action === 'ADD' && ev.childEPCs?.length)
71
+ this.aggregation.set(ev.parentID, [...ev.childEPCs]);
72
+ else if (ev.action === 'DELETE')
73
+ this.aggregation.delete(ev.parentID);
74
+ return;
75
+ }
76
+ if (ev.type === 'TransactionEvent')
77
+ return; // 거래 연결 — 오더 상태는 order.status 델타로
78
+ if (ev.type === 'TransformationEvent') {
79
+ // 변환: 입력 소비(제거) → 출력 생산(readPoint 에 등장)
80
+ for (const epc of ev.inputEPCList ?? [])
81
+ this.remove(epc);
82
+ const loc = ev.readPoint?.id ?? '';
83
+ for (const epc of ev.outputEPCList ?? [])
84
+ this.items.set(epc, { epc, location: loc, disposition: ev.disposition });
85
+ return;
86
+ }
87
+ // ObjectEvent
88
+ if (ev.action === 'DELETE') {
89
+ for (const epc of ev.epcList)
90
+ this.remove(epc);
91
+ return;
92
+ }
93
+ const loc = ev.readPoint?.id;
94
+ const gtin = ev.quantityList?.[0]?.epcClass;
95
+ for (const epc of ev.epcList) {
96
+ const cur = this.items.get(epc);
97
+ this.items.set(epc, { epc, gtin: gtin ?? cur?.gtin, location: loc ?? cur?.location ?? '', disposition: ev.disposition ?? cur?.disposition });
98
+ }
99
+ }
100
+ /** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
101
+ remove(epc) {
102
+ this.items.delete(epc);
103
+ const children = this.aggregation.get(epc);
104
+ if (children) {
105
+ this.aggregation.delete(epc);
106
+ for (const c of children)
107
+ this.remove(c);
108
+ }
109
+ }
110
+ /** 현재 투영 State — 노드 점유는 아이템 위치 집계로 유도(pure projection). */
111
+ snapshot() {
112
+ const occ = new Map();
113
+ for (const it of this.items.values())
114
+ occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
115
+ return {
116
+ revision: this.revision,
117
+ nodes: [...this.master.values()].map(n => ({ id: n.id, type: n.type, capacity: n.capacity, occupancy: occ.get(n.id) ?? 0 })),
118
+ items: [...this.items.values()].map(i => ({ epc: i.epc, gtin: i.gtin, location: i.location, disposition: i.disposition })),
119
+ tasks: [...this.tasks.values()].map(t => ({ ...t })),
120
+ movers: [...this.movers.values()].map(m => ({ ...m })),
121
+ orders: [...this.orders.values()].map(o => ({ ...o }))
122
+ };
123
+ }
124
+ }
@@ -0,0 +1,28 @@
1
+ import type { StateSnapshot } from './contract.ts';
2
+ import { type StateDivergence } from './divergence.ts';
3
+ /** 관찰 대상 = fork 가능한 트윈(FlowEngine 이 만족). */
4
+ export interface ObservableTwin {
5
+ getSnapshot(): StateSnapshot;
6
+ tick(dtMs: number): void;
7
+ fork(): ObservableTwin;
8
+ }
9
+ export interface DivergenceAlert {
10
+ forecastAtSimMs: number;
11
+ horizonSimMs: number;
12
+ divergence: StateDivergence;
13
+ }
14
+ export interface TwinObserverOptions {
15
+ horizonMs: number;
16
+ tickMs?: number;
17
+ onDivergence?: (alert: DivergenceAlert) => void;
18
+ }
19
+ export declare class TwinObserver {
20
+ private live;
21
+ private opts;
22
+ private pending;
23
+ constructor(live: ObservableTwin, opts: TwinObserverOptions);
24
+ /** 관측 1회 — 만기 예측을 실제와 대조(발산 알림) + 새 예측 생성. 호스트가 주기적으로 호출. */
25
+ observe(): void;
26
+ /** 대기 중(아직 만기 안 된) 예측 수 — 진단용. */
27
+ get pendingCount(): number;
28
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ * TwinObserver — 자동 정합 루프 (트윈이 자기 모델↔현실 이탈을 능동 감지).
3
+ *
4
+ * fork(현재예측) + compareStates(정합)를 살아있는 루프로: 주기적으로 fork 해서 horizon 앞으로 굴려
5
+ * 예측을 저장하고, 실제(live)가 그 시점에 도달하면 예측과 대조해 드리프트를 알린다.
6
+ * 순수 sim(방해 없음)이면 예측=미래(RNG 연속) → 발산 0. 방해(커맨드·실이벤트)면 발산 → 이상/개입 신호.
7
+ */
8
+ import { compareStates } from "./divergence.js";
9
+ export class TwinObserver {
10
+ live;
11
+ opts;
12
+ pending = [];
13
+ constructor(live, opts) {
14
+ this.live = live;
15
+ this.opts = opts;
16
+ }
17
+ /** 관측 1회 — 만기 예측을 실제와 대조(발산 알림) + 새 예측 생성. 호스트가 주기적으로 호출. */
18
+ observe() {
19
+ const now = this.live.getSnapshot().simClockMs;
20
+ // 1) 만기 도래 예측을 실제와 대조
21
+ const due = this.pending.filter(p => p.horizonSimMs <= now);
22
+ this.pending = this.pending.filter(p => p.horizonSimMs > now);
23
+ for (const p of due) {
24
+ const d = compareStates(p.predicted, this.live.getSnapshot());
25
+ if (d.hasDrift)
26
+ this.opts.onDivergence?.({ forecastAtSimMs: p.madeAtSimMs, horizonSimMs: p.horizonSimMs, divergence: d });
27
+ }
28
+ // 2) 새 예측: fork → horizon 까지 굴림 → 저장
29
+ const fc = this.live.fork();
30
+ const step = this.opts.tickMs ?? 1000;
31
+ const target = now + this.opts.horizonMs;
32
+ let guard = 0;
33
+ while (fc.getSnapshot().simClockMs < target && guard++ < 1_000_000)
34
+ fc.tick(step);
35
+ this.pending.push({ madeAtSimMs: now, horizonSimMs: fc.getSnapshot().simClockMs, predicted: fc.getSnapshot() });
36
+ }
37
+ /** 대기 중(아직 만기 안 된) 예측 수 — 진단용. */
38
+ get pendingCount() {
39
+ return this.pending.length;
40
+ }
41
+ }
@@ -0,0 +1,13 @@
1
+ export declare const BIZSTEP: {
2
+ readonly receiving: "urn:epcglobal:cbv:bizstep:receiving";
3
+ readonly storing: "urn:epcglobal:cbv:bizstep:storing";
4
+ readonly picking: "urn:epcglobal:cbv:bizstep:picking";
5
+ readonly packing: "urn:epcglobal:cbv:bizstep:packing";
6
+ readonly staging_outbound: "urn:epcglobal:cbv:bizstep:staging_outbound";
7
+ readonly shipping: "urn:epcglobal:cbv:bizstep:shipping";
8
+ readonly replenishing: "urn:epcglobal:cbv:bizstep:replenishing";
9
+ };
10
+ export declare const BTT: {
11
+ readonly po: "urn:epcglobal:cbv:btt:po";
12
+ readonly so: "urn:epcglobal:cbv:btt:so";
13
+ };
@@ -0,0 +1,20 @@
1
+ /*
2
+ * WMS Profile — 물류창고 어휘(CBV bizStep + business transaction type).
3
+ * EPCIS machinery(타입·빌더·검증기·URI·DISP)는 도메인-중립 `epcis.ts` 에 있다.
4
+ * 설계 SoT: operato-twin/design/profiles/wms.md §11
5
+ */
6
+ // bizStep — CBV URN (wms.md §11 확정 어휘)
7
+ export const BIZSTEP = {
8
+ receiving: 'urn:epcglobal:cbv:bizstep:receiving',
9
+ storing: 'urn:epcglobal:cbv:bizstep:storing',
10
+ picking: 'urn:epcglobal:cbv:bizstep:picking',
11
+ packing: 'urn:epcglobal:cbv:bizstep:packing',
12
+ staging_outbound: 'urn:epcglobal:cbv:bizstep:staging_outbound',
13
+ shipping: 'urn:epcglobal:cbv:bizstep:shipping',
14
+ replenishing: 'urn:epcglobal:cbv:bizstep:replenishing'
15
+ };
16
+ // bizTransaction 유형 — CBV btt (§2 "거래" 매핑: PO=입고 ASN, SO=출고 오더)
17
+ export const BTT = {
18
+ po: 'urn:epcglobal:cbv:btt:po',
19
+ so: 'urn:epcglobal:cbv:btt:so'
20
+ };
@@ -0,0 +1,29 @@
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
+ export type YardMode = 'live' | 'drop';
6
+ export declare class YmsKernel extends FlowEngine {
7
+ private doorRR;
8
+ private cargoSeq;
9
+ private trailerCargo;
10
+ private apptMode;
11
+ /** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
12
+ mode: YardMode;
13
+ constructor(tenantId: string, policy?: AllocationPolicy, mode?: YardMode);
14
+ /** 인바운드(하차) 자극 — 화물 적재된 트레일러 도착. */
15
+ protected onArrival(_spec: GeneratorSpec): void;
16
+ /** 아웃바운드(상차) 자극 — 빈 트레일러 도착(도크에서 staging 화물 적재 예정). */
17
+ protected onOrder(_spec: GeneratorSpec): void;
18
+ /** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
19
+ private spawnAppointment;
20
+ /**
21
+ * 스케줄 게이트(base 가 created 오더마다 매 tick 호출) — 조건 충족 시 도크로 이동:
22
+ * ① 창 도래(clock ≥ windowStart) ② 배정 도크도어 예약 가능
23
+ * ③ drop: 트레일러가 야드 주차됨 / live: 게이트 대기 중 ④ 아웃바운드: staging 적재 화물 충분
24
+ * 하나라도 안 되면 대기(다음 tick 재시도). = 시간창 예약 스케줄링.
25
+ */
26
+ protected allocate(o: FlowOrder): void;
27
+ /** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
28
+ protected onTaskComplete(t: FlowTask): void;
29
+ }
@@ -0,0 +1,181 @@
1
+ /*
2
+ * YMS Kernel — 야드 어포인트먼트 스케줄링(도메인). mechanics 는 FlowEngine base 재사용.
3
+ * 인바운드(하차): 게이트-인 → 야드 대기 → (창+도어) 도크 → 하차 → 출차.
4
+ * 아웃바운드(상차): 빈 트레일러 도착 → 야드 대기 → (창+도어+staging 화물) 도크 → 상차 → 적재 출차.
5
+ *
6
+ * ★ 검증 질문: base 가 "시간창 예약 스케줄링"을 흡수하는가?
7
+ * → clock(base) + 노드 점유(도크도어 예약) + allocate hook(스케줄 게이트)로 표현 — base 변경 0.
8
+ *
9
+ * 상·하차 대칭(AggregationEvent):
10
+ * - 하차(inbound): 게이트-인에 트레일러←화물 조립(ADD), 도크에서 분해(DELETE) → 화물이 staging 에 등장(WMS receiving hand-off).
11
+ * - 상차(outbound): 빈 트레일러 도착, 도크에서 staging 화물을 트레일러로 조립(ADD) → 적재 트레일러 출차(WMS shipping hand-off).
12
+ * 두 방향이 inbound-unload → staging → outbound-load 로 이어지면 크로스도크.
13
+ *
14
+ * live vs drop(야드 운영 모드):
15
+ * - drop(기본): 트레일러를 야드 슬롯에 내려놓고(주차) 창 도래 시 호슬러가 도크로 pull. 야드 버퍼 사용.
16
+ * - live: 게이트에서 대기하다 창 도래 시 도크로 직행(야드 미경유). 게이트/도크를 더 오래 점유.
17
+ */
18
+ import { firstFitPolicy } from "./allocation-policy.js";
19
+ import { FlowEngine } from "./flow-engine.js";
20
+ import { DISP, objectEvent, transactionEvent, gdtiUri, ssccUri } from "./epcis.js";
21
+ import { YARD_BIZSTEP, BTT_DELIVERY, graiUri } from "./yms-profile.js";
22
+ const TRAVEL_MS = 20_000; // 야드 이동
23
+ const DWELL_MS = 30_000; // 도크 체류(상·하차) — depart 이동에 folded
24
+ const WINDOW_DELAY_MS = 40_000; // 도착 후 어포인트먼트 창까지(early arrival → 대기)
25
+ const CARGO_PER_TRAILER = 2; // 트레일러 적재/하역 화물(SSCC) 수
26
+ const CP = '0614141';
27
+ export class YmsKernel extends FlowEngine {
28
+ doorRR = 0;
29
+ cargoSeq = 0;
30
+ trailerCargo = new Map(); // 트레일러 epc → 화물 SSCC(하차 전 opaque / 상차 예약분)
31
+ apptMode = new Map(); // orderId → 상하차 모드(생성 시 캡처)
32
+ /** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
33
+ mode;
34
+ constructor(tenantId, policy = firstFitPolicy, mode = 'drop') {
35
+ super(tenantId, policy);
36
+ this.mode = mode;
37
+ }
38
+ /** 인바운드(하차) 자극 — 화물 적재된 트레일러 도착. */
39
+ onArrival(_spec) { this.spawnAppointment('appointment'); }
40
+ /** 아웃바운드(상차) 자극 — 빈 트레일러 도착(도크에서 staging 화물 적재 예정). */
41
+ onOrder(_spec) { this.spawnAppointment('appointment-out'); }
42
+ /** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
43
+ spawnAppointment(kind) {
44
+ const gate = this.nodeByType('gate');
45
+ const doors = [...this.nodes.values()].filter(n => n.type === 'dock-door').sort((a, b) => a.id.localeCompare(b.id));
46
+ if (!gate || doors.length === 0)
47
+ return;
48
+ const inbound = kind === 'appointment';
49
+ const epc = graiUri(CP, '10', ++this.epcSeq);
50
+ this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
51
+ gate.occupancy++;
52
+ this.emit(objectEvent({ eventTime: this.now(), action: 'ADD', bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
53
+ // 인바운드: 화물 적재된 채 도착 → 조립(트레일러←화물) 기록. 아웃바운드: 빈 트레일러(화물은 도크에서 적재).
54
+ if (inbound) {
55
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(CP, ++this.cargoSeq));
56
+ this.trailerCargo.set(epc, cargo);
57
+ this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id }); // 적재된 채 도착(자식 opaque, 미materialize)
58
+ }
59
+ const door = doors[this.doorRR++ % doors.length];
60
+ const id = `order-${++this.orderSeq}`;
61
+ const appt = gdtiUri(CP, '404', ++this.soSeq);
62
+ const order = { id, kind, status: 'created', requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id, windowStartMs: this.clockMs + WINDOW_DELAY_MS };
63
+ this.orders.set(id, order);
64
+ this.apptMode.set(id, this.mode);
65
+ this.emitOrder(order);
66
+ // drop: 야드 주차(gate→yard-slot). live: 태스크 없음(게이트 대기 → allocate 가 도크 직행).
67
+ if (this.mode === 'drop') {
68
+ const slotId = this.policy.selectPlacement({ item: { epc, gtin: 'trailer', qty: 1 }, slots: this.slotViews('yard-slot') });
69
+ if (!slotId)
70
+ return; // 야드 만차 → 게이트 대기
71
+ const task = { id: `task-${++this.taskSeq}`, kind: 'spot', status: 'created', itemEpc: epc, fromNode: gate.id, toNode: slotId, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'spot', fromNode: gate.id, toNode: slotId }, TRAVEL_MS) };
72
+ this.tasks.set(task.id, task);
73
+ this.emitTask(task);
74
+ }
75
+ }
76
+ /**
77
+ * 스케줄 게이트(base 가 created 오더마다 매 tick 호출) — 조건 충족 시 도크로 이동:
78
+ * ① 창 도래(clock ≥ windowStart) ② 배정 도크도어 예약 가능
79
+ * ③ drop: 트레일러가 야드 주차됨 / live: 게이트 대기 중 ④ 아웃바운드: staging 적재 화물 충분
80
+ * 하나라도 안 되면 대기(다음 tick 재시도). = 시간창 예약 스케줄링.
81
+ */
82
+ allocate(o) {
83
+ const trailer = this.items.get(o.allocated[0]);
84
+ if (!trailer)
85
+ return;
86
+ if (this.clockMs < (o.windowStartMs ?? 0))
87
+ return; // 창 도래 전
88
+ const door = this.nodes.get(o.dockDoor);
89
+ const inflight = [...this.tasks.values()].some(t => t.status !== 'completed' && t.toNode === door.id);
90
+ if (door.occupancy > 0 || inflight)
91
+ return; // 도어 사용중/예약됨 → 대기(직렬화)
92
+ const mode = this.apptMode.get(o.id) ?? 'drop';
93
+ if (mode === 'drop' && this.nodes.get(trailer.location)?.type !== 'yard-slot')
94
+ return; // 아직 미주차
95
+ const outbound = o.kind === 'appointment-out';
96
+ const bizStep = outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading;
97
+ if (outbound) {
98
+ // 아웃바운드: staging 에 적재 화물(sellable, 미예약) 충분해야 도크로. 예약(disposition 미변경 → 조용한 예약).
99
+ const staging = this.nodeByType('staging');
100
+ const reserved = new Set([...this.trailerCargo.values()].flat());
101
+ const avail = [...this.items.values()]
102
+ .filter(i => i.location === staging?.id && i.disposition === DISP.sellable && !reserved.has(i.epc))
103
+ .sort((a, b) => a.epc.localeCompare(b.epc));
104
+ if (!staging || avail.length < CARGO_PER_TRAILER)
105
+ return; // 적재 화물 부족 → 대기
106
+ this.trailerCargo.set(trailer.epc, avail.slice(0, CARGO_PER_TRAILER).map(i => i.epc));
107
+ }
108
+ trailer.disposition = DISP.reserved;
109
+ this.emit(transactionEvent({ eventTime: this.now(), action: 'ADD', bizStep, bizTransactionList: [{ type: BTT_DELIVERY, bizTransaction: o.bizTransaction }], epcList: [trailer.epc], readPoint: door.id }));
110
+ // drop: 야드→도크 pull. live: 게이트→도크 직행(spot-live). 둘 다 toNode=도크도어.
111
+ const dockKind = mode === 'drop' ? 'pull' : 'spot-live';
112
+ const task = { id: `task-${++this.taskSeq}`, kind: dockKind, status: 'created', itemEpc: trailer.epc, fromNode: trailer.location, toNode: door.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: dockKind, fromNode: trailer.location, toNode: door.id }, TRAVEL_MS), orderId: o.id };
113
+ this.tasks.set(task.id, task);
114
+ this.emitTask(task);
115
+ o.status = 'docking';
116
+ this.emitOrder(o);
117
+ }
118
+ /** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
119
+ onTaskComplete(t) {
120
+ const trailer = this.items.get(t.itemEpc);
121
+ const from = this.nodes.get(t.fromNode);
122
+ const to = this.nodes.get(t.toNode);
123
+ from.occupancy--;
124
+ to.occupancy++;
125
+ trailer.location = to.id;
126
+ if (t.kind === 'dwell') {
127
+ // 도크 체류(무자원 dwell) 완료 → 출차 이동(transport). 체류 중 호슬러 미점유(drop-and-hook 충실).
128
+ const gate = this.nodeByType('gate');
129
+ const dep = { id: `task-${++this.taskSeq}`, kind: 'depart', status: 'created', itemEpc: trailer.epc, fromNode: t.toNode, toNode: gate.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'depart', fromNode: t.toNode, toNode: gate.id }, TRAVEL_MS), orderId: t.orderId };
130
+ this.tasks.set(dep.id, dep);
131
+ this.emitTask(dep);
132
+ return;
133
+ }
134
+ if (to.type === 'yard-slot') {
135
+ // drop 주차 — 야드 대기(창 도래까지). 도어는 depart 완료까지 미점유.
136
+ trailer.disposition = DISP.sellable;
137
+ this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: YARD_BIZSTEP.staging, disposition: DISP.sellable, epcList: [trailer.epc], readPoint: to.id, bizLocation: to.id }));
138
+ return;
139
+ }
140
+ if (to.type === 'dock-door') {
141
+ const order = t.orderId ? this.orders.get(t.orderId) : undefined;
142
+ const outbound = order?.kind === 'appointment-out';
143
+ const staging = this.nodeByType('staging');
144
+ const cargo = this.trailerCargo.get(trailer.epc) ?? [];
145
+ // 도크 도착 — 상·하차 관측. 도어는 depart 완료까지 점유(예약 유지).
146
+ this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading, disposition: DISP.in_progress, epcList: [trailer.epc], readPoint: to.id, bizLocation: to.id }));
147
+ if (outbound) {
148
+ // 상차: staging 화물 → 트레일러 조립(merge). 화물은 staging 이탈(consume → 트레일러 안 opaque, 적재 출차).
149
+ if (cargo.length && staging) {
150
+ this.aggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.loading, readPoint: to.id, consume: { readPoint: staging.id, disposition: DISP.in_transit } });
151
+ }
152
+ this.trailerCargo.delete(trailer.epc); // 적재 완료 → 예약분 소진(트레일러가 화물 보유)
153
+ trailer.disposition = DISP.sellable; // 적재 완료(loaded)
154
+ }
155
+ else if (cargo.length && staging) {
156
+ // 하차: 트레일러←화물 분해(split) → 화물이 staging 에 materialize(WMS receiving hand-off).
157
+ this.disaggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.unloading, readPoint: to.id, materialize: { location: staging.id, disposition: DISP.sellable } });
158
+ this.trailerCargo.delete(trailer.epc);
159
+ }
160
+ // 도크 체류 = 무자원 dwell 태스크(호슬러 해방). 완료 시 depart 이동 생성.
161
+ const dwell = { id: `task-${++this.taskSeq}`, kind: 'dwell', status: 'created', itemEpc: trailer.epc, fromNode: to.id, toNode: to.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: 'dwell', fromNode: to.id, toNode: to.id }, DWELL_MS), orderId: t.orderId, intent: 'dwell' };
162
+ this.tasks.set(dwell.id, dwell);
163
+ this.emitTask(dwell);
164
+ if (order) {
165
+ order.status = 'dwelling';
166
+ this.emitOrder(order);
167
+ }
168
+ return;
169
+ }
170
+ // gate (depart 완료) → 출차(DELETE). 인바운드=빈 트레일러, 아웃바운드=적재 트레일러(화물은 상차 시 이미 opaque 이탈).
171
+ this.emit(objectEvent({ eventTime: this.now(), action: 'DELETE', bizStep: YARD_BIZSTEP.departing, disposition: DISP.in_transit, epcList: [trailer.epc], readPoint: to.id }));
172
+ this.items.delete(trailer.epc);
173
+ to.occupancy--; // 게이트 통과(잔류 안 함)
174
+ const order = t.orderId ? this.orders.get(t.orderId) : undefined;
175
+ if (order) {
176
+ order.fulfilled = 1;
177
+ order.status = 'departed';
178
+ this.emitOrder(order);
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,11 @@
1
+ export declare const YARD_BIZSTEP: {
2
+ readonly arriving: "urn:epcglobal:cbv:bizstep:arriving";
3
+ readonly staging: "urn:epcglobal:cbv:bizstep:staging";
4
+ readonly loading: "urn:epcglobal:cbv:bizstep:loading";
5
+ readonly unloading: "urn:epcglobal:cbv:bizstep:unloading";
6
+ readonly departing: "urn:epcglobal:cbv:bizstep:departing";
7
+ };
8
+ /** 어포인트먼트/배송 거래 유형(CBV btt). */
9
+ export declare const BTT_DELIVERY = "urn:epcglobal:cbv:btt:deliv";
10
+ /** 트레일러/컨테이너 = GRAI(Global Returnable Asset Identifier, 반납형 자산). */
11
+ export declare function graiUri(companyPrefix: string, assetType: string, serial: number): string;
@@ -0,0 +1,22 @@
1
+ /*
2
+ * YMS Profile — yard(야드) 버티컬. 공유 코어 재사용 검증용 두 번째 프로파일.
3
+ * EPCIS 빌더(objectEvent 등)·DISP 는 epcis 에서 재사용하고, 야드 어휘만 더한다.
4
+ * (설계: profiles/yms.md, 03-reference-standards §YMS=GS1 EPCIS zone)
5
+ *
6
+ * 목적: contract·state-projector·runtime·allocation-policy·EPCIS 빌더가 WMS 전용이 아니라
7
+ * 도메인-일반임을 야드로 증명. 코어 수정 없이 프로파일만 추가되면 헌장 원칙 2 검증.
8
+ */
9
+ // 야드 bizStep — CBV (yms.md §11).
10
+ export const YARD_BIZSTEP = {
11
+ arriving: 'urn:epcglobal:cbv:bizstep:arriving', // 게이트-인
12
+ staging: 'urn:epcglobal:cbv:bizstep:staging', // 야드 슬롯 주차·대기
13
+ loading: 'urn:epcglobal:cbv:bizstep:loading', // 도크 작업(상차: 트레일러←화물)
14
+ unloading: 'urn:epcglobal:cbv:bizstep:unloading', // 도크 작업(하차: 트레일러→화물)
15
+ departing: 'urn:epcglobal:cbv:bizstep:departing' // 게이트-아웃
16
+ };
17
+ /** 어포인트먼트/배송 거래 유형(CBV btt). */
18
+ export const BTT_DELIVERY = 'urn:epcglobal:cbv:btt:deliv';
19
+ /** 트레일러/컨테이너 = GRAI(Global Returnable Asset Identifier, 반납형 자산). */
20
+ export function graiUri(companyPrefix, assetType, serial) {
21
+ return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, '0')}`;
22
+ }