@operato/twin-kernel 0.2.0 → 0.2.2

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.
@@ -8,6 +8,20 @@ export interface CanonicalEnvelope<T = unknown> {
8
8
  data: T;
9
9
  }
10
10
  export type TaskStatus = 'created' | 'assigned' | 'in-progress' | 'completed';
11
+ /**
12
+ * 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
13
+ *
14
+ * 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는 노드 상태 채널이
15
+ * 없어 비어 있었다. 화면은 그 값을 그대로 보여 주고 있었다 — **정보처럼 보이는데 정보가 아니었다.**
16
+ *
17
+ * 문턱은 병목 주목(`deriveAttentions`)이 쓰는 것과 **같다**: 90% 이상이면 임박, 100% 이상이면 포화.
18
+ * 규칙이 둘이면 화면과 주목이 다른 말을 한다. 용량을 모르면 상태도 모른다(undefined — 꾸미지 않는다).
19
+ */
20
+ export declare const NODE_SATURATION_NEAR = 0.9;
21
+ export declare function nodeStatusOf(n: {
22
+ occupancy?: number;
23
+ capacity?: number;
24
+ }): 'available' | 'near-full' | 'full' | undefined;
11
25
  export interface NodeState {
12
26
  id: string;
13
27
  type: string;
@@ -29,6 +43,7 @@ export interface NodeState {
29
43
  */
30
44
  parallelism?: number;
31
45
  occupancy: number;
46
+ /** 포화도 파생 상태 — `nodeStatusOf` 가 낸다(두 구동이 같은 함수를 쓴다). 용량 미상이면 없다. */
32
47
  status?: string;
33
48
  parentId?: string;
34
49
  /**
@@ -182,9 +197,18 @@ export interface TaskState {
182
197
  resourceRef?: string;
183
198
  orderId?: string;
184
199
  progress?: number;
185
- /** 남은 시간·총 소요(ms) — 진행 중인 작업을 이어서 굴리는 데 필요(씨앗의 충실도). */
200
+ /**
201
+ * 남은 시간·총 소요(ms) — 진행 중인 작업을 이어서 굴리는 데 필요(씨앗의 충실도).
202
+ * `remainingMs` 는 **마지막 전이 시점의 값**이다. 델타는 매 tick 오지 않으므로(설계) 미러가 든 값은
203
+ * 그때의 것이고, 지금 값은 `startedAtSimMs` 로 보간한다 — 모션(MoverMotion)과 같은 규율.
204
+ */
186
205
  remainingMs?: number;
187
206
  durationMs?: number;
207
+ /**
208
+ * 착수 시각(절대 sim-clock) — **보간 앵커.** 이것이 없으면 미러는 "그때 얼마 남았었나" 만 알고
209
+ * "지금 얼마 남았나" 를 못 낸다. progress = (now − startedAtSimMs) / durationMs.
210
+ */
211
+ startedAtSimMs?: number;
188
212
  /** 작업 의도 — 무자원이 설계인지(체류) 기록 누락인지 구별하는 근거. */
189
213
  intent?: 'transport' | 'process' | 'dwell';
190
214
  /**
@@ -199,6 +223,18 @@ export interface OrderState {
199
223
  status: string;
200
224
  progress?: number;
201
225
  held?: boolean;
226
+ /**
227
+ * 요청·이행 수량과 라인 — **진척(progress)으로 압축하지 않는다.**
228
+ *
229
+ * 예전에는 상태가 `progress` 하나만 들고 있어서, 미러에서 예측을 세우려면 **저널을 따로 읽어**
230
+ * 오더 원값을 되찾아야 했다(`buildForecastKernel`). 남은 데맨드(라인별 requested − fulfilled)를
231
+ * 모르면 "무엇을 얼마나 더 내보내야 하나" 를 재계획할 수 없기 때문이다.
232
+ *
233
+ * 진척은 이 둘에서 나오는 파생값이다 — 파생을 남기고 원본을 버린 것이 잘못이었다.
234
+ */
235
+ requested?: number;
236
+ fulfilled?: number;
237
+ lines?: ObservedOrderLine[];
202
238
  }
203
239
  export type AttentionSeverity = 'low' | 'medium' | 'high' | 'critical';
204
240
  export type AttentionState = 'active' | 'acknowledged' | 'cleared';
@@ -380,10 +416,17 @@ export interface QualityDelta {
380
416
  }
381
417
  export interface TaskStatusDelta {
382
418
  taskId: string;
419
+ /** 착수 시각(절대 sim-clock) — 보간 앵커. TaskState 와 같은 뜻. */
420
+ startedAtSimMs?: number;
383
421
  /** 투입된 사람들 — 미러가 인원 배정을 그대로 비추려면 델타에 실려야 한다. */
384
422
  personnel?: string[];
385
423
  /** 투입된 물리 자산(팔레트 등). */
386
424
  assets?: string[];
425
+ /**
426
+ * 투입된 설비 전부 — `resourceRef` 는 그중 **대표**(첫 번째)다.
427
+ * 대표만 두면 "크레인 + 스프레더" 처럼 함께 잡히는 설비가 상태에서 사라진다.
428
+ */
429
+ resources?: string[];
387
430
  orderId?: string;
388
431
  kind: string;
389
432
  status: TaskStatus;
@@ -414,6 +457,8 @@ export interface EquipmentStatusDelta {
414
457
  kind: string;
415
458
  status: string;
416
459
  location?: string;
460
+ /** 지금 붙어 있는 작업 — 사람·자산 델타와 같은 자리. 없으면 미러가 작업↔자원 연결을 모른다. */
461
+ taskId?: string;
417
462
  motion?: MoverMotion;
418
463
  }
419
464
  /** 관측된 오더 라인(SKU 데맨드) — 실 시스템 오더는 품목 라인을 가짐. 이행 예측(남은 데맨드 재계획)에 필요. */
package/dist/contract.js CHANGED
@@ -2,6 +2,23 @@
2
2
  * Face 1 — 3채널 계약 (walking skeleton 범위).
3
3
  * 설계 SoT: operato-twin/design/integration/face1-contract.md
4
4
  */
5
+ /**
6
+ * 자리의 상태 — **포화도에서 파생한다.** 저장하는 값이 아니다.
7
+ *
8
+ * 예전에는 시뮬이 `'idle'` 로 두고 한 번도 바꾸지 않았고(변경 지점 0), 미러에는 노드 상태 채널이
9
+ * 없어 비어 있었다. 화면은 그 값을 그대로 보여 주고 있었다 — **정보처럼 보이는데 정보가 아니었다.**
10
+ *
11
+ * 문턱은 병목 주목(`deriveAttentions`)이 쓰는 것과 **같다**: 90% 이상이면 임박, 100% 이상이면 포화.
12
+ * 규칙이 둘이면 화면과 주목이 다른 말을 한다. 용량을 모르면 상태도 모른다(undefined — 꾸미지 않는다).
13
+ */
14
+ export const NODE_SATURATION_NEAR = 0.9;
15
+ export function nodeStatusOf(n) {
16
+ const cap = n.capacity;
17
+ if (!(typeof cap === 'number' && cap > 0))
18
+ return undefined;
19
+ const r = (n.occupancy ?? 0) / cap;
20
+ return r >= 1 ? 'full' : r >= NODE_SATURATION_NEAR ? 'near-full' : 'available';
21
+ }
5
22
  // ── 운영 델타(비-EPCIS) — State 채널의 나머지 절반 ──────────────────────────
6
23
  // EPCIS 이벤트는 재고/위치만 재구성 가능. tasks·movers(equipment)·orders 의 운영 상태는
7
24
  // 이 델타로 미러한다. envelope.eventType = 'task.status' | 'equipment.status' | 'order.status'.
@@ -126,6 +126,19 @@ export interface OperationDef {
126
126
  assetClass?: string;
127
127
  quantity: number;
128
128
  }[];
129
+ /**
130
+ * 필요 설비 — **ISA-95 `OperationsSegment.EquipmentSpecification`**(`EquipmentClassID` + `Quantity`).
131
+ *
132
+ * 예전에는 작업 하나에 설비 **한 대**만 붙었다(`resourceType` 하나). 그래서 "크레인 1대 + 스프레더
133
+ * 1대", "용접 로봇 2대가 함께" 같은 현장을 표현할 수 없었고, 그 동시 점유가 만드는 줄이 예측에서
134
+ * 사라졌다. 인원·자산과 **같은 규칙**으로 요구한다: 등급 + 대수, 부분 확보 없이 전량 아니면 대기.
135
+ *
136
+ * 미지정이면 기존 거동(`resourceType` 한 대, 없으면 아무 유휴 설비).
137
+ */
138
+ equipmentSpecification?: {
139
+ equipmentClass?: string;
140
+ quantity: number;
141
+ }[];
129
142
  }
130
143
  /** 라우트(오퍼레이션 시퀀스) — ISA-95 ProcessSegment 연결. */
131
144
  export interface RouteDef {
@@ -1,4 +1,4 @@
1
- import type { Attention, BoardDef, Command, CommandAck, EventHandler, MoverMotion, OeeMetrics, AssetState, GeneratorSpec, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, NodeState, ItemState, MoverState, OrderStatusDelta, TaskState } from './contract.ts';
1
+ import type { Attention, BoardDef, CanonicalEnvelope, Command, CommandAck, EventHandler, MoverMotion, OeeMetrics, AssetState, GeneratorSpec, OrderState, PersonState, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, NodeState, ItemState, MoverState, OrderStatusDelta, TaskState } from './contract.ts';
2
2
  import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
3
3
  import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
4
4
  import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
@@ -12,13 +12,29 @@ export interface FlowNode {
12
12
  status: string;
13
13
  parentId?: string;
14
14
  }
15
+ /**
16
+ * 시뮬이 들고 있는 물품 — **계약(ItemState)을 축소하지 않는다.**
17
+ *
18
+ * 미러(투영기)는 로트·단위·소속·마스터데이터까지 들고 있는데 시뮬은 여섯 필드뿐이었다. 그래서 같은
19
+ * 사실을 두 구동이 다르게 말했다(파리티 테스트가 잡았다). 여기서 갖는 것은 **보유해야 하는 상태**다 —
20
+ * 품번 키·로트처럼 식별자에서 순수하게 나오는 값은 저장하지 않고 스냅샷에서 파생한다(둘을 다 저장하면
21
+ * 어긋날 수 있다).
22
+ */
15
23
  export interface FlowItem {
16
24
  epc: string;
17
25
  location: string;
18
26
  disposition: string;
19
27
  gtin?: string;
20
28
  qty?: number;
29
+ /** 수량 단위(UN/CEFACT). 개수면 없다 — 없는 것을 'EA' 로 꾸미지 않는다. */
30
+ uom?: string;
31
+ /** 소속 물류단위(팔레트 SSCC 등) — 조립으로 맺어진다. 3D 적재 표현의 재료. */
32
+ parent?: string;
33
+ /** 이 물류단위를 싣고 있는 반복사용 자산(GRAI) — `parent` 와 다른 축. */
34
+ carriedBy?: string;
21
35
  expiry?: number;
36
+ /** 개체·로트 마스터데이터 원문 — 생겨날 때 정해지고 뒤 이벤트가 지우지 않는다. */
37
+ ilmd?: Record<string, unknown>;
22
38
  }
23
39
  /** 물리 자산 — 반복사용(팔레트·랙·용기). 설비도 물품도 아니다(GRAI vs SSCC 구분은 계약 주석 참조). */
24
40
  export interface FlowAsset {
@@ -73,10 +89,14 @@ export interface FlowTask {
73
89
  fromNode: string;
74
90
  toNode: string;
75
91
  resource: string | null;
92
+ /** 착수 시각(절대 sim-clock) — 보간 앵커(모션과 같은 규율). */
93
+ startedAtSimMs?: number;
76
94
  /** 이 작업에 투입된 사람들 — 설비와 별개 축(설비 1대 + 작업자 2명이 동시에 잡힌다). */
77
95
  personnel?: string[];
78
96
  /** 이 작업에 투입된 물리 자산(팔레트 등). */
79
97
  assets?: string[];
98
+ /** 이 작업에 투입된 설비 전부 — `resource` 는 그중 대표(첫 번째). */
99
+ resources?: string[];
80
100
  remainingMs: number;
81
101
  durationMs: number;
82
102
  orderId?: string;
@@ -178,6 +198,13 @@ export declare abstract class FlowEngine implements TwinKernel {
178
198
  * 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
179
199
  */
180
200
  protected operationSpecs: Map<string, OperationDef>;
201
+ /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
202
+ private observer?;
203
+ /** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
204
+ private observedDirty;
205
+ private observeMode;
206
+ /** 관측 구동이 투영기를 세울 때 필요한 원본 보드(구조는 이벤트가 아니라 마스터에서 온다). */
207
+ protected boardDef?: BoardDef;
181
208
  /** 명세 소비 기록 — 무엇을 선언값으로, 무엇을 기본값으로 계산했나(정직한 자기보고). */
182
209
  private specUse;
183
210
  protected epcSeq: number;
@@ -185,6 +212,8 @@ export declare abstract class FlowEngine implements TwinKernel {
185
212
  protected orderSeq: number;
186
213
  protected soSeq: number;
187
214
  private handlers;
215
+ /** 구독자 접근(관측 재방출) — emit 과 같은 목록을 쓴다(두 경로가 갈리지 않게). */
216
+ private handlersRef;
188
217
  private gens;
189
218
  private generating;
190
219
  private speed;
@@ -228,6 +257,7 @@ export declare abstract class FlowEngine implements TwinKernel {
228
257
  persons?: PersonState[];
229
258
  assets?: AssetState[];
230
259
  tasks?: TaskState[];
260
+ orders?: OrderState[];
231
261
  }, orders?: OrderStatusDelta[]): void;
232
262
  /** what-if 구성 변주 — 노드 용량 변경(fork 대상). 존재하면 true. */
233
263
  setNodeCapacity(nodeId: string, capacity: number): boolean;
@@ -260,6 +290,29 @@ export declare abstract class FlowEngine implements TwinKernel {
260
290
  fork(tenantId?: string): this;
261
291
  protected now(): string;
262
292
  protected randInt(min: number, max: number): number;
293
+ /**
294
+ * 관측 구동(P0 스파이크) — **이벤트로 커널을 굴린다.**
295
+ *
296
+ * 상태를 만드는 구동이 둘인데(시뮬 `tick` / 미러 `apply`) 지금은 **모델도 둘**이라 한쪽만 고치면
297
+ * 갈라진다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
298
+ * 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
299
+ *
300
+ * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
301
+ * (`hydrateObserved`)로 커널 상태에 심는다. 그래서 관측으로 굴린 커널을 그대로 `fork`·`tick` 할 수
302
+ * 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
303
+ *
304
+ * **비용은 정직하게**: 이벤트마다 전체를 다시 심으므로 O(상태 크기)다. P1 에서 반영 로직을 순수
305
+ * reduce 모듈로 추출해 투영기와 공유하면 사라진다. 지금은 계약이 성립하는지만 본다.
306
+ *
307
+ * `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
308
+ */
309
+ apply(envelope: CanonicalEnvelope): void;
310
+ /** 관측분을 커널 상태로 옮긴다 — 필요할 때 한 번만(같은 규칙, 같은 씨앗 경로). */
311
+ private settleObserved;
312
+ /** 구독자 목록 — 관측 재방출용(private handlers 에 접근). */
313
+ private observedHandlers;
314
+ /** 관측 구동으로 굴러가는 중인가 — 소비처가 "이 커널의 진실이 어디서 오나" 를 물을 수 있게. */
315
+ get observing(): boolean;
263
316
  /**
264
317
  * 오퍼레이션 명세를 싣는다 — 도메인 정의(ISA-95 OperationsSegment)의 시뮬 명세를 커널이 소비하는 입구.
265
318
  * 같은 key 를 다시 실으면 덮어쓴다(정의가 권위).
@@ -335,6 +388,30 @@ export declare abstract class FlowEngine implements TwinKernel {
335
388
  bizLocation?: string;
336
389
  bizTransactionList?: BizTransactionElement[];
337
390
  }): void;
391
+ /**
392
+ * 할당(예약) — **처분 변화를 이벤트로 낸다.**
393
+ *
394
+ * 예전에는 네 곳(WMS·YMS·MES 두 경로)이 각자 `disposition = reserved` 로 상태만 바꾸고 거래
395
+ * 이벤트(TransactionEvent)만 냈다. 거래 이벤트는 **처분을 싣지 않으므로** 미러는 그 물건이 잡혔다는
396
+ * 사실을 영영 알 수 없었다(적합성 하네스가 잡았다). 저널로 복원해도, 예측 씨앗에도 안 실린다.
397
+ *
398
+ * 관측 이벤트로 낸다 — 표준이 처분 변화를 표현하는 자리다(ObjectEvent OBSERVE + disposition).
399
+ * 물건이 여러 자리에 흩어져 있으면 **자리별로 나눠** 낸다(한 이벤트에 한 readPoint 가 맞다).
400
+ *
401
+ * `bizStep` 은 **호출부가 정한다.** 할당 자체를 가리키는 CBV 단계(reserving)를 1차 출처로 확인하지
402
+ * 못했으므로 어휘를 발명하지 않고, 그 할당이 속한 업무 단계를 그대로 쓴다.
403
+ */
404
+ protected reserve(epcs: string[], bizStep: string): void;
405
+ /**
406
+ * 처분 변화 관측 — **상태와 이벤트를 한 번에.** 둘을 따로 쓰면 반드시 갈라진다.
407
+ *
408
+ * 실제로 양쪽으로 갈라져 있었다: 할당은 상태만 바꾸고 이벤트를 안 냈고(미러가 모름), 야드 도크
409
+ * 도착은 이벤트만 내고 상태를 안 바꿨다(이벤트와 상태가 다른 말). 적합성 하네스가 둘 다 잡았다.
410
+ *
411
+ * 물건이 여러 자리에 있으면 자리별로 나눠 낸다(한 이벤트에 한 readPoint 가 맞다).
412
+ * 이미 그 처분이면 아무 일도 하지 않는다(같은 사실을 두 번 말하지 않는다).
413
+ */
414
+ protected observeDisposition(epcs: string[], disposition: string, bizStep: string, at?: string): void;
338
415
  /**
339
416
  * containment 조립(EPCIS AggregationEvent ADD) — 자식들을 부모(용기)로 집約.
340
417
  * consume 지정 시 자식이 컨테이너로 흡수되며 독립 아이템에서 이탈(dematerialize: ObjectEvent DELETE + 제거).
@@ -382,7 +459,10 @@ export declare abstract class FlowEngine implements TwinKernel {
382
459
  /** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
383
460
  protected emitAsset(a: FlowAsset): void;
384
461
  protected emitPerson(p: FlowPerson): void;
462
+ /** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
463
+ * 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
385
464
  protected emitMover(m: FlowMover, motion?: MoverMotion): void;
465
+ /** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
386
466
  protected emitOrder(o: FlowOrder): void;
387
467
  /**
388
468
  * 자극 간격 — **계약이 선언한 네 분포를 실제로 판정한다.**
@@ -431,6 +511,12 @@ export declare abstract class FlowEngine implements TwinKernel {
431
511
  * (반쯤 잡고 실패하면 사람이 아무 일도 못 하면서 묶인다).
432
512
  */
433
513
  private claimPersonnel;
514
+ /**
515
+ * 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
516
+ * 명세(`equipmentSpecification`)가 없으면 기존 거동: `resourceType` 한 대(없으면 아무 유휴 설비).
517
+ * 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
518
+ */
519
+ private claimEquipment;
434
520
  /** 확보한 사람을 작업에 묶는다(설비까지 확정된 뒤). */
435
521
  private assignCrew;
436
522
  /** 작업이 끝나면 사람을 놓아 준다 — 설비 해제와 별개 경로. */
@@ -460,9 +546,27 @@ export declare abstract class FlowEngine implements TwinKernel {
460
546
  * down 중 무버는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
461
547
  */
462
548
  private processFailures;
549
+ /**
550
+ * 물품 상태 산출 — **보유값 + 식별자에서 나오는 파생값.**
551
+ *
552
+ * 품번 키(`gtinKey`)·로트(`lot`)는 식별자의 순수 함수라 저장하지 않고 여기서 낸다. 투영기와 **같은
553
+ * 규칙**(`parseEpc`)을 쓴다 — 두 구동이 같은 식별자를 다르게 뜯으면 같은 사실이 다르게 보인다.
554
+ * 로트는 LGTIN 이면 식별자 안에 있고, 직렬 개체는 마스터데이터(`ilmd`)에 실려 온다.
555
+ */
556
+ protected itemState(i: FlowItem): ItemState;
463
557
  protected progressOf(t: FlowTask): number;
464
558
  private generate;
465
559
  private processOrders;
560
+ /**
561
+ * 작업 진행 — **진행을 먼저, 배정을 나중에.**
562
+ *
563
+ * 예전에는 배정을 먼저 하고 같은 tick 에서 곧바로 dt 만큼 깎았다. 그래서 이제 막 시작한 작업이
564
+ * 시작하자마자 한 스텝 진행된 것으로 계산됐고, **방출한 모션 앵커(startedAtSimMs)와 스냅샷이 한
565
+ * tick 어긋났다**(적합성 하네스가 잡았다). 스텝이 커질수록 오차도 커진다.
566
+ */
466
567
  private processTasks;
568
+ private assignTasks;
569
+ /** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
570
+ private advanceTasks;
467
571
  }
468
572
  export {};