@operato/twin-kernel 0.7.39 → 0.7.40
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/contract.d.ts +174 -8
- package/dist/contract.js +42 -1
- package/dist/domain-definition.d.ts +17 -0
- package/dist/domain-definition.js +7 -0
- package/dist/ems-kernel.js +12 -0
- package/dist/epcis.d.ts +22 -0
- package/dist/epcis.js +91 -2
- package/dist/face2-adapter.d.ts +21 -1
- package/dist/face2-adapter.js +12 -1
- package/dist/flow-engine.d.ts +156 -8
- package/dist/flow-engine.js +324 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel.d.ts +15 -0
- package/dist/kernel.js +50 -16
- package/dist/mes-kernel.d.ts +117 -27
- package/dist/mes-kernel.js +331 -168
- package/dist/observed-reducer.js +7 -4
- package/dist/yms-kernel.js +12 -5
- package/dist-cjs/index.cjs +632 -182
- package/package.json +1 -1
package/dist/kernel.js
CHANGED
|
@@ -11,9 +11,20 @@ import { FlowEngine } from "./flow-engine.js";
|
|
|
11
11
|
import { planMakeToOrder } from "./make-to-order.js";
|
|
12
12
|
import { BIZSTEP, BTT } from "./wms-profile.js";
|
|
13
13
|
import { DISP, ILMD_ATTR, aggregationEvent, gdtiUri, objectEvent, ssccUri, transactionEvent } from "./epcis.js";
|
|
14
|
-
import { subLotIdOf } from "./contract.js";
|
|
14
|
+
import { subLotIdOf, itemKeyOf } from "./contract.js";
|
|
15
15
|
const TRAVEL_MS = 30_000;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 이 내장 시나리오가 쓰는 **날조된 회사 프리픽스** — 선언이 없는 트윈만 이 길로 온다.
|
|
18
|
+
*
|
|
19
|
+
* 값이 실 배정 대역(`06…`)이라 저널에 남는 것은 **남의 번호일 수 있는 데이터**다. GS1 은 2022 년에
|
|
20
|
+
* 모든 예제를 `952` 로 옮겼으므로(CBV 2.0 변경 이력), 이 값은 「GS1 이 버린 옛 예제」이기도 하다.
|
|
21
|
+
*
|
|
22
|
+
* 지우는 길은 이 상수를 다른 값으로 바꾸는 것이 **아니다** — 이 경로는 부품·라우트까지 상수여서
|
|
23
|
+
* 정체성만 고치면 「지원하는 척」이 된다. 선언(`productionSpec`)으로 도는 트윈은 이 상수를 지나지
|
|
24
|
+
* 않으며, 지나는 트윈은 `identityGroundingOf` 가 `fabricated` 로 판정한다 — 릴리즈 게이트가 그 값으로
|
|
25
|
+
* 막는다. 이름에 `LEGACY_` 를 둔 이유는 **새 코드가 무심코 집어 들지 못하게** 하는 것이다.
|
|
26
|
+
*/
|
|
27
|
+
const LEGACY_COMPANY_PREFIX = '0614141';
|
|
17
28
|
const SHELF_MS = 30 * 24 * 3_600_000; // 기본 유통기한(30일)
|
|
18
29
|
const SHELF_JITTER_MS = 5 * 24 * 3_600_000; // 로트별 만료 편차(FEFO 가 FIFO 와 갈리게)
|
|
19
30
|
export class WmsKernel extends FlowEngine {
|
|
@@ -21,18 +32,38 @@ export class WmsKernel extends FlowEngine {
|
|
|
21
32
|
constructor(tenantId, policy = firstFitPolicy) {
|
|
22
33
|
super(tenantId, policy);
|
|
23
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* 내장 시나리오가 요구하는 자리 — **없으면 말한다.**
|
|
37
|
+
*
|
|
38
|
+
* ── 왜 (2026-08-20) ────────────────────────────────────────────────────────
|
|
39
|
+
* 이 커널은 입고·중간·보관 자리를 **자기 낱말로** 찾았고(`dock`·`staging`·`storage`), 없으면 조용히
|
|
40
|
+
* `return` 했다. 그래서 그 낱말을 쓰지 않는 현장의 트윈은 **아무 일도 일어나지 않는 공장**이 됐다 —
|
|
41
|
+
* 오류도 로그도 없이. 실제로 확인된 자리다: 첫 실 시스템(F&B)에는 `dock`·`gate`·`staging` 이 **한 건도
|
|
42
|
+
* 없다**(자리 타입이 자유 문자열이고, 입출고가 도크 단계 없이 자리에 직접 일어난다).
|
|
43
|
+
*
|
|
44
|
+
* 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
|
|
45
|
+
* 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
|
|
46
|
+
* 것이고, 던지면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
|
|
47
|
+
* 조용한 정지는 그 사실조차 남기지 않는다.
|
|
48
|
+
*/
|
|
49
|
+
builtInLocation(type, what) {
|
|
50
|
+
const loc = this.locationByType(type);
|
|
51
|
+
if (!loc) {
|
|
52
|
+
throw new Error(`built-in WMS scenario needs a location of type '${type}' (${what}) and this twin has none — ` +
|
|
53
|
+
`rename it, or drive this twin from declarations instead of the built-in scenario`);
|
|
54
|
+
}
|
|
55
|
+
return loc;
|
|
56
|
+
}
|
|
24
57
|
/** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
|
|
25
58
|
onArrival(spec) {
|
|
26
|
-
const dock = this.
|
|
27
|
-
|
|
28
|
-
return;
|
|
29
|
-
const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq); // 팔레트 SSCC
|
|
59
|
+
const dock = this.builtInLocation('dock', 'inbound pallets land here');
|
|
60
|
+
const epc = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq); // 팔레트 SSCC
|
|
30
61
|
const gtin = this.pickGtin(spec.content.skuMix); // SGTIN idpat = epcClass
|
|
31
62
|
/* 품목 구성이 비어 있으면 **도착을 만들지 않는다** — 무엇이 왔는지 말할 수 없는 입고는 사실이 아니다. */
|
|
32
63
|
if (!gtin)
|
|
33
64
|
return;
|
|
34
65
|
const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max); // 케이스 수(비직렬)
|
|
35
|
-
const po = gdtiUri(
|
|
66
|
+
const po = gdtiUri(LEGACY_COMPANY_PREFIX, '401', ++this.poSeq);
|
|
36
67
|
const eventTime = this.now();
|
|
37
68
|
const qtyList = [{ epcClass: gtin, quantity: qty }];
|
|
38
69
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
@@ -106,7 +137,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
106
137
|
/** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
|
|
107
138
|
createSalesOrder(lines, spec) {
|
|
108
139
|
const id = `order-${++this.orderSeq}`;
|
|
109
|
-
const so = gdtiUri(
|
|
140
|
+
const so = gdtiUri(LEGACY_COMPANY_PREFIX, '402', ++this.soSeq);
|
|
110
141
|
const requested = lines.reduce((s, l) => s + l.qty, 0);
|
|
111
142
|
const order = {
|
|
112
143
|
id, kind: 'outbound', status: 'created', requested, fulfilled: 0, bizTransaction: so,
|
|
@@ -122,9 +153,10 @@ export class WmsKernel extends FlowEngine {
|
|
|
122
153
|
* 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
|
|
123
154
|
*/
|
|
124
155
|
allocate(o) {
|
|
125
|
-
|
|
126
|
-
if (!
|
|
156
|
+
/* 오더에 라인이 없으면 배정할 것이 없다 — 그건 자리 문제가 아니다(사실대로 건너뛴다). */
|
|
157
|
+
if (!o.lines)
|
|
127
158
|
return;
|
|
159
|
+
const staging = this.builtInLocation('staging', 'picked pallets wait here before shipping');
|
|
128
160
|
const chosenAll = [];
|
|
129
161
|
for (const line of o.lines) {
|
|
130
162
|
const already = o.allocated.filter(e => this.items.get(e)?.gtin === line.gtin).length;
|
|
@@ -179,7 +211,8 @@ export class WmsKernel extends FlowEngine {
|
|
|
179
211
|
const to = this.locations.get(t.toNode);
|
|
180
212
|
from.occupancy--;
|
|
181
213
|
to.occupancy++;
|
|
182
|
-
|
|
214
|
+
/* 자리 이동은 저장소 통로로 — 직접 고치면 자리별 색인이 어긋난다(그러면 재고가 조용히 사라진다). */
|
|
215
|
+
this.items.relocate(itemKeyOf(item), to.id);
|
|
183
216
|
/*
|
|
184
217
|
* 부품 이송 — 자리만 옮긴다. 처분은 예약으로 남겨 둔다(다른 오더가 집어 가면 이 가공이 굶는다).
|
|
185
218
|
* 관측은 `staging_outbound` 가 아니라 이동 그대로다 — 아직 아무것도 만들지 않았다.
|
|
@@ -316,23 +349,24 @@ export class WmsKernel extends FlowEngine {
|
|
|
316
349
|
continue;
|
|
317
350
|
/* 클래스 줄을 팔레트로 바꾼다 — 자리 점유는 줄 하나에서 줄 하나로(늘지 않는다). */
|
|
318
351
|
this.items.delete(key);
|
|
319
|
-
const pallet = ssccUri(
|
|
352
|
+
const pallet = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
|
|
320
353
|
const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
|
|
321
354
|
this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
|
|
322
355
|
const eventTime = this.now();
|
|
323
356
|
this.emit(aggregationEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.packing, parentID: pallet, childQuantityList: qtyList, readPoint: at }));
|
|
324
357
|
this.emit(objectEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.packing, disposition: DISP.in_progress, epcList: [pallet], quantityList: qtyList, readPoint: at, bizLocation: at }));
|
|
325
358
|
/* 보관으로 — `putaway` 완료가 판매 가능으로 표시하고 storing 관측을 낸다(입고와 같은 걸음). */
|
|
326
|
-
const storage = this.
|
|
327
|
-
|
|
328
|
-
this.pushTask(order, 'putaway', pallet, at, storage.id);
|
|
359
|
+
const storage = this.builtInLocation('storage', 'received pallets are put away here');
|
|
360
|
+
this.pushTask(order, 'putaway', pallet, at, storage.id);
|
|
329
361
|
}
|
|
330
362
|
}
|
|
331
363
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
332
364
|
finalizeOrder(order, staging) {
|
|
365
|
+
/* 출고 도크가 없으면 **중간 자리에서 그대로 출하한다** — 이것은 뜻을 바꾸는 대체가 아니라 실제
|
|
366
|
+
창고의 모양이다(도크 단계가 없는 현장이 있다). 그 사실은 화물의 출발 자리로 저널에 남는다. */
|
|
333
367
|
const shipDock = this.locationByType('dock-ship') ?? staging;
|
|
334
368
|
const eventTime = this.now();
|
|
335
|
-
const shipment = ssccUri(
|
|
369
|
+
const shipment = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
|
|
336
370
|
order.shipmentEpc = shipment;
|
|
337
371
|
const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
|
|
338
372
|
// 패킹: 화물(shipment) ← 팔레트 조립(merge). 팔레트는 출하 DELETE 까지 독립 유지 → consume 없음.
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import type { GeneratorSpec, Command, CommandAck, ProductionSpec } from './contract.ts';
|
|
1
|
+
import type { GeneratorSpec, Command, CommandAck, ProductionSpec, TwinModelDef, IdentityGroundingView } from './contract.ts';
|
|
2
2
|
import type { AllocationPolicy } from './allocation-policy.ts';
|
|
3
3
|
import { FlowEngine } from './flow-engine.ts';
|
|
4
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
|
-
/** 레거시 MES 제품 목록(gtin+라벨) — mes.changeover 대상 제품 소싱용. 정의-구동 전환 시 recipe 출력으로 교체. */
|
|
16
|
-
export declare const MES_PRODUCTS: {
|
|
17
|
-
gtin: string;
|
|
18
|
-
label: string;
|
|
19
|
-
}[];
|
|
20
5
|
export declare class MesKernel extends FlowEngine {
|
|
21
6
|
private wipSeq;
|
|
22
7
|
private prodSeq;
|
|
@@ -36,7 +21,39 @@ export declare class MesKernel extends FlowEngine {
|
|
|
36
21
|
* 모든 계산이 거짓이 된다).
|
|
37
22
|
*/
|
|
38
23
|
private assertNoDoubleProduction;
|
|
39
|
-
|
|
24
|
+
/**
|
|
25
|
+
* **레시피 모드에서는 MES 가 산출을 소유한다** — 코어는 비켜선다(§`producesOwnOutputs`).
|
|
26
|
+
*
|
|
27
|
+
* MES 의 산출은 개체 정체성(직렬번호) + 수율 + 오더 계보를 갖는다. 코어의 일반 산출은 비직렬
|
|
28
|
+
* 클래스+수량이라 그 셋을 표현하지 못한다. 둘이 같은 선언을 보고 각자 만들면 재고가 두 배가 된다.
|
|
29
|
+
*
|
|
30
|
+
* 레시피가 없으면(유통가공 등) 소유를 주장하지 않는다 — 그때는 코어의 산출이 맞다.
|
|
31
|
+
*/
|
|
32
|
+
protected producesOwnOutputs(_opKey: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* 이 트윈의 정체성 근거 — 선언에서 파생한다(§`identityGroundingOf`).
|
|
35
|
+
*
|
|
36
|
+
* 선언이 근거를 정한다: 이름공간을 선언했으면 `declared`, 발급을 주장했으면 `issued`(주장이지 사실이
|
|
37
|
+
* 아니다). 선언이 없는 MES 트윈은 이제 만들 수 없으므로(`loadTwinModel` 이 거절한다) `fabricated` 로
|
|
38
|
+
* 판정될 일이 없다.
|
|
39
|
+
*/
|
|
40
|
+
protected identityGroundingView(): IdentityGroundingView;
|
|
41
|
+
/**
|
|
42
|
+
* 선언이 말하는 자리 타입이 **이 트윈에 있나** — 로드 시점에 한 번, 모아서 말한다 (2026-08-20).
|
|
43
|
+
*
|
|
44
|
+
* ── 왜 로드 시점인가 ───────────────────────────────────────────────────────
|
|
45
|
+
* 이것은 **선언의 흠**이다: 그 트윈의 생산 선언과 그 트윈의 모델이 서로 어긋났다. 틱에서 발견하면
|
|
46
|
+
* 그 트윈은 「도는데 아무 일도 안 일어나는」 모습이 된다 — 실제로 `allocateDef` 가 첫 공정의 자리를
|
|
47
|
+
* 못 찾으면 조용히 `return` 했고, 오더는 영원히 배정되지 않은 채 화면에는 「running」이라 적혔다.
|
|
48
|
+
*
|
|
49
|
+
* ── 좁히지 않는다 ─────────────────────────────────────────────────────────
|
|
50
|
+
* 「이 트윈이 실제로 지나는 라우트만 본다」는 예외를 한 번 검토했다가 버렸다. 근거로 삼은 「여러
|
|
51
|
+
* 트윈이 한 선언을 공유한다」가 **이 코드에 없는 이야기**였다: 호스트는 트윈마다 자기 모델에서
|
|
52
|
+
* 선언을 만든다(`productionSpecOf(model)`). 없는 사정을 상상해 검사에 구멍을 내지 않는다.
|
|
53
|
+
*
|
|
54
|
+
* 모아서 한 번에 말한다 — 하나씩 실패하면 사람이 같은 기동을 열 번 반복한다.
|
|
55
|
+
*/
|
|
56
|
+
loadTwinModel(def: TwinModelDef): void;
|
|
40
57
|
/**
|
|
41
58
|
* MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
|
|
42
59
|
* 자동 체인지오버(task.changeoverKey 상이 시 셋업)의 수동 버전 — 운영자가 사전 전환(툴링 교체) 지시.
|
|
@@ -44,28 +61,74 @@ export declare class MesKernel extends FlowEngine {
|
|
|
44
61
|
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
45
62
|
*/
|
|
46
63
|
protected handleCommand(cmd: Command): CommandAck;
|
|
47
|
-
/** 부품 수령
|
|
64
|
+
/** 부품 수령 — 도착한 품목이 선언된 입력 자재면 그 자재가 선언한 자리에 생성. */
|
|
48
65
|
protected onArrival(spec: GeneratorSpec): void;
|
|
49
|
-
/** 작업지시 —
|
|
66
|
+
/** 작업지시 — 오더가 자기 레시피를 든다(선언된 레시피 중에서). */
|
|
50
67
|
protected onOrder(_spec: GeneratorSpec): void;
|
|
51
|
-
/** 할당 —
|
|
68
|
+
/** 할당 — 레시피 입력 전량 확보 후 첫 라우트 단계 태스크(부족하면 대기). */
|
|
52
69
|
protected allocate(o: FlowOrder): void;
|
|
53
|
-
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
54
|
-
private emitStation;
|
|
55
|
-
/** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
|
|
56
70
|
/**
|
|
57
71
|
* MES 는 완료 시점에 **오더와 제품 정의**를 딛고 선다 — 둘 중 하나만 없어도 끝맺을 수 없다.
|
|
58
72
|
*
|
|
59
73
|
* 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
60
|
-
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서
|
|
74
|
+
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
|
|
61
75
|
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
|
|
62
76
|
*/
|
|
63
77
|
protected canComplete(t: FlowTask): boolean;
|
|
64
78
|
protected onTaskComplete(t: FlowTask): void;
|
|
79
|
+
/** 선언된 레시피 전부 — 오더가 자기 것을 고르고, 수령이 전부의 소요를 본다. */
|
|
80
|
+
private recipesDef;
|
|
81
|
+
/**
|
|
82
|
+
* 이 오더의 레시피 — **오더가 들면 그것, 없으면 선언 수준의 기본**(§`FlowOrder.recipeKey`).
|
|
83
|
+
*
|
|
84
|
+
* 오더가 든 키가 선언에 없으면 **말한다**: 조용히 첫 레시피로 떨어지면 그 오더는 다른 물건을 만들고,
|
|
85
|
+
* 그 뒤 계보·재고·수율이 전부 엉뚱한 품목에 붙는다.
|
|
86
|
+
*/
|
|
65
87
|
private recipeDef;
|
|
66
88
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
67
89
|
private classOf;
|
|
68
90
|
private serialOf;
|
|
91
|
+
/**
|
|
92
|
+
* 이 공정이 **만든다고 선언한 품목** — 표준 `OpMaterialSpecification` `MaterialUse='produced'`.
|
|
93
|
+
*
|
|
94
|
+
* ── 왜 이 함수가 생겼나 (2026-08-20) ──────────────────────────────────────
|
|
95
|
+
* 예전에는 라우트의 중간 단계마다 커널이 `sgtinUri(prefix, 'WIP', ++seq)` 로 **품목을 지어냈다.**
|
|
96
|
+
* 그러면 배합물·가열완료물·소분물이 전부 같은 상품코드가 된다 — 저널에 남는 것은 「무엇인지 아무도
|
|
97
|
+
* 답할 수 없는 물건」이고, 그것이 영구 기록이 된다. 게다가 그 조립은 `binding` 을 **건너뛴다**:
|
|
98
|
+
* 다른 모든 품목은 `classOf`/`serialOf` 로 인스턴스가 주입한 품번을 지나는데, `'WIP'` 만 커널이
|
|
99
|
+
* itemRef 자리에 리터럴을 넣었다.
|
|
100
|
+
*
|
|
101
|
+
* ── 규율 ──────────────────────────────────────────────────────────────────
|
|
102
|
+
* 만드는 것은 **선언이 정한다**. 선언이 없으면 만들지 않는다(§`produceMaterials` 와 같은 규율 —
|
|
103
|
+
* 「무엇을 만드는지 말하지 않으면 만들지 않는다」). 있으면 그 품목으로 만든다. 어느 쪽도 지어내지 않는다.
|
|
104
|
+
*
|
|
105
|
+
* 둘 이상을 선언하면 **거부한다**: 어느 것이 이 단계의 산출인지는 모델이 말할 일이고, 커널이 첫 줄을
|
|
106
|
+
* 고르면 나머지는 조용히 사라진다.
|
|
107
|
+
*/
|
|
108
|
+
private producedMaterialKeyOf;
|
|
109
|
+
/**
|
|
110
|
+
* 표준 GTIN 클래스 → **모델의 자재 키.**
|
|
111
|
+
*
|
|
112
|
+
* 공정 명세는 표준 식별자(`MaterialDefinitionID` = GTIN idpat)로 품목을 가리키고, MES 프로파일의
|
|
113
|
+
* 나머지는 자재 키(`MaterialDef.key` + `binding`)로 가리킨다. **둘은 다른 층이고 `binding` 이 통로다.**
|
|
114
|
+
* 그래서 선언된 식별자가 이 트윈의 `binding` 을 지나 나오는 것인지 되짚어 확인한다.
|
|
115
|
+
*
|
|
116
|
+
* 없으면 **모델 위반이므로 멈춘다** — 커널이 바인딩 없는 품목의 정체성을 만들어 낼 자리가 없다.
|
|
117
|
+
*/
|
|
118
|
+
private materialKeyOfClass;
|
|
119
|
+
/**
|
|
120
|
+
* 이 자재가 놓이는 **자리 타입** — 선언이 권위다(`MaterialDef.locationType`).
|
|
121
|
+
*
|
|
122
|
+
* 예전에는 이 자리에 `'raw-store'`·`'fg-store'` 가 박혀 있었다. 그래서 현장은 자기 창고를 그 이름으로
|
|
123
|
+
* **개명해야** 트윈이 굴러갔다 — 커널이 현장의 낱말을 정하는 셈이었다. 선언이 없으면 **지어내지 않고
|
|
124
|
+
* 말한다**: 예전 거동(조용한 return)에서는 아무도 왜 자재가 들어오지 않는지 알 수 없었다.
|
|
125
|
+
*
|
|
126
|
+
* 레거시(선언 없는 내장 프로파일) 경로는 이 함수를 쓰지 않는다 — 그쪽에는 대조할 선언이 없으므로
|
|
127
|
+
* 커널 어휘 자체가 계약이다.
|
|
128
|
+
*/
|
|
129
|
+
private locationTypeOfMaterial;
|
|
130
|
+
/** 그 자리 타입의 자리 — 이 트윈에 없으면 말한다(선언과 모델이 어긋난 사실이다). */
|
|
131
|
+
private locationOfMaterial;
|
|
69
132
|
/**
|
|
70
133
|
* 라우트를 용량 계산에 알려 준다 — 수율을 거슬러 올릴 때 순서가 곧 계산이다.
|
|
71
134
|
*
|
|
@@ -73,11 +136,38 @@ export declare class MesKernel extends FlowEngine {
|
|
|
73
136
|
* 틀린다). 생산 정의를 가진 커널만 이 답을 안다.
|
|
74
137
|
*/
|
|
75
138
|
protected routeKeys(): string[] | undefined;
|
|
76
|
-
/**
|
|
139
|
+
/** 선언된 레시피들이 쓰는 **서로 다른** 라우트 키들. 하나면 용량을 답할 수 있다. */
|
|
140
|
+
protected distinctRouteKeys(): string[];
|
|
141
|
+
/** recipe.route → 오퍼레이션 시퀀스 해소. 오더가 자기 레시피를 들면 그 라우트다. */
|
|
77
142
|
private routeOps;
|
|
78
|
-
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면
|
|
143
|
+
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 **그 자재가 선언한 자리**에 생성. */
|
|
79
144
|
private onArrivalDef;
|
|
80
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* 정의 모드 작업지시 — 레시피 산출물 1개. **오더가 자기 레시피를 든다**(§`FlowOrder.recipeKey`).
|
|
147
|
+
*
|
|
148
|
+
* 선언 수준의 `recipeKey` 가 있으면 그 하나만 만든다(현장이 한 품목만 돌린다고 말한 것이다). 없으면
|
|
149
|
+
* 선언된 레시피를 **차례로** 돈다 — 그것이 제품 전환이고, 체인지오버 셋업은 `changeoverKey`(산출물
|
|
150
|
+
* 클래스)가 달라지는 것으로 자동으로 따라온다. 커널 상수 제품 목록이 필요했던 이유가 이것이었다.
|
|
151
|
+
*
|
|
152
|
+
* 차례는 `orderSeq` 로 정한다 — 난수를 쓰면 fork 예측이 결정적이지 않게 된다.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* **작업지시의 거래 문서 식별자** — 선언이 정하고, 커널은 문서 타입을 정하지 않는다.
|
|
156
|
+
*
|
|
157
|
+
* ── 무엇이 틀렸나 (2026-08-21) ────────────────────────────────────────────
|
|
158
|
+
* 여기는 `gdtiUri(prefix, '403', seq)` 였다. GDTI 는 `회사 프리픽스 + 문서 타입 + 일련번호`이고
|
|
159
|
+
* **문서 타입은 GS1 이 공표하는 목록이 아니다** — 프리픽스를 배정받은 회사가 자기 번호 용량에서
|
|
160
|
+
* 정한다. 그런데 커널이 `'403'` 을 스스로 정해 저널에 영구히 기록했다. 회사의 배정 권한을 커널이
|
|
161
|
+
* 대신 행사한 것이고, 프리픽스 날조와 같은 종류다.
|
|
162
|
+
*
|
|
163
|
+
* ── 순서 ──────────────────────────────────────────────────────────────────
|
|
164
|
+
* ① 선언된 이름공간이 있으면 그 아래 `bt` 형태로 만든다(CBV §8.5.4·§8.5.5). **문서 타입이 필요
|
|
165
|
+
* 없는 길**이고, 회사가 도메인만 있으면 되므로 진입장벽이 가장 낮다.
|
|
166
|
+
* ② 없고 문서 타입을 선언했으면 GDTI 로 만든다(그 현장이 GDTI 를 쓰는 것이다).
|
|
167
|
+
* ③ 둘 다 없으면 **말한다.** 지어내면 그 값이 영구 기록이 되고, 어느 화면도 그것이 발명된
|
|
168
|
+
* 번호라고 말해 주지 않는다.
|
|
169
|
+
*/
|
|
170
|
+
private workOrderId;
|
|
81
171
|
private onOrderDef;
|
|
82
172
|
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
83
173
|
private allocateDef;
|