@operato/twin-kernel 0.1.0 → 0.2.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.
@@ -1,22 +1,60 @@
1
- import type { Attention, BoardDef, Command, CommandAck, EventHandler, MoverMotion, OeeMetrics, GeneratorSpec, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe, NodeState, ItemState, MoverState, OrderStatusDelta } from './contract.ts';
1
+ import type { Attention, BoardDef, CanonicalEnvelope, Command, CommandAck, EventHandler, MoverMotion, OeeMetrics, AssetState, GeneratorSpec, 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';
5
+ import type { OperationDef } from './domain-definition.ts';
5
6
  export interface FlowNode {
6
7
  id: string;
7
8
  type: string;
8
9
  capacity: number;
10
+ parallelism?: number;
9
11
  occupancy: number;
10
12
  status: string;
11
13
  parentId?: string;
12
14
  }
15
+ /**
16
+ * 시뮬이 들고 있는 물품 — **계약(ItemState)을 축소하지 않는다.**
17
+ *
18
+ * 미러(투영기)는 로트·단위·소속·마스터데이터까지 들고 있는데 시뮬은 여섯 필드뿐이었다. 그래서 같은
19
+ * 사실을 두 구동이 다르게 말했다(파리티 테스트가 잡았다). 여기서 갖는 것은 **보유해야 하는 상태**다 —
20
+ * 품번 키·로트처럼 식별자에서 순수하게 나오는 값은 저장하지 않고 스냅샷에서 파생한다(둘을 다 저장하면
21
+ * 어긋날 수 있다).
22
+ */
13
23
  export interface FlowItem {
14
24
  epc: string;
15
25
  location: string;
16
26
  disposition: string;
17
27
  gtin?: string;
18
28
  qty?: number;
29
+ /** 수량 단위(UN/CEFACT). 개수면 없다 — 없는 것을 'EA' 로 꾸미지 않는다. */
30
+ uom?: string;
31
+ /** 소속 물류단위(팔레트 SSCC 등) — 조립으로 맺어진다. 3D 적재 표현의 재료. */
32
+ parent?: string;
33
+ /** 이 물류단위를 싣고 있는 반복사용 자산(GRAI) — `parent` 와 다른 축. */
34
+ carriedBy?: string;
19
35
  expiry?: number;
36
+ /** 개체·로트 마스터데이터 원문 — 생겨날 때 정해지고 뒤 이벤트가 지우지 않는다. */
37
+ ilmd?: Record<string, unknown>;
38
+ }
39
+ /** 물리 자산 — 반복사용(팔레트·랙·용기). 설비도 물품도 아니다(GRAI vs SSCC 구분은 계약 주석 참조). */
40
+ export interface FlowAsset {
41
+ id: string;
42
+ assetClass?: string;
43
+ location?: string;
44
+ status: string;
45
+ taskId: string | null;
46
+ carrying?: string;
47
+ }
48
+ /** 사람 — 설비와 별개 자원(고장·OEE 가 아니라 등급·교대로 산다). */
49
+ export interface FlowPerson {
50
+ id: string;
51
+ personnelClass?: string;
52
+ status: string;
53
+ taskId: string | null;
54
+ window?: {
55
+ startHour: number;
56
+ endHour: number;
57
+ };
20
58
  }
21
59
  export interface FlowMover {
22
60
  id: string;
@@ -24,6 +62,11 @@ export interface FlowMover {
24
62
  location: string;
25
63
  status: string;
26
64
  taskId: string | null;
65
+ /** 교대(가동시간) — 지정 시 이 시간대에만 배정된다. 미지정=24시간 가용. */
66
+ window?: {
67
+ startHour: number;
68
+ endHour: number;
69
+ };
27
70
  runMs: number;
28
71
  setupMs: number;
29
72
  downMs: number;
@@ -46,6 +89,12 @@ export interface FlowTask {
46
89
  fromNode: string;
47
90
  toNode: string;
48
91
  resource: string | null;
92
+ /** 착수 시각(절대 sim-clock) — 보간 앵커(모션과 같은 규율). */
93
+ startedAtSimMs?: number;
94
+ /** 이 작업에 투입된 사람들 — 설비와 별개 축(설비 1대 + 작업자 2명이 동시에 잡힌다). */
95
+ personnel?: string[];
96
+ /** 이 작업에 투입된 물리 자산(팔레트 등). */
97
+ assets?: string[];
49
98
  remainingMs: number;
50
99
  durationMs: number;
51
100
  orderId?: string;
@@ -130,19 +179,39 @@ export declare abstract class FlowEngine implements TwinKernel {
130
179
  nodes: Map<string, FlowNode>;
131
180
  items: Map<string, FlowItem>;
132
181
  movers: Map<string, FlowMover>;
182
+ /** 사람 — 선언하지 않으면 빈 맵(인원 제약 없는 트윈, 기존 거동). */
183
+ persons: Map<string, FlowPerson>;
184
+ /** 물리 자산 — 선언하지 않으면 빈 맵(자산 제약 없는 트윈, 기존 거동). */
185
+ assets: Map<string, FlowAsset>;
133
186
  tasks: Map<string, FlowTask>;
134
187
  orders: Map<string, FlowOrder>;
135
188
  revision: number;
136
189
  clockMs: number;
137
190
  protected rng: Rng;
138
191
  protected policy: AllocationPolicy;
139
- /** duration 시임(선택) — 미주입 시 도메인 상수. 씬/보드 바인딩이 거리·속도 기반 estimator 주입. */
192
+ /** duration 시임(선택) — 미주입 시 명세, 명세도 없으면 도메인 상수. 이력 보정 추정기가 여기 들어온다. */
140
193
  durationEstimator?: DurationEstimator;
194
+ /**
195
+ * 오퍼레이션 명세(선택) — 작업 종류(`FlowTask.kind` = `OperationDef.key`) → 소요·변동·모수.
196
+ * 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
197
+ */
198
+ protected operationSpecs: Map<string, OperationDef>;
199
+ /** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
200
+ private observer?;
201
+ /** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
202
+ private observedDirty;
203
+ private observeMode;
204
+ /** 관측 구동이 투영기를 세울 때 필요한 원본 보드(구조는 이벤트가 아니라 마스터에서 온다). */
205
+ protected boardDef?: BoardDef;
206
+ /** 명세 소비 기록 — 무엇을 선언값으로, 무엇을 기본값으로 계산했나(정직한 자기보고). */
207
+ private specUse;
141
208
  protected epcSeq: number;
142
209
  protected taskSeq: number;
143
210
  protected orderSeq: number;
144
211
  protected soSeq: number;
145
212
  private handlers;
213
+ /** 구독자 접근(관측 재방출) — emit 과 같은 목록을 쓴다(두 경로가 갈리지 않게). */
214
+ private handlersRef;
146
215
  private gens;
147
216
  private generating;
148
217
  private speed;
@@ -183,6 +252,9 @@ export declare abstract class FlowEngine implements TwinKernel {
183
252
  nodes: NodeState[];
184
253
  items: ItemState[];
185
254
  movers: MoverState[];
255
+ persons?: PersonState[];
256
+ assets?: AssetState[];
257
+ tasks?: TaskState[];
186
258
  }, orders?: OrderStatusDelta[]): void;
187
259
  /** what-if 구성 변주 — 노드 용량 변경(fork 대상). 존재하면 true. */
188
260
  setNodeCapacity(nodeId: string, capacity: number): boolean;
@@ -215,8 +287,77 @@ export declare abstract class FlowEngine implements TwinKernel {
215
287
  fork(tenantId?: string): this;
216
288
  protected now(): string;
217
289
  protected randInt(min: number, max: number): number;
218
- /** task 소요 산출 — estimator 주입 시 그 값, 미주입/undefined 시 도메인 상수(fallback). "얼마"만 소비, "경로"는 씬. */
290
+ /**
291
+ * 관측 구동(P0 스파이크) — **이벤트로 커널을 굴린다.**
292
+ *
293
+ * 상태를 만드는 구동이 둘인데(시뮬 `tick` / 미러 `apply`) 지금은 **모델도 둘**이라 한쪽만 고치면
294
+ * 갈라진다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
295
+ * 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
296
+ *
297
+ * 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
298
+ * (`hydrateObserved`)로 커널 상태에 심는다. 그래서 관측으로 굴린 커널을 그대로 `fork`·`tick` 할 수
299
+ * 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
300
+ *
301
+ * **비용은 정직하게**: 이벤트마다 전체를 다시 심으므로 O(상태 크기)다. P1 에서 반영 로직을 순수
302
+ * reduce 모듈로 추출해 투영기와 공유하면 사라진다. 지금은 계약이 성립하는지만 본다.
303
+ *
304
+ * `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
305
+ */
306
+ apply(envelope: CanonicalEnvelope): void;
307
+ /** 관측분을 커널 상태로 옮긴다 — 필요할 때 한 번만(같은 규칙, 같은 씨앗 경로). */
308
+ private settleObserved;
309
+ /** 구독자 목록 — 관측 재방출용(private handlers 에 접근). */
310
+ private observedHandlers;
311
+ /** 관측 구동으로 굴러가는 중인가 — 소비처가 "이 커널의 진실이 어디서 오나" 를 물을 수 있게. */
312
+ get observing(): boolean;
313
+ /**
314
+ * 오퍼레이션 명세를 싣는다 — 도메인 정의(ISA-95 OperationsSegment)의 시뮬 명세를 커널이 소비하는 입구.
315
+ * 같은 key 를 다시 실으면 덮어쓴다(정의가 권위).
316
+ */
317
+ loadOperations(ops?: OperationDef[]): void;
318
+ /**
319
+ * task 소요 산출 — 우선순위: **① 추정기(이력 보정) → ② 명세(ISA-95 Duration + 변동) → ③ 도메인 상수.**
320
+ *
321
+ * 이 순서인 이유: 실측에서 배운 값이 선언값을 이기고, 선언값이 우리가 코드에 박아 둔 상수를 이긴다.
322
+ * 셋 중 무엇을 썼는지는 `specCoverage()` 로 드러낸다 — 상수를 쓴 것이 조용히 넘어가지 않게.
323
+ * "얼마"만 소비하고 "경로"는 씬이 소유한다(좌표-free 유지).
324
+ */
219
325
  protected durationOf(ctx: DurationContext, fallbackMs: number): number;
326
+ /**
327
+ * 소요시간 변동 표본 — **엔진 난수원으로만** 뽑는다(fork 결정성). 표준 밖 확장이므로 미지정이면 상수.
328
+ * 모수가 모자라면(uniform 에 min/max 없음 등) 변동을 발명하지 않고 평균을 그대로 쓴다.
329
+ */
330
+ private applyVariability;
331
+ /**
332
+ * 퍼짐 표본 — **엔진 난수원으로만** 뽑는다(fork 결정성). 선언 명세(ISO 표기)와 실측 분포(ms)가
333
+ * 같은 수식을 쓴다: 한쪽만 고치면 두 경로가 다른 답을 낸다.
334
+ * 모수가 모자라거나 뒤집혀 있으면 **퍼짐을 발명하지 않고** 평균을 그대로 쓴다.
335
+ */
336
+ private sampleSpread;
337
+ /** 명세 모수(숫자) — 선언 없으면 undefined(0 으로 꾸미지 않는다). 소비처가 기본값을 정한다. */
338
+ protected paramNumber(opKey: string, id: string): number | undefined;
339
+ /** 명세 모수(기간) — ISO 8601 문자열을 밀리초로. 선언 없으면 undefined. */
340
+ protected paramDuration(opKey: string, id: string): number | undefined;
341
+ private noteSpecUse;
342
+ private noteParamUse;
343
+ /**
344
+ * 시뮬 명세 자기보고 — **어디까지 데이터로 말했고 어디부터 우리가 박아 둔 상수인가.**
345
+ *
346
+ * 시뮬레이션 결과를 받는 쪽이 이걸 봐야 한다: 소요시간이 전부 기본값이면 그 예측으로 말할 수 있는 것은
347
+ * "같은 조건에서의 상대 비교" 뿐이고 "몇 시에 끝난다" 는 근거가 없다. 그 구분을 숫자로 드러낸다.
348
+ */
349
+ specCoverage(): {
350
+ operations: {
351
+ kind: string;
352
+ duration: 'measured' | 'declared' | 'default';
353
+ variability?: string;
354
+ parameters: string[];
355
+ }[];
356
+ /** 이력·계산에서 온 소요(추정기) — 선언값보다 강한 근거. */
357
+ measuredDurations: number;
358
+ declaredDurations: number;
359
+ defaultDurations: number;
360
+ };
220
361
  /** skuMix 에서 weight 로 gtin 선택(rng) — 도착/오더 자극의 품목 결정. */
221
362
  protected pickGtin(mix: {
222
363
  gtin: string;
@@ -244,6 +385,30 @@ export declare abstract class FlowEngine implements TwinKernel {
244
385
  bizLocation?: string;
245
386
  bizTransactionList?: BizTransactionElement[];
246
387
  }): void;
388
+ /**
389
+ * 할당(예약) — **처분 변화를 이벤트로 낸다.**
390
+ *
391
+ * 예전에는 네 곳(WMS·YMS·MES 두 경로)이 각자 `disposition = reserved` 로 상태만 바꾸고 거래
392
+ * 이벤트(TransactionEvent)만 냈다. 거래 이벤트는 **처분을 싣지 않으므로** 미러는 그 물건이 잡혔다는
393
+ * 사실을 영영 알 수 없었다(적합성 하네스가 잡았다). 저널로 복원해도, 예측 씨앗에도 안 실린다.
394
+ *
395
+ * 관측 이벤트로 낸다 — 표준이 처분 변화를 표현하는 자리다(ObjectEvent OBSERVE + disposition).
396
+ * 물건이 여러 자리에 흩어져 있으면 **자리별로 나눠** 낸다(한 이벤트에 한 readPoint 가 맞다).
397
+ *
398
+ * `bizStep` 은 **호출부가 정한다.** 할당 자체를 가리키는 CBV 단계(reserving)를 1차 출처로 확인하지
399
+ * 못했으므로 어휘를 발명하지 않고, 그 할당이 속한 업무 단계를 그대로 쓴다.
400
+ */
401
+ protected reserve(epcs: string[], bizStep: string): void;
402
+ /**
403
+ * 처분 변화 관측 — **상태와 이벤트를 한 번에.** 둘을 따로 쓰면 반드시 갈라진다.
404
+ *
405
+ * 실제로 양쪽으로 갈라져 있었다: 할당은 상태만 바꾸고 이벤트를 안 냈고(미러가 모름), 야드 도크
406
+ * 도착은 이벤트만 내고 상태를 안 바꿨다(이벤트와 상태가 다른 말). 적합성 하네스가 둘 다 잡았다.
407
+ *
408
+ * 물건이 여러 자리에 있으면 자리별로 나눠 낸다(한 이벤트에 한 readPoint 가 맞다).
409
+ * 이미 그 처분이면 아무 일도 하지 않는다(같은 사실을 두 번 말하지 않는다).
410
+ */
411
+ protected observeDisposition(epcs: string[], disposition: string, bizStep: string, at?: string): void;
247
412
  /**
248
413
  * containment 조립(EPCIS AggregationEvent ADD) — 자식들을 부모(용기)로 집約.
249
414
  * consume 지정 시 자식이 컨테이너로 흡수되며 독립 아이템에서 이탈(dematerialize: ObjectEvent DELETE + 제거).
@@ -280,10 +445,89 @@ export declare abstract class FlowEngine implements TwinKernel {
280
445
  protected slotViews(nodeType: string): SlotView[];
281
446
  protected emit(event: EpcisEvent): void;
282
447
  protected emitOp(eventType: string, data: unknown): void;
448
+ /**
449
+ * 작업 전이 방출 — **커널이 아는 것을 미러도 알게** 한다.
450
+ *
451
+ * 예전에는 진척·남은 시간·의도를 싣지 않아, 미러 상태를 씨앗으로 한 예측이 "진행 중인 일이 없는
452
+ * 현장" 에서 출발했고, 소비처는 무자원 체류를 기록 누락으로 오해할 수밖에 없었다.
453
+ * 진척은 진행 중일 때만 뜻이 있으므로 그때만 싣는다(생성·완료 시점의 0/1 은 노이즈).
454
+ */
283
455
  protected emitTask(t: FlowTask): void;
456
+ /** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
457
+ protected emitAsset(a: FlowAsset): void;
458
+ protected emitPerson(p: FlowPerson): void;
459
+ /** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
460
+ * 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
284
461
  protected emitMover(m: FlowMover, motion?: MoverMotion): void;
285
462
  protected emitOrder(o: FlowOrder): void;
463
+ /**
464
+ * 자극 간격 — **계약이 선언한 네 분포를 실제로 판정한다.**
465
+ *
466
+ * 예전에는 `poisson` 만 구현하고 나머지는 전부 상수로 떨어졌다. 계약이 `uniform`·`profile` 을
467
+ * 선언하고 있었으므로, 그것을 지정한 사람은 자기가 요청한 분포로 도는 줄 알았다 — **조용한 거짓**이다.
468
+ *
469
+ * constant 간격이 일정(평균 그대로)
470
+ * poisson 무기억 도착(지수 간격) — 평균 유지
471
+ * uniform 0..2×평균 균등 — 평균을 유지하면서 흔들린다(교과서적 U(0,2μ))
472
+ * profile 시간대별 배율(`profile[시]`)로 도착률을 조절 — 하루 안의 수요 곡선
473
+ */
286
474
  private intervalMs;
475
+ /**
476
+ * 시간대 배율 — `profile[시]`. `profile` 분포일 때만 적용하며, 배열이 짧으면 **순환**한다
477
+ * (24개면 하루, 8개면 8시간 주기). 미지정·다른 분포면 1(무영향).
478
+ * 시(hour)는 **시뮬 시각 자신의 프레임**(BASE_EPOCH 기준 UTC)이다 — 계약에 표준시가 없으므로
479
+ * 현지 시간대 해석은 아직 하지 않는다(꾸미지 않는다).
480
+ */
481
+ /**
482
+ * 다음 발화 시각 — **발화가 없는 시간대를 영원한 침묵으로 만들지 않는다.**
483
+ *
484
+ * 배율 0(그 시간대 도착 없음)이면 간격이 무한이 된다. 그것을 그대로 예약하면 이후 어떤 시간대가
485
+ * 와도 깨어나지 않는다 — 그래서 **다음 정시로 미뤄 다시 판정**한다(시간대가 바뀌면 배율도 바뀐다).
486
+ * 시나리오 시작과 구동 루프가 같은 규칙을 쓰도록 한 곳에 둔다(예전에는 시작 경로만 따로였다).
487
+ */
488
+ private nextFireMs;
489
+ private profileFactor;
490
+ /**
491
+ * 필요 물리 자산을 확보한다 — 인원과 **같은 규칙**(등급으로 요구, 부분 투입 없음, 확정은 나중).
492
+ * 자산은 사람과 달리 교대가 없고 **자리**가 있다(빈 팔레트가 어디 있는지가 다음 문제이지만,
493
+ * 지금은 자리를 따지지 않는다 — 따지려면 자산 이송 작업이 먼저 있어야 한다).
494
+ */
495
+ private claimAssets;
496
+ /** 확보한 자산을 작업에 묶는다 — 싣는 물류단위(SSCC)가 있으면 연결한다(GRAI ↔ SSCC). */
497
+ private assignAssets;
498
+ /**
499
+ * 작업이 끝나면 자산을 놓아 준다 — **사람과 다른 점: 자산은 도착 자리에 남는다**(물건이므로).
500
+ * 싣고 있던 것은 놓는다(빈 팔레트로 돌아간다 — 회수·재사용의 출발점).
501
+ */
502
+ private releaseAssets;
503
+ /**
504
+ * 필요 인원을 확보한다 — **등급으로 요구하고 등급으로 고른다**(특정인 지목이 아니다).
505
+ * 요구가 없으면 빈 배열, 모자라면 `null`(작업은 기다린다 — **부분 투입으로 시작하지 않는다**).
506
+ * 여기서는 고르기만 하고 잡지 않는다: 설비까지 확보된 뒤 `assignCrew` 가 확정한다
507
+ * (반쯤 잡고 실패하면 사람이 아무 일도 못 하면서 묶인다).
508
+ */
509
+ private claimPersonnel;
510
+ /** 확보한 사람을 작업에 묶는다(설비까지 확정된 뒤). */
511
+ private assignCrew;
512
+ /** 작업이 끝나면 사람을 놓아 준다 — 설비 해제와 별개 경로. */
513
+ private releaseCrew;
514
+ /** 사람의 교대 판정 — 자원(offShift)과 같은 규칙. */
515
+ protected personOffShift(p: FlowPerson): boolean;
516
+ /**
517
+ * 이 자리가 동시 처리 한도에 찼는가 — `parallelism` 을 선언한 자리만 판정한다(미선언=제약 없음).
518
+ * 세는 대상은 **그 자리에서 진행 중인 작업**(`toNode` 기준, in-progress). 대기 중인 작업은 세지 않는다.
519
+ */
520
+ private stationFull;
521
+ /** 교대 밖인가 — 자원이 지금 일하지 않는 이유 중 고장·계획정지와 구별되는 세 번째. */
522
+ protected offShift(m: FlowMover): boolean;
523
+ /** 시뮬 시각의 시(0..23) — 운영시간·시간대 배율의 기준. */
524
+ protected hourOfDay(): number;
525
+ /**
526
+ * 운영시간 안인가 — `window {startHour, endHour}`. **선언만 되고 소비처가 없던 필드**를 판정한다.
527
+ * `startHour <= endHour` 면 같은 날 구간, 넘어가면 자정을 가로지르는 구간(야간 교대: 22→6).
528
+ * 미지정이면 언제나 참(24시간 가동).
529
+ */
530
+ private inWindow;
287
531
  /** 지수분포 표본(고장/수리 간격) — mean 을 평균으로 하는 무기억 프로세스. */
288
532
  private sampleExp;
289
533
  /**
@@ -292,9 +536,27 @@ export declare abstract class FlowEngine implements TwinKernel {
292
536
  * down 중 무버는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
293
537
  */
294
538
  private processFailures;
539
+ /**
540
+ * 물품 상태 산출 — **보유값 + 식별자에서 나오는 파생값.**
541
+ *
542
+ * 품번 키(`gtinKey`)·로트(`lot`)는 식별자의 순수 함수라 저장하지 않고 여기서 낸다. 투영기와 **같은
543
+ * 규칙**(`parseEpc`)을 쓴다 — 두 구동이 같은 식별자를 다르게 뜯으면 같은 사실이 다르게 보인다.
544
+ * 로트는 LGTIN 이면 식별자 안에 있고, 직렬 개체는 마스터데이터(`ilmd`)에 실려 온다.
545
+ */
546
+ protected itemState(i: FlowItem): ItemState;
295
547
  protected progressOf(t: FlowTask): number;
296
548
  private generate;
297
549
  private processOrders;
550
+ /**
551
+ * 작업 진행 — **진행을 먼저, 배정을 나중에.**
552
+ *
553
+ * 예전에는 배정을 먼저 하고 같은 tick 에서 곧바로 dt 만큼 깎았다. 그래서 이제 막 시작한 작업이
554
+ * 시작하자마자 한 스텝 진행된 것으로 계산됐고, **방출한 모션 앵커(startedAtSimMs)와 스냅샷이 한
555
+ * tick 어긋났다**(적합성 하네스가 잡았다). 스텝이 커질수록 오차도 커진다.
556
+ */
298
557
  private processTasks;
558
+ private assignTasks;
559
+ /** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
560
+ private advanceTasks;
299
561
  }
300
562
  export {};