@operato/twin-kernel 0.4.1 → 0.4.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.
- package/dist/capability.js +10 -5
- package/dist/contract.d.ts +78 -0
- package/dist/contract.js +14 -1
- package/dist/flow-engine.d.ts +9 -0
- package/dist/flow-engine.js +33 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel.d.ts +35 -0
- package/dist/kernel.js +148 -1
- package/dist/make-to-order.d.ts +58 -0
- package/dist/make-to-order.js +118 -0
- package/dist/mes-kernel.d.ts +3 -16
- package/dist/mes-kernel.js +40 -21
- package/dist/observed-reducer.d.ts +10 -0
- package/dist/observed-reducer.js +11 -1
- package/dist/scenario-validate.d.ts +15 -0
- package/dist/scenario-validate.js +72 -0
- package/dist/wms-profile.d.ts +12 -1
- package/dist/wms-profile.js +17 -3
- package/dist-cjs/index.cjs +292 -37
- package/package.json +1 -1
package/dist/capability.js
CHANGED
|
@@ -11,25 +11,30 @@
|
|
|
11
11
|
* 이 의도를 *실현*한다 — 공유 계약은 관측(계층 B)뿐, 기제 methods(계층 A)는 공유 안 함.
|
|
12
12
|
* ⚠ Mobile ≠ Transferable: Mobile=자원 자신이 이동, Transferable=자리가 아이템 이동(씬 기제).
|
|
13
13
|
*/
|
|
14
|
+
/*
|
|
15
|
+
* `label` 은 **언어 중립 i18n 키**(`twin.capability.<key>`) — 사람 언어는 표현계층이 렌더한다(L2).
|
|
16
|
+
* 타입/시스템 라벨(domain-catalog)이 먼저 이 규약으로 옮겨졌고, 능력 라벨만 사람 말로 남아
|
|
17
|
+
* 영어 화면에 한글이 새고 있었다. `semantics` 는 설계 문서용 산문이므로 대상이 아니다.
|
|
18
|
+
*/
|
|
14
19
|
export const CAPABILITIES = {
|
|
15
20
|
operable: {
|
|
16
|
-
key: 'operable', label: '
|
|
21
|
+
key: 'operable', label: 'twin.capability.operable', semantics: '능동 자원의 운영 상태(유휴/가동/고장). status 교차 관심사를 여기 하나로.',
|
|
17
22
|
stateFields: ['status'], results: ['statusChanged']
|
|
18
23
|
},
|
|
19
24
|
storable: {
|
|
20
|
-
key: 'storable', label: '
|
|
25
|
+
key: 'storable', label: 'twin.capability.storable', semantics: '아이템을 보유하는 위치 — 점유/용량. (씬 기제: Capacity)',
|
|
21
26
|
stateFields: ['occupancy', 'capacity'], invariants: ['0 <= occupancy <= capacity (capacity>0)'], results: ['occupancyChanged']
|
|
22
27
|
},
|
|
23
28
|
mobile: {
|
|
24
|
-
key: 'mobile', label: '
|
|
29
|
+
key: 'mobile', label: 'twin.capability.mobile', semantics: '자원 자신이 자리 간 이동. Transferable(아이템 이동)과 다름. (씬 기제: CarrierLine)',
|
|
25
30
|
stateFields: ['location', 'motion'], models: ['Motion'], results: ['moved', 'motionTick']
|
|
26
31
|
},
|
|
27
32
|
processable: {
|
|
28
|
-
key: 'processable', label: '
|
|
33
|
+
key: 'processable', label: 'twin.capability.processable', semantics: '변환/가공 수행 — 산출(양품/불량). 운영 status 는 Operable 조합. progress 방출은 후속.',
|
|
29
34
|
stateFields: ['output'], results: ['completed']
|
|
30
35
|
},
|
|
31
36
|
trackable: {
|
|
32
|
-
key: 'trackable', label: '
|
|
37
|
+
key: 'trackable', label: 'twin.capability.trackable', semantics: '오더/아이템 생애 추적 — 생애단계(도메인 라벨, 무방언)·진행·보류.',
|
|
33
38
|
stateFields: ['lifecycle', 'progress', 'held'], results: ['lifecycleChanged']
|
|
34
39
|
}
|
|
35
40
|
};
|
package/dist/contract.d.ts
CHANGED
|
@@ -971,6 +971,13 @@ export interface StateSnapshot {
|
|
|
971
971
|
tasks: TaskState[];
|
|
972
972
|
orders: OrderState[];
|
|
973
973
|
attentions?: Attention[];
|
|
974
|
+
/**
|
|
975
|
+
* 확인(ack)해 둔 주목 신호 id — **상태에서 파생되지 않는 유일한 축.**
|
|
976
|
+
*
|
|
977
|
+
* 신호 자체는 상태에서 다시 계산되지만 "사람이 봤다" 는 계산으로 되살릴 수 없다. 스냅샷으로
|
|
978
|
+
* 왕복시켜야 재기동·되짚기에서 확인 상태가 유지된다.
|
|
979
|
+
*/
|
|
980
|
+
acked?: string[];
|
|
974
981
|
}
|
|
975
982
|
export interface Command<T = unknown> {
|
|
976
983
|
commandId: string;
|
|
@@ -1084,7 +1091,25 @@ export declare const OP_EVENT: {
|
|
|
1084
1091
|
readonly asset: "asset.status";
|
|
1085
1092
|
readonly order: "order.status";
|
|
1086
1093
|
readonly quality: "quality.output";
|
|
1094
|
+
/**
|
|
1095
|
+
* 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
|
|
1096
|
+
*
|
|
1097
|
+
* 다른 파생 상태는 상태에서 다시 계산된다(주목 신호 자체가 그렇다). 그런데 "누가 이것을 봤다" 는
|
|
1098
|
+
* 계산으로 되살릴 수 없다. 저널에 남기지 않으면 재기동하면 확인해 둔 신호가 다시 빨개지고,
|
|
1099
|
+
* 과거를 되짚어도 그때 무엇을 확인했는지 알 수 없다 — 저널이 현실을 불완전하게 담는 자리였다.
|
|
1100
|
+
*/
|
|
1101
|
+
readonly attentionAck: "attention.acked";
|
|
1087
1102
|
};
|
|
1103
|
+
/**
|
|
1104
|
+
* 주목 신호 확인 델타 — 확인한 신호의 id 와 시각.
|
|
1105
|
+
*
|
|
1106
|
+
* 신호의 내용은 싣지 않는다(상태에서 다시 계산된다). 여기 남기는 것은 **사람이 확인했다는 사실** 하나다.
|
|
1107
|
+
*/
|
|
1108
|
+
export interface AttentionAckDelta {
|
|
1109
|
+
id: string;
|
|
1110
|
+
/** 확인한 시각 — 없으면 이벤트 시각을 쓴다. */
|
|
1111
|
+
at?: ISOTime;
|
|
1112
|
+
}
|
|
1088
1113
|
/** 사람 상태 델타 — 배정·해제·교대 전이 시 방출. 미러가 인원 가용을 비추는 근거. */
|
|
1089
1114
|
export interface PersonStatusDelta extends EffectivePeriod {
|
|
1090
1115
|
personId: string;
|
|
@@ -1328,6 +1353,59 @@ export interface BoardDef {
|
|
|
1328
1353
|
personnelClasses?: ResourceClassDef[];
|
|
1329
1354
|
equipmentClasses?: ResourceClassDef[];
|
|
1330
1355
|
assetClasses?: ResourceClassDef[];
|
|
1356
|
+
/**
|
|
1357
|
+
* **생산 선언** — 이 트윈이 무엇을 어떻게 만드는가(ISA-95 `OperationsSegment` + BOM).
|
|
1358
|
+
*
|
|
1359
|
+
* ── 이름을 `productionSpec` 으로 정한 이유 (2026-08-05, 되돌리지 말 것) ─────────────────────────
|
|
1360
|
+
* 이 자리는 예전에 **`mesSpec`** 이라는 이름으로, 그것도 **계약에 선언되지 않은 채**
|
|
1361
|
+
* (`(board as any).mesSpec`) 타고 있었다.
|
|
1362
|
+
*
|
|
1363
|
+
* 그 이름의 유래는 자재가 아니라 **소비자**다: `MesKernel` 의 생성자 인자 이름이
|
|
1364
|
+
* `mesSpec: MesDefinitionSpec` 이고(정의-구동 모드를 MES 에만 도입한 커밋 `ce4fbeb`), 호스트가
|
|
1365
|
+
* 보드에 얹을 때 그 인자 이름을 그대로 가져왔다. 소비자가 하나일 때는 어색하지 않았다.
|
|
1366
|
+
*
|
|
1367
|
+
* `materialSpec` 도 답이 아니다. ① 담긴 것이 자재가 아니다 — 타입·오퍼레이션(소요·변동·모수·
|
|
1368
|
+
* 인원/설비/자산/자재 명세)·라우트·레시피이고 자재는 그중 한 항목의 한 필드다. ② 그 이름은 이미
|
|
1369
|
+
* 표준 자리로 쓰인다 — `OperationDef.materialSpecification` 이 ISA-95 `OpMaterialSpecificationType`
|
|
1370
|
+
* 1:1 이다. 보드 수준에서 같은 이름을 쓰면 "어느 쪽 자재 명세냐" 가 매번 헷갈린다.
|
|
1371
|
+
*
|
|
1372
|
+
* `productionSpec` = ISA-95 의 생산 영역 어휘이고, 담긴 것("이 트윈이 무엇을 어떻게 만드는가")
|
|
1373
|
+
* 그대로다. 특정 시스템(MES/WMS)에도, 특정 자원(자재)에도 기울지 않는다.
|
|
1374
|
+
*
|
|
1375
|
+
* 이름이 MES 였던 결과로 두 가지가 굳었다.
|
|
1376
|
+
*
|
|
1377
|
+
* ① **일반 기제에 한 시스템 이름이 붙었다.** 담고 있는 것은 ISA-95 `OperationsSegment`(소요·모수·
|
|
1378
|
+
* 자재 명세)와 BOM 이고, 그것은 MES 만의 것이 아니다 — 창고의 유통가공(키팅·세트조립)도 같은
|
|
1379
|
+
* "자재를 소비해 자재를 산출하는 공정" 이다. 이름이 MES 였기 때문에 **WMS 트윈은 이 선언을 실을
|
|
1380
|
+
* 생각조차 하지 못했고**, 유통가공 창고 템플릿은 BOM 을 설명 문서(`detail`)에만 적어 둔 채
|
|
1381
|
+
* 키트를 만들지 못하는 트윈을 만들어 냈다(랙이 꽉 차고 출고가 0건이었다).
|
|
1382
|
+
* ② 계약에 없으니 **아무도 이 자리를 발견할 수 없었다.** 타입이 말해 주지 않는 필드는 없는 필드다.
|
|
1383
|
+
*
|
|
1384
|
+
* 그래서 표준 어휘를 따르는 일반 이름으로 정식 선언한다. `mesSpec` 은 기존 트윈이 그대로 돌도록
|
|
1385
|
+
* **읽기 호환으로만** 남긴다(새로 쓰지 않는다).
|
|
1386
|
+
*
|
|
1387
|
+
* ── 시스템별 생산의 경계 (여기서도 되돌리지 말 것) ────────────────────────────────────────────
|
|
1388
|
+
* MES 의 생산은 **직렬번호·수율·생산오더 연결**을 갖는 레시피 경로다. 코어의 일반 자재 명세
|
|
1389
|
+
* (`use: 'produced'`)는 **비직렬 클래스+수량**이라 그 셋을 표현하지 못한다 — 그래서 둘을 합치지
|
|
1390
|
+
* 않는다. 겹치면 같은 산출이 두 번 생기므로 MES 커널이 기동 시점에 거부한다(`assertNoDoubleProduction`).
|
|
1391
|
+
* 유통가공(VAS)은 비직렬 클래스+수량이 맞는 모양이므로 코어의 일반 경로를 쓴다.
|
|
1392
|
+
*/
|
|
1393
|
+
productionSpec?: ProductionSpec;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* 생산 선언 — 도메인 정의(ISA-95 operations + BOM)와, 그 추상 자재 키를 실제 GS1 식별자로 잇는 바인딩.
|
|
1397
|
+
*
|
|
1398
|
+
* 정의는 **gtin 을 모른다**(어느 회사의 물건인지는 현장의 사실이다). 그래서 키→item reference 바인딩과
|
|
1399
|
+
* 회사 프리픽스를 여기서 받아 `urn:epc:idpat:sgtin:<prefix>.<ref>.*` 를 만든다.
|
|
1400
|
+
*/
|
|
1401
|
+
export interface ProductionSpec {
|
|
1402
|
+
/** 무엇이 있고 어떻게 흐르나 — `DomainDefinition`(자기 타입, zero-dep). */
|
|
1403
|
+
definition: import('./domain-definition.ts').DomainDefinition;
|
|
1404
|
+
/** 자재 키 → GS1 item reference. */
|
|
1405
|
+
binding?: Record<string, string>;
|
|
1406
|
+
companyPrefix?: string;
|
|
1407
|
+
/** 쓸 레시피 키(미지정 시 첫 레시피) — 직렬 생산 경로(MES)만 쓴다. */
|
|
1408
|
+
recipeKey?: string;
|
|
1331
1409
|
}
|
|
1332
1410
|
export interface TwinKernel {
|
|
1333
1411
|
loadBoard(def: BoardDef): void;
|
package/dist/contract.js
CHANGED
|
@@ -497,7 +497,20 @@ export const OP_EVENT = {
|
|
|
497
497
|
/** 물리 자산 상태 전이 — 어디 있나·무엇을 싣고 있나(빈 팔레트인가). */
|
|
498
498
|
asset: 'asset.status',
|
|
499
499
|
order: 'order.status',
|
|
500
|
-
quality: 'quality.output' // 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
|
|
500
|
+
quality: 'quality.output', // 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
|
|
501
|
+
/**
|
|
502
|
+
* 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
|
|
503
|
+
*
|
|
504
|
+
* 다른 파생 상태는 상태에서 다시 계산된다(주목 신호 자체가 그렇다). 그런데 "누가 이것을 봤다" 는
|
|
505
|
+
* 계산으로 되살릴 수 없다. 저널에 남기지 않으면 재기동하면 확인해 둔 신호가 다시 빨개지고,
|
|
506
|
+
* 과거를 되짚어도 그때 무엇을 확인했는지 알 수 없다 — 저널이 현실을 불완전하게 담는 자리였다.
|
|
507
|
+
*/
|
|
508
|
+
/*
|
|
509
|
+
* 값이 커맨드(`CMD.attentionAck='attention.ack'`)와 겹치지 않게 **과거형**으로 둔다 — 커맨드는
|
|
510
|
+
* "확인해라"(요청)이고 이벤트는 "확인했다"(사실)다. 같은 문자열을 쓰면 저널에서 요청과 사실이
|
|
511
|
+
* 구별되지 않는다.
|
|
512
|
+
*/
|
|
513
|
+
attentionAck: 'attention.acked'
|
|
501
514
|
};
|
|
502
515
|
// ── Command 채널 어휘 — 트윈의 "행위(act)" 면 (prescriptive/트랜잭션 프론트엔드) ──
|
|
503
516
|
// 코어 공통: order.hold/resume(할당 보류). 도메인: order.release(즉시 투입) 등은 handleCommand 로.
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -348,6 +348,8 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
348
348
|
assets?: AssetState[];
|
|
349
349
|
tasks?: TaskState[];
|
|
350
350
|
orders?: OrderState[];
|
|
351
|
+
/** 확인해 둔 주목 신호 id — 계산으로 되살릴 수 없는 유일한 축이라 스냅샷에서 이어받는다. */
|
|
352
|
+
acked?: string[];
|
|
351
353
|
}, orders?: OrderStatusDelta[]): void;
|
|
352
354
|
/** what-if 구성 변주 — 자리 용량 변경(fork 대상). 존재하면 true. */
|
|
353
355
|
setLocationCapacity(locationId: string, capacity: number): boolean;
|
|
@@ -448,6 +450,13 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
448
450
|
* 오퍼레이션 명세를 싣는다 — 도메인 정의(ISA-95 OperationsSegment)의 시뮬 명세를 커널이 소비하는 입구.
|
|
449
451
|
* 같은 key 를 다시 실으면 덮어쓴다(정의가 권위).
|
|
450
452
|
*/
|
|
453
|
+
/**
|
|
454
|
+
* 선언된 오퍼레이션들 — 도메인 커널이 "무엇을 만들 수 있나" 를 물을 수 있게.
|
|
455
|
+
*
|
|
456
|
+
* `operationSpecs` 를 도메인이 직접 뒤지지 않게 읽기 창구를 둔다: 저장 형태(맵)가 바뀌어도
|
|
457
|
+
* 도메인은 몰라야 하고, 도메인이 그 맵에 쓰는 일이 생기면 정의가 권위라는 규약이 깨진다.
|
|
458
|
+
*/
|
|
459
|
+
protected declaredOperations(): readonly OperationDef[];
|
|
451
460
|
loadOperations(ops?: OperationDef[]): void;
|
|
452
461
|
/**
|
|
453
462
|
* 라우트(공정 순서) — 수율을 거슬러 올릴 때 필요하다. 기본은 모른다(선언 순서를 쓴다).
|
package/dist/flow-engine.js
CHANGED
|
@@ -255,6 +255,15 @@ export class FlowEngine {
|
|
|
255
255
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
256
256
|
loadBoard(def) {
|
|
257
257
|
this.boardDef = def;
|
|
258
|
+
/*
|
|
259
|
+
* **생산 선언은 어느 커널이든 싣는다.** 예전에는 MES 커널만 생성자에서 이것을 실었고(그 시절
|
|
260
|
+
* 이름은 `mesSpec` 이었다), 그래서 창고 트윈은 "자재를 소비해 자재를 산출하는 공정" 을 선언할
|
|
261
|
+
* 방법이 없었다 — 유통가공 창고 템플릿이 BOM 을 설명 문서에만 적어 둔 채 키트를 못 만드는
|
|
262
|
+
* 트윈을 만들어 낸 원인이다. 담긴 것은 ISA-95 `OperationsSegment` 이고 시스템에 종속되지 않으므로
|
|
263
|
+
* 여기(공통)에서 싣는다. MES 는 그 위에 직렬·수율·오더연결 레시피 경로를 더 갖는다.
|
|
264
|
+
*/
|
|
265
|
+
if (def.productionSpec?.definition?.operations?.length)
|
|
266
|
+
this.loadOperations(def.productionSpec.definition.operations);
|
|
258
267
|
for (const n of readBoardLocations(def))
|
|
259
268
|
this.locations.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: 'idle', parentId: n.parentId });
|
|
260
269
|
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
@@ -313,6 +322,10 @@ export class FlowEngine {
|
|
|
313
322
|
this.revision = from;
|
|
314
323
|
}
|
|
315
324
|
hydrateObserved(snap, orders = []) {
|
|
325
|
+
/* 확인 처리를 먼저 이어받는다 — 아래에서 상태를 심으면 곧바로 주목 신호가 계산되므로, 늦게
|
|
326
|
+
* 이어받으면 그 한 번은 확인 안 된 것으로 계산된다(화면이 잠깐 빨개진다). */
|
|
327
|
+
for (const id of snap.acked ?? [])
|
|
328
|
+
this._acked.add(id);
|
|
316
329
|
/* **관측된 것을 버리지 않는다.** 예전에는 자리·설비 상태를 'idle' 로, OEE 누적을 0 으로 덮고
|
|
317
330
|
* 물품의 로트·단위·소속·마스터데이터를 떨어뜨렸다. 씨앗이 잃은 것은 **예측도 모른다** —
|
|
318
331
|
* 고장 난 설비를 정상으로, 진행 중인 일을 없는 것으로 놓고 미래를 굴리면 답이 낙관 쪽으로 치우친다. */
|
|
@@ -549,8 +562,15 @@ export class FlowEngine {
|
|
|
549
562
|
}
|
|
550
563
|
case CMD.attentionAck: {
|
|
551
564
|
const id = cmd.args?.id;
|
|
552
|
-
if (id)
|
|
565
|
+
if (id) {
|
|
553
566
|
this._acked.add(id);
|
|
567
|
+
/*
|
|
568
|
+
* **사실로 남긴다.** 확인 처리는 사람이 한 행위라 상태에서 다시 계산될 수 없다. 저널에
|
|
569
|
+
* 남기지 않으면 재기동하면 확인해 둔 신호가 다시 빨개지고, 과거를 되짚어도 그때 무엇을
|
|
570
|
+
* 확인했는지 알 수 없다. 다른 커맨드(보류·재개)가 델타를 내는 것과 같은 길이다.
|
|
571
|
+
*/
|
|
572
|
+
this.emitOp(OP_EVENT.attentionAck, { id, at: this.now() });
|
|
573
|
+
}
|
|
554
574
|
return ok();
|
|
555
575
|
}
|
|
556
576
|
// Operable 코어 — 자원(설비·설비) 제어. capability-keyed(resourceId), 모든 operable 자원 공통.
|
|
@@ -735,7 +755,9 @@ export class FlowEngine {
|
|
|
735
755
|
...(o.endTime ? { endTime: o.endTime } : {}),
|
|
736
756
|
held: o.held
|
|
737
757
|
})),
|
|
738
|
-
attentions: this.computeAttentions()
|
|
758
|
+
attentions: this.computeAttentions(),
|
|
759
|
+
/* 확인해 둔 신호 — 스냅샷으로 왕복해야 재기동 후에도 확인 상태가 유지된다. */
|
|
760
|
+
acked: [...this._acked]
|
|
739
761
|
};
|
|
740
762
|
}
|
|
741
763
|
/*
|
|
@@ -896,6 +918,15 @@ export class FlowEngine {
|
|
|
896
918
|
* 오퍼레이션 명세를 싣는다 — 도메인 정의(ISA-95 OperationsSegment)의 시뮬 명세를 커널이 소비하는 입구.
|
|
897
919
|
* 같은 key 를 다시 실으면 덮어쓴다(정의가 권위).
|
|
898
920
|
*/
|
|
921
|
+
/**
|
|
922
|
+
* 선언된 오퍼레이션들 — 도메인 커널이 "무엇을 만들 수 있나" 를 물을 수 있게.
|
|
923
|
+
*
|
|
924
|
+
* `operationSpecs` 를 도메인이 직접 뒤지지 않게 읽기 창구를 둔다: 저장 형태(맵)가 바뀌어도
|
|
925
|
+
* 도메인은 몰라야 하고, 도메인이 그 맵에 쓰는 일이 생기면 정의가 권위라는 규약이 깨진다.
|
|
926
|
+
*/
|
|
927
|
+
declaredOperations() {
|
|
928
|
+
return [...this.operationSpecs.values()];
|
|
929
|
+
}
|
|
899
930
|
loadOperations(ops = []) {
|
|
900
931
|
for (const o of ops)
|
|
901
932
|
if (o?.key)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './contract.ts';
|
|
2
|
+
export * from './scenario-validate.ts';
|
|
2
3
|
export * from './domain-definition.ts';
|
|
3
4
|
export * from './counterfactual.ts';
|
|
4
5
|
export * from './forecast.ts';
|
|
@@ -25,5 +26,4 @@ export * from './capacity.ts';
|
|
|
25
26
|
export { WmsKernel } from './kernel.ts';
|
|
26
27
|
export { YmsKernel } from './yms-kernel.ts';
|
|
27
28
|
export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from './mes-kernel.ts';
|
|
28
|
-
export type { MesDefinitionSpec } from './mes-kernel.ts';
|
|
29
29
|
export * from './vocabulary.ts';
|
package/dist/index.js
CHANGED
package/dist/kernel.d.ts
CHANGED
|
@@ -28,6 +28,41 @@ export declare class WmsKernel extends FlowEngine {
|
|
|
28
28
|
protected allocate(o: FlowOrder): void;
|
|
29
29
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
30
30
|
protected onTaskComplete(t: FlowTask): void;
|
|
31
|
+
/**
|
|
32
|
+
* 부족한 라인을 **만들어서** 채운다 — 부품 이송 → 가공 → 되돌리기 3단 사슬.
|
|
33
|
+
*
|
|
34
|
+
* 사슬인 이유는 커널의 두 규칙 때문이다(둘 다 의도된 규칙이라 우회하지 않는다):
|
|
35
|
+
* 자재는 **작업이 일어나는 자리에** 있어야 소비되고, 산출물은 `in_progress` 로 태어나 **팔 수 있는
|
|
36
|
+
* 재고가 아니다.** 그래서 부품을 작업대로 옮기고, 가공하고, 나온 것을 보관 자리로 되돌린다.
|
|
37
|
+
*
|
|
38
|
+
* **한 오더에 사슬 하나만** 굴린다. 매 tick 마다 다시 발행하면 같은 부품을 두 번 끌어오는 작업이
|
|
39
|
+
* 쌓이고, 그중 하나만 성공한 뒤 나머지는 영원히 재료를 기다린다.
|
|
40
|
+
*/
|
|
41
|
+
private makeShortLines;
|
|
42
|
+
/** 이 오더가 굴리고 있는 생산 사슬이 있나 — 이송·가공·되돌리기 중 하나라도 열려 있으면 그렇다. */
|
|
43
|
+
private hasOpenMakeChain;
|
|
44
|
+
/** 지금 재고 — 판단 함수가 보는 형태로. */
|
|
45
|
+
private stockLines;
|
|
46
|
+
/** 부품을 작업대로 — 팔레트 이동이므로 피킹과 같은 기제다(부분 소비는 코어가 한다). */
|
|
47
|
+
private issueFeed;
|
|
48
|
+
/** 가공 — 작업 종류를 **오퍼레이션 키**로 둔다(코어가 그 키로 명세를 찾는다). */
|
|
49
|
+
private issueProcess;
|
|
50
|
+
private pushTask;
|
|
51
|
+
/**
|
|
52
|
+
* 가공 완료 — 코어가 이미 소비(시작)와 산출(완료 직전)을 실행했다. 남은 일은 만든 것을 **재고로
|
|
53
|
+
* 들여놓는 것**이다: 산출물은 `in_progress` 로 태어나 팔 수 있는 재고가 아니다.
|
|
54
|
+
*
|
|
55
|
+
* ── 왜 팔레트로 묶나 ───────────────────────────────────────────────────────
|
|
56
|
+
* 코어의 산출은 **클래스+수량 줄**이고 그 키에는 자리가 박혀 있다(`품목@자리`). 창고의 출고 경로는
|
|
57
|
+
* 직렬 물류단위(팔레트 SSCC)를 다루므로, 그 줄을 그대로 출고에 태우면 키를 EPC 로 착각해 조회가
|
|
58
|
+
* 깨진다(실제로 그렇게 터졌다). 억지로 태우면 EPCIS 에도 **EPC 가 아닌 문자열**이 `epcList` 로
|
|
59
|
+
* 나가는데, 그것은 코어가 산출에서 경계한 바로 그 일이다.
|
|
60
|
+
*
|
|
61
|
+
* 그래서 만든 물건을 **입고가 하는 것과 같은 방식**으로 들여놓는다: 팔레트(SSCC)를 만들고 그 안에
|
|
62
|
+
* 세트 N개를 담는다(AggregationEvent). 현장에서도 가공물은 팔레트에 실려 보관으로 간다.
|
|
63
|
+
* 그 뒤는 기존 경로 그대로다 — `putaway` 로 보관 자리에 넣으면 판매 가능이 된다.
|
|
64
|
+
*/
|
|
65
|
+
private onProcessComplete;
|
|
31
66
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
32
67
|
private finalizeOrder;
|
|
33
68
|
}
|
package/dist/kernel.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import { CMD } from "./contract.js";
|
|
9
9
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
10
10
|
import { FlowEngine } from "./flow-engine.js";
|
|
11
|
+
import { planMakeToOrder } from "./make-to-order.js";
|
|
11
12
|
import { BIZSTEP, BTT } from "./wms-profile.js";
|
|
12
13
|
import { DISP, ILMD_ATTR, aggregationEvent, gdtiUri, objectEvent, ssccUri, transactionEvent } from "./epcis.js";
|
|
14
|
+
import { subLotIdOf } from "./contract.js";
|
|
13
15
|
const TRAVEL_MS = 30_000;
|
|
14
16
|
const COMPANY_PREFIX = '0614141';
|
|
15
17
|
const SHELF_MS = 30 * 24 * 3_600_000; // 기본 유통기한(30일)
|
|
@@ -138,8 +140,13 @@ export class WmsKernel extends FlowEngine {
|
|
|
138
140
|
chosenAll.push(epc);
|
|
139
141
|
}
|
|
140
142
|
}
|
|
143
|
+
/*
|
|
144
|
+
* **재고에 없으면 만들 수 있는지 본다** — 유통가공(키팅·세트조립) 창고가 여기서 막혀 있었다.
|
|
145
|
+
* 인바운드는 부품을 넣고 오더는 세트를 요구하는데 그 세트를 만드는 공정을 아무도 부르지 않아,
|
|
146
|
+
* 랙이 꽉 차고 출고가 한 건도 나오지 않았다. 판단은 `planMakeToOrder`(순수)가 한다.
|
|
147
|
+
*/
|
|
141
148
|
if (chosenAll.length === 0)
|
|
142
|
-
return;
|
|
149
|
+
return this.makeShortLines(o);
|
|
143
150
|
/* 할당은 아직 집은 것이 아니다 — 물건은 보관 자리에 있고 처분만 '예약' 으로 바뀐다.
|
|
144
151
|
* 그래서 단계는 storing 이다(피킹 관측은 실제로 집을 때 따로 난다). */
|
|
145
152
|
this.reserve(chosenAll, BIZSTEP.storing);
|
|
@@ -156,6 +163,13 @@ export class WmsKernel extends FlowEngine {
|
|
|
156
163
|
}
|
|
157
164
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
158
165
|
onTaskComplete(t) {
|
|
166
|
+
/*
|
|
167
|
+
* **생산 사슬의 걸음들을 먼저 가른다.** 아래 이동 처리는 "주체가 팔레트 하나" 를 전제하는데,
|
|
168
|
+
* 가공은 클래스+수량 소비/산출이라 주체가 없다 — 그대로 두면 물품을 찾지 못해 터진다(실제로
|
|
169
|
+
* 그랬다). 걸음마다 무엇이 끝난 것인지 다르므로 각각 답한다.
|
|
170
|
+
*/
|
|
171
|
+
if (t.intent === 'process')
|
|
172
|
+
return this.onProcessComplete(t);
|
|
159
173
|
const item = this.itemByRef(t.itemEpc);
|
|
160
174
|
/* 여기 도달했다면 코어가 이미 물품을 확인했다(주체가 사라진 작업은 완료 전에 접힌다).
|
|
161
175
|
그래도 단정(`!`)은 쓰지 않는다 — 계약이 바뀌면 조용히 틀리는 대신 분명히 멈춘다. */
|
|
@@ -166,6 +180,14 @@ export class WmsKernel extends FlowEngine {
|
|
|
166
180
|
from.occupancy--;
|
|
167
181
|
to.occupancy++;
|
|
168
182
|
item.location = to.id;
|
|
183
|
+
/*
|
|
184
|
+
* 부품 이송 — 자리만 옮긴다. 처분은 예약으로 남겨 둔다(다른 오더가 집어 가면 이 가공이 굶는다).
|
|
185
|
+
* 관측은 `staging_outbound` 가 아니라 이동 그대로다 — 아직 아무것도 만들지 않았다.
|
|
186
|
+
*/
|
|
187
|
+
if (t.kind === 'feed') {
|
|
188
|
+
this.emit(objectEvent({ eventTime: this.now(), action: 'OBSERVE', bizStep: BIZSTEP.storing, disposition: item.disposition, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
169
191
|
if (t.kind === 'putaway') {
|
|
170
192
|
item.disposition = DISP.sellable;
|
|
171
193
|
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 }));
|
|
@@ -181,6 +203,131 @@ export class WmsKernel extends FlowEngine {
|
|
|
181
203
|
if (order.picked.length === order.allocated.length)
|
|
182
204
|
this.finalizeOrder(order, to);
|
|
183
205
|
}
|
|
206
|
+
// ── 수요가 부르는 생산(유통가공) ────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* 부족한 라인을 **만들어서** 채운다 — 부품 이송 → 가공 → 되돌리기 3단 사슬.
|
|
209
|
+
*
|
|
210
|
+
* 사슬인 이유는 커널의 두 규칙 때문이다(둘 다 의도된 규칙이라 우회하지 않는다):
|
|
211
|
+
* 자재는 **작업이 일어나는 자리에** 있어야 소비되고, 산출물은 `in_progress` 로 태어나 **팔 수 있는
|
|
212
|
+
* 재고가 아니다.** 그래서 부품을 작업대로 옮기고, 가공하고, 나온 것을 보관 자리로 되돌린다.
|
|
213
|
+
*
|
|
214
|
+
* **한 오더에 사슬 하나만** 굴린다. 매 tick 마다 다시 발행하면 같은 부품을 두 번 끌어오는 작업이
|
|
215
|
+
* 쌓이고, 그중 하나만 성공한 뒤 나머지는 영원히 재료를 기다린다.
|
|
216
|
+
*/
|
|
217
|
+
makeShortLines(o) {
|
|
218
|
+
if (!o.lines?.length)
|
|
219
|
+
return;
|
|
220
|
+
if (this.hasOpenMakeChain(o.id))
|
|
221
|
+
return; // 이미 굴러가는 중
|
|
222
|
+
const stock = this.stockLines();
|
|
223
|
+
const locations = [...this.locations.values()].map(l => ({ id: l.id, type: l.type }));
|
|
224
|
+
const ops = this.declaredOperations();
|
|
225
|
+
for (const line of o.lines) {
|
|
226
|
+
const have = stock.filter(s => s.gtin === line.gtin && s.sellable && this.locations.get(s.location)?.type === 'storage')
|
|
227
|
+
.reduce((n, s) => n + s.qty, 0);
|
|
228
|
+
const short = line.requested - have;
|
|
229
|
+
if (short <= 0)
|
|
230
|
+
continue;
|
|
231
|
+
const plan = planMakeToOrder(line.gtin, short, ops, stock, locations);
|
|
232
|
+
for (const step of plan.steps) {
|
|
233
|
+
if (step.step === 'feed')
|
|
234
|
+
this.issueFeed(o, step.gtin, step.from, step.to, step.qty);
|
|
235
|
+
else
|
|
236
|
+
this.issueProcess(o, step.operation, step.at);
|
|
237
|
+
}
|
|
238
|
+
if (plan.steps.length)
|
|
239
|
+
return; // 한 번에 한 라인씩 — 부품을 여러 라인이 동시에 다투지 않게
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** 이 오더가 굴리고 있는 생산 사슬이 있나 — 이송·가공·되돌리기 중 하나라도 열려 있으면 그렇다. */
|
|
243
|
+
hasOpenMakeChain(orderId) {
|
|
244
|
+
for (const t of this.tasks.values()) {
|
|
245
|
+
if (t.orderId !== orderId || t.status === 'completed')
|
|
246
|
+
continue;
|
|
247
|
+
if (t.kind === 'feed' || t.intent === 'process')
|
|
248
|
+
return true;
|
|
249
|
+
/* 만든 것을 들여놓는 적재도 사슬의 일부다 — 끝나기 전에 다시 발행하면 같은 부품을 또 끌어온다. */
|
|
250
|
+
if (t.kind === 'putaway' && t.orderId === orderId)
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
/** 지금 재고 — 판단 함수가 보는 형태로. */
|
|
256
|
+
stockLines() {
|
|
257
|
+
return [...this.items.values()]
|
|
258
|
+
.filter(i => i.gtin)
|
|
259
|
+
.map(i => ({ gtin: i.gtin, location: i.location, qty: i.qty ?? 1, sellable: i.disposition === DISP.sellable }));
|
|
260
|
+
}
|
|
261
|
+
/** 부품을 작업대로 — 팔레트 이동이므로 피킹과 같은 기제다(부분 소비는 코어가 한다). */
|
|
262
|
+
issueFeed(o, gtin, from, to, qty) {
|
|
263
|
+
const src = [...this.items.values()].find(i => i.gtin === gtin && i.location === from && i.disposition === DISP.sellable);
|
|
264
|
+
if (!src)
|
|
265
|
+
return;
|
|
266
|
+
/* 이송 중에 다른 오더가 같은 팔레트를 집지 않게 예약으로 바꾼다 — 코어의 자재 게이트는
|
|
267
|
+
처분을 보지 않지만, 이 커널의 `allocate` 는 `sellable` 만 집으므로 이것으로 충돌을 막는다. */
|
|
268
|
+
src.disposition = DISP.reserved;
|
|
269
|
+
void qty; // 팔레트 단위로 옮긴다 — 필요량은 코어가 소비 시점에 부분 취득한다
|
|
270
|
+
this.pushTask(o, 'feed', src.epc, from, to);
|
|
271
|
+
}
|
|
272
|
+
/** 가공 — 작업 종류를 **오퍼레이션 키**로 둔다(코어가 그 키로 명세를 찾는다). */
|
|
273
|
+
issueProcess(o, operation, at) {
|
|
274
|
+
/* 물품을 지목하지 않는다 — 클래스+수량 소비/산출이므로 주체가 팔레트 하나가 아니다.
|
|
275
|
+
코어의 작업 게이트는 빈 `itemEpc` 를 견딘다(`t.itemEpc && …` 조건부 확인). */
|
|
276
|
+
/* **의도를 process 로 밝힌다** — 코어가 이 표시로 이동(모션)과 변환을 가르고, 이 커널의 완료 훅도
|
|
277
|
+
그것으로 판단한다. "선언된 오퍼레이션인가" 로 가르면 산출을 선언한 이동 작업까지 삼킨다. */
|
|
278
|
+
this.pushTask(o, operation, '', at, at, 'process');
|
|
279
|
+
}
|
|
280
|
+
pushTask(o, kind, itemEpc, from, to, intent) {
|
|
281
|
+
const id = `task-${++this.taskSeq}`;
|
|
282
|
+
const task = {
|
|
283
|
+
id, kind, status: 'created', itemEpc, fromNode: from, toNode: to, resource: null, remainingMs: 0,
|
|
284
|
+
durationMs: this.durationOf({ kind, fromNode: from, toNode: to }, TRAVEL_MS),
|
|
285
|
+
...(intent ? { intent } : {}),
|
|
286
|
+
...(o ? { orderId: o.id } : {})
|
|
287
|
+
};
|
|
288
|
+
this.tasks.set(id, task);
|
|
289
|
+
this.emitTask(task);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 가공 완료 — 코어가 이미 소비(시작)와 산출(완료 직전)을 실행했다. 남은 일은 만든 것을 **재고로
|
|
293
|
+
* 들여놓는 것**이다: 산출물은 `in_progress` 로 태어나 팔 수 있는 재고가 아니다.
|
|
294
|
+
*
|
|
295
|
+
* ── 왜 팔레트로 묶나 ───────────────────────────────────────────────────────
|
|
296
|
+
* 코어의 산출은 **클래스+수량 줄**이고 그 키에는 자리가 박혀 있다(`품목@자리`). 창고의 출고 경로는
|
|
297
|
+
* 직렬 물류단위(팔레트 SSCC)를 다루므로, 그 줄을 그대로 출고에 태우면 키를 EPC 로 착각해 조회가
|
|
298
|
+
* 깨진다(실제로 그렇게 터졌다). 억지로 태우면 EPCIS 에도 **EPC 가 아닌 문자열**이 `epcList` 로
|
|
299
|
+
* 나가는데, 그것은 코어가 산출에서 경계한 바로 그 일이다.
|
|
300
|
+
*
|
|
301
|
+
* 그래서 만든 물건을 **입고가 하는 것과 같은 방식**으로 들여놓는다: 팔레트(SSCC)를 만들고 그 안에
|
|
302
|
+
* 세트 N개를 담는다(AggregationEvent). 현장에서도 가공물은 팔레트에 실려 보관으로 간다.
|
|
303
|
+
* 그 뒤는 기존 경로 그대로다 — `putaway` 로 보관 자리에 넣으면 판매 가능이 된다.
|
|
304
|
+
*/
|
|
305
|
+
onProcessComplete(t) {
|
|
306
|
+
const made = (t.materialActual ?? []).filter(m => m.use === 'produced');
|
|
307
|
+
const order = t.orderId ? this.orders.get(t.orderId) : undefined;
|
|
308
|
+
const at = t.toNode;
|
|
309
|
+
for (const m of made) {
|
|
310
|
+
const key = subLotIdOf(m.definitionId, at);
|
|
311
|
+
const row = this.items.get(key);
|
|
312
|
+
if (!row)
|
|
313
|
+
continue; // 누가 이미 소비했다 — 없는 것을 만들어 내지 않는다
|
|
314
|
+
const qty = row.qty ?? 0;
|
|
315
|
+
if (qty <= 0)
|
|
316
|
+
continue;
|
|
317
|
+
/* 클래스 줄을 팔레트로 바꾼다 — 자리 점유는 줄 하나에서 줄 하나로(늘지 않는다). */
|
|
318
|
+
this.items.delete(key);
|
|
319
|
+
const pallet = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
320
|
+
const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
|
|
321
|
+
this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
|
|
322
|
+
const eventTime = this.now();
|
|
323
|
+
this.emit(aggregationEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.packing, parentID: pallet, childQuantityList: qtyList, readPoint: at }));
|
|
324
|
+
this.emit(objectEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.packing, disposition: DISP.in_progress, epcList: [pallet], quantityList: qtyList, readPoint: at, bizLocation: at }));
|
|
325
|
+
/* 보관으로 — `putaway` 완료가 판매 가능으로 표시하고 storing 관측을 낸다(입고와 같은 걸음). */
|
|
326
|
+
const storage = this.locationByType('storage');
|
|
327
|
+
if (storage)
|
|
328
|
+
this.pushTask(order, 'putaway', pallet, at, storage.id);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
184
331
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
185
332
|
finalizeOrder(order, staging) {
|
|
186
333
|
const shipDock = this.locationByType('dock-ship') ?? staging;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { OperationDef } from './domain-definition.ts';
|
|
2
|
+
/** 이 판단이 보는 재고 한 줄 — 무엇이 어디에 얼마나, 팔 수 있나. */
|
|
3
|
+
export interface StockLine {
|
|
4
|
+
/** 품목 식별자(GTIN). */
|
|
5
|
+
gtin: string;
|
|
6
|
+
/** 지금 있는 자리. */
|
|
7
|
+
location: string;
|
|
8
|
+
/** 수량(개수). */
|
|
9
|
+
qty: number;
|
|
10
|
+
/** 팔 수 있는 상태인가 — 아니면 예약·가공 중이다. */
|
|
11
|
+
sellable: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** 자리 하나 — 타입으로 역할을 안다(작업대인가 보관인가). */
|
|
14
|
+
export interface LocationLine {
|
|
15
|
+
id: string;
|
|
16
|
+
type: string;
|
|
17
|
+
}
|
|
18
|
+
/** 발행할 걸음. */
|
|
19
|
+
export type MakeStep =
|
|
20
|
+
/** 부품을 작업대로 옮긴다 — 자재가 작업 자리에 있어야 소비되기 때문이다. */
|
|
21
|
+
{
|
|
22
|
+
step: 'feed';
|
|
23
|
+
gtin: string;
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
qty: number;
|
|
27
|
+
}
|
|
28
|
+
/** 가공한다 — 코어가 소비(시작)·산출(완료)을 실행한다. */
|
|
29
|
+
| {
|
|
30
|
+
step: 'process';
|
|
31
|
+
operation: string;
|
|
32
|
+
at: string;
|
|
33
|
+
};
|
|
34
|
+
export interface MakePlan {
|
|
35
|
+
/** 지금 발행할 걸음들. 비어 있으면 발행할 것이 없다(기다리는 중이거나 만들 수 없다). */
|
|
36
|
+
steps: MakeStep[];
|
|
37
|
+
/**
|
|
38
|
+
* 왜 아무것도 발행하지 않았나 — **조용히 아무 일도 없는 상태를 남기지 않는다.**
|
|
39
|
+
* `not-producible` 만들 방법이 선언되지 않았다 · `waiting-feed` 부품이 오는 중 ·
|
|
40
|
+
* `short-materials` 부품 자체가 부족하다(만들 수 없다) · `no-station` 공정 자리가 없다.
|
|
41
|
+
*/
|
|
42
|
+
reason?: 'not-producible' | 'waiting-feed' | 'short-materials' | 'no-station';
|
|
43
|
+
}
|
|
44
|
+
/** 품목 → 그것을 산출하는 공정. 선언에서 파생한다(코드에 표를 두지 않는다). */
|
|
45
|
+
export declare function producedByIndex(ops: readonly OperationDef[]): Map<string, OperationDef>;
|
|
46
|
+
/**
|
|
47
|
+
* 부족한 품목을 만들기 위해 **지금** 발행할 걸음을 정한다.
|
|
48
|
+
*
|
|
49
|
+
* @param gtin 부족한 품목(오더가 요구하는 것).
|
|
50
|
+
* @param shortQty 부족 수량.
|
|
51
|
+
* @param ops 이 트윈에 선언된 오퍼레이션들.
|
|
52
|
+
* @param stock 현재 재고.
|
|
53
|
+
* @param locations 자리 목록(공정 자리 해소용).
|
|
54
|
+
*
|
|
55
|
+
* 한 번에 **한 걸음 종류만** 낸다: 부품이 아직 작업대에 다 오지 않았으면 이송만 내고 가공은 다음에
|
|
56
|
+
* 낸다. 두 걸음을 한꺼번에 내면 가공이 부품 없이 시작을 시도하고, 코어가 거절하는 작업이 큐에 쌓인다.
|
|
57
|
+
*/
|
|
58
|
+
export declare function planMakeToOrder(gtin: string, shortQty: number, ops: readonly OperationDef[], stock: readonly StockLine[], locations: readonly LocationLine[]): MakePlan;
|